From cef6157803320544651bfc96457d2f8a6df0abd6 Mon Sep 17 00:00:00 2001 From: Olivier Galibert Date: Mon, 14 Apr 2025 11:31:53 +0200 Subject: New sound infrastructure. Should be added soon: - mute - lua hookup (with documentation) - speaker/microphone resampling To be added a little later: - compression - reverb Needs to be added by someone else: - coreaudio - direct - portaudio - xaudio2 - js --- src/emu/sound.cpp | 3276 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 2044 insertions(+), 1232 deletions(-) (limited to 'src/emu/sound.cpp') diff --git a/src/emu/sound.cpp b/src/emu/sound.cpp index c9626855197..2cad8289f80 100644 --- a/src/emu/sound.cpp +++ b/src/emu/sound.cpp @@ -10,6 +10,9 @@ #include "emu.h" +#include "audio_effects/aeffect.h" +#include "resampler.h" + #include "config.h" #include "emuopts.h" #include "main.h" @@ -20,1124 +23,999 @@ #include "osdepend.h" +#include //************************************************************************** // DEBUGGING //************************************************************************** -//#define VERBOSE 1 -#define LOG_OUTPUT_FUNC osd_printf_debug +#define LOG_OUTPUT_FUNC m_machine.logerror -#include "logmacro.h" +#define LOG_OSD_INFO (1U << 1) +#define LOG_MAPPING (1U << 2) +#define LOG_OSD_STREAMS (1U << 3) +#define LOG_ORDER (1U << 4) -#define LOG_OUTPUT_WAV (0) +#define VERBOSE -1 + +#include "logmacro.h" -//************************************************************************** -// GLOBAL VARIABLES -//************************************************************************** const attotime sound_manager::STREAMS_UPDATE_ATTOTIME = attotime::from_hz(STREAMS_UPDATE_FREQUENCY); -//************************************************************************** -// STREAM BUFFER -//************************************************************************** -//------------------------------------------------- -// stream_buffer - constructor -//------------------------------------------------- -stream_buffer::stream_buffer(u32 sample_rate) : - m_end_second(0), - m_end_sample(0), - m_sample_rate(sample_rate), - m_sample_attos((sample_rate == 0) ? ATTOSECONDS_PER_SECOND : ((ATTOSECONDS_PER_SECOND + sample_rate - 1) / sample_rate)), - m_buffer(sample_rate) -{ -} +//**// Output buffer management +// 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. -//------------------------------------------------- -// stream_buffer - destructor -//------------------------------------------------- -stream_buffer::~stream_buffer() +template emu::detail::output_buffer_interleaved::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) { -#if (SOUND_DEBUG) - if (m_wav_file) - flush_wav(); -#endif } +template void emu::detail::output_buffer_interleaved::set_buffer_size(u32 buffer_size) +{ + m_buffer.resize(m_channels*buffer_size, 0); +} -//------------------------------------------------- -// set_sample_rate - set a new sample rate for -// this buffer -//------------------------------------------------- - -void stream_buffer::set_sample_rate(u32 rate, bool resample) +template void emu::detail::output_buffer_interleaved::prepare_space(u32 samples) { - // skip if nothing is actually changing - if (rate == m_sample_rate) + if(!m_channels) return; - // force resampling off if coming to or from an invalid rate, or if we're at time 0 (startup) - sound_assert(rate >= SAMPLE_RATE_MINIMUM - 1); - if (rate < SAMPLE_RATE_MINIMUM || m_sample_rate < SAMPLE_RATE_MINIMUM || (m_end_second == 0 && m_end_sample == 0)) - resample = false; - - // note the time and period of the current buffer (end_time is AFTER the final sample) - attotime prevperiod = sample_period(); - attotime prevend = end_time(); - - // compute the time and period of the new buffer - attotime newperiod = attotime(0, (ATTOSECONDS_PER_SECOND + rate - 1) / rate); - attotime newend = attotime(prevend.seconds(), (prevend.attoseconds() / newperiod.attoseconds()) * newperiod.attoseconds()); - - // buffer a short runway of previous samples; in order to support smooth - // sample rate changes (needed by, e.g., Q*Bert's Votrax), we buffer a few - // samples at the previous rate, and then reconstitute them resampled - // (via simple point sampling) at the new rate. The litmus test is the - // voice when jumping off the edge in Q*Bert; without this extra effort - // it is crackly and/or glitchy at times - sample_t buffer[64]; - int buffered_samples = std::min(m_sample_rate, std::min(rate, u32(std::size(buffer)))); - - // if the new rate is lower, downsample into our holding buffer; - // otherwise just copy into our holding buffer for later upsampling - bool new_rate_higher = (rate > m_sample_rate); - if (resample) - { - if (!new_rate_higher) - backfill_downsample(&buffer[0], buffered_samples, newend, newperiod); - else - { - u32 end = m_end_sample; - for (int index = 0; index < buffered_samples; index++) - { - end = prev_index(end); -#if (SOUND_DEBUG) - // multiple resamples can occur before clearing out old NaNs so - // neuter them for this specific case - if (std::isnan(m_buffer[end])) - buffer[index] = 0; - else -#endif - buffer[index] = get(end); - } - } - } - - // ensure our buffer is large enough to hold a full second at the new rate - if (m_buffer.size() < rate) - m_buffer.resize(rate); - - // set the new rate - m_sample_rate = rate; - m_sample_attos = newperiod.attoseconds(); - - // compute the new end sample index based on the buffer time - m_end_sample = time_to_buffer_index(prevend, false, true); - - // if the new rate is higher, upsample from our temporary buffer; - // otherwise just copy our previously-downsampled data - if (resample) - { -#if (SOUND_DEBUG) - // for aggressive debugging, fill the buffer with NANs to catch anyone - // reading beyond what we resample below - fill(NAN); -#endif - - if (new_rate_higher) - backfill_upsample(&buffer[0], buffered_samples, prevend, prevperiod); - else - { - u32 end = m_end_sample; - for (int index = 0; index < buffered_samples; index++) - { - end = prev_index(end); - put(end, buffer[index]); - } - } + // 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; } - // if not resampling, clear the buffer - else - fill(0); + // 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); } - -//------------------------------------------------- -// open_wav - open a WAV file for logging purposes -//------------------------------------------------- - -#if (SOUND_DEBUG) -void stream_buffer::open_wav(char const *filename) +template void emu::detail::output_buffer_interleaved::commit(u32 samples) { - // always open at 48k so that sound programs can handle it - // re-sample as needed - m_wav_file = util::wav_open(filename, 48000, 1); + m_write_position += samples; } -#endif +template void emu::detail::output_buffer_interleaved::sync() +{ + m_sync_sample += m_write_position - m_sync_position; + m_sync_position = m_write_position; +} -//------------------------------------------------- -// flush_wav - flush data to the WAV file -//------------------------------------------------- - -#if (SOUND_DEBUG) -void stream_buffer::flush_wav() +template emu::detail::output_buffer_flat::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) { - // skip if no file - if (!m_wav_file) - return; + for(auto &b : m_buffer) + b.resize(buffer_size, 0); +} - // grab a view of the data from the last-written point - read_stream_view view(this, m_last_written, m_end_sample, 1.0f); - m_last_written = m_end_sample; +template void emu::detail::output_buffer_flat::register_save_state(device_t &device, const char *id1, const char *id2) +{ + auto &save = device.machine().save(); - // iterate over chunks for conversion - s16 buffer[1024]; - for (int samplebase = 0; samplebase < view.samples(); samplebase += std::size(buffer)) - { - // clamp to the buffer size - int cursamples = view.samples() - samplebase; - if (cursamples > std::size(buffer)) - cursamples = std::size(buffer); + for(unsigned int i=0; i != m_buffer.size(); i++) + save.save_item(&device, id1, id2, i, NAME(m_buffer[i])); - // convert and fill - for (int sampindex = 0; sampindex < cursamples; sampindex++) - buffer[sampindex] = s16(view.get(samplebase + sampindex) * 32768.0); + 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)); - // write to the WAV - util::wav_add_data_16(*m_wav_file, buffer, cursamples); - } } -#endif - -//------------------------------------------------- -// index_time - return the attotime of a given -// index within the buffer -//------------------------------------------------- - -attotime stream_buffer::index_time(s32 index) const +template void emu::detail::output_buffer_flat::set_buffer_size(u32 buffer_size) { - index = clamp_index(index); - return attotime(m_end_second - ((index > m_end_sample) ? 1 : 0), index * m_sample_attos); + for(auto &b : m_buffer) + b.resize(buffer_size, 0); } - -//------------------------------------------------- -// time_to_buffer_index - given an attotime, -// return the buffer index corresponding to it -//------------------------------------------------- - -u32 stream_buffer::time_to_buffer_index(attotime time, bool round_up, bool allow_expansion) +template void emu::detail::output_buffer_flat::prepare_space(u32 samples) { - // compute the sample index within the second - int sample = (time.attoseconds() + (round_up ? (m_sample_attos - 1) : 0)) / m_sample_attos; - sound_assert(sample >= 0 && sample <= size()); - - // if the time is past the current end, make it the end - if (time.seconds() > m_end_second || (time.seconds() == m_end_second && sample > m_end_sample)) - { - sound_assert(allow_expansion); - - m_end_sample = sample; - m_end_second = time.m_seconds; + if(!m_channels) + return; - // due to round_up, we could tweak over the line into the next second - if (sample >= size()) - { - m_end_sample -= size(); - m_end_second++; - } + // 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; } - // if the time is before the start, fail - if (time.seconds() + 1 < m_end_second || (time.seconds() + 1 == m_end_second && sample < m_end_sample)) - throw emu_fatalerror("Attempt to create an out-of-bounds view"); - - return clamp_index(sample); + // 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 void emu::detail::output_buffer_flat::commit(u32 samples) +{ + m_write_position += samples; +} -//------------------------------------------------- -// backfill_downsample - this is called BEFORE -// the sample rate change to downsample from the -// end of the current buffer into a temporary -// holding location -//------------------------------------------------- - -void stream_buffer::backfill_downsample(sample_t *dest, int samples, attotime newend, attotime newperiod) +template void emu::detail::output_buffer_flat::sync() { - // compute the time of the first sample to be backfilled; start one period before - attotime time = newend - newperiod; + m_sync_sample += m_write_position - m_sync_position; + m_sync_position = m_write_position; +} - // loop until we run out of buffered data - int dstindex; - for (dstindex = 0; dstindex < samples && time.seconds() >= 0; dstindex++) - { - u32 srcindex = time_to_buffer_index(time, false); -#if (SOUND_DEBUG) - // multiple resamples can occur before clearing out old NaNs so - // neuter them for this specific case - if (std::isnan(m_buffer[srcindex])) - dest[dstindex] = 0; +template void emu::detail::output_buffer_flat::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 -#endif - dest[dstindex] = get(srcindex); - time -= newperiod; + for(u32 channel = 0; channel != m_channels; channel++) + std::fill(m_buffer[channel].begin(), m_buffer[channel].begin() + m_history, 0.0); + + m_write_position += delta; + m_sync_position = m_history; } - for ( ; dstindex < samples; dstindex++) - dest[dstindex] = 0; } +template void emu::detail::output_buffer_flat::resample(u32 previous_rate, u32 next_rate, attotime sync_time, attotime now) +{ + if(!m_write_position) + return; -//------------------------------------------------- -// backfill_upsample - this is called AFTER the -// sample rate change to take a copied buffer -// of samples at the old rate and upsample them -// to the new (current) rate -//------------------------------------------------- + auto si = [](attotime time, u32 rate) -> s64 { + return time.m_seconds * rate + ((time.m_attoseconds / 100000000) * rate) / 10000000000; + }; + + auto cv = [](u32 source_rate, u32 dest_rate, s64 time) -> std::pair { + 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); + } -void stream_buffer::backfill_upsample(sample_t const *src, int samples, attotime prevend, attotime prevperiod) -{ - // compute the time of the first sample to be backfilled; start one period before - attotime time = end_time() - sample_period(); + 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); - // also adjust the buffered sample end time to point to the sample time of the - // final sample captured - prevend -= prevperiod; + double step = double(previous_rate) / double(next_rate); + u32 pindex = ppos - pbase; + u32 nend = nwrite - nbase; - // loop until we run out of buffered data - u32 end = m_end_sample; - int srcindex = 0; - while (1) - { - // if our backfill time is before the current buffered sample time, - // back up until we have a sample that covers this time - while (time < prevend && srcindex < samples) - { - prevend -= prevperiod; - srcindex++; - } + // 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 - // stop when we run out of source - if (srcindex >= samples) - break; + std::vector 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 - // write this sample at the pevious position - end = prev_index(end); - put(end, src[srcindex]); + 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; - // back up to the next sample time - time -= sample_period(); + 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 +} -//************************************************************************** -// SOUND STREAM OUTPUT -//************************************************************************** +template class emu::detail::output_buffer_flat; +template class emu::detail::output_buffer_interleaved; -//------------------------------------------------- -// sound_stream_output - constructor -//------------------------------------------------- -sound_stream_output::sound_stream_output() : - m_stream(nullptr), - m_index(0), - m_gain(1.0) +// 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) { } -//------------------------------------------------- -// init - initialization -//------------------------------------------------- +//**// Streams and routes -void sound_stream_output::init(sound_stream &stream, u32 index, char const *tag) +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_sync_timer(nullptr), + m_callback(std::move(callback)) { - // set the passed-in data - m_stream = &stream; - m_index = index; + sound_assert(outputs > 0 || inputs > 0); - // save our state - auto &save = stream.device().machine().save(); - save.save_item(&stream.device(), "stream.output", tag, index, NAME(m_gain)); + // create a name + m_name = m_device.name(); + m_name += " '"; + m_name += m_device.tag(); + m_name += "'"; -#if (LOG_OUTPUT_WAV) - std::string filename = stream.device().machine().basename(); - filename += stream.device().tag(); - for (int index = 0; index < filename.size(); index++) - if (filename[index] == ':') - filename[index] = '_'; - if (dynamic_cast(&stream) != nullptr) - filename += "_resampler"; - filename += "_OUT_"; - char buf[10]; - sprintf(buf, "%d", index); - filename += buf; - filename += ".wav"; - m_buffer.open_wav(filename.c_str()); -#endif -} + // 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; -//------------------------------------------------- -// name - return the friendly name of this output -//------------------------------------------------- +} -std::string sound_stream_output::name() const +sound_stream::~sound_stream() { - // start with our owning stream's name - std::ostringstream str; - util::stream_format(str, "%s Ch.%d", m_stream->name(), m_stream->output_base() + m_index); - return str.str(); } - -//------------------------------------------------- -// optimize_resampler - optimize resamplers by -// either returning the native rate or another -// input's resampler if they can be reused -//------------------------------------------------- - -sound_stream_output &sound_stream_output::optimize_resampler(sound_stream_output *input_resampler) +void sound_stream::add_bw_route(sound_stream *source, int output, int input, float gain) { - // if no resampler, or if the resampler rate matches our rate, return ourself - if (input_resampler == nullptr || buffer_sample_rate() == input_resampler->buffer_sample_rate()) - return *this; - - // scan our list of resamplers to see if there's another match - for (auto &resampler : m_resampler_list) - if (resampler->buffer_sample_rate() == input_resampler->buffer_sample_rate()) - return *resampler; - - // add the input to our list and return the one we were given back - m_resampler_list.push_back(input_resampler); - return *input_resampler; + m_bw_routes.emplace_back(route_bw(source, output, input, gain)); } - - -//************************************************************************** -// SOUND STREAM INPUT -//************************************************************************** - -//------------------------------------------------- -// sound_stream_input - constructor -//------------------------------------------------- - -sound_stream_input::sound_stream_input() : - m_owner(nullptr), - m_native_source(nullptr), - m_resampler_source(nullptr), - m_index(0), - m_gain(1.0), - m_user_gain(1.0) +void sound_stream::add_fw_route(sound_stream *target, int input, int output) { + m_fw_routes.emplace_back(route_fw(target, input, output)); } - -//------------------------------------------------- -// init - initialization -//------------------------------------------------- - -void sound_stream_input::init(sound_stream &stream, u32 index, char const *tag, sound_stream_output *resampler) +bool sound_stream::set_route_gain(sound_stream *source, int source_channel, int target_channel, float gain) { - // set the passed-in values - m_owner = &stream; - m_index = index; - m_resampler_source = resampler; - - // save our state - auto &save = stream.device().machine().save(); - save.save_item(&stream.device(), "stream.input", tag, index, NAME(m_gain)); - save.save_item(&stream.device(), "stream.input", tag, index, NAME(m_user_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 false; } - -//------------------------------------------------- -// name - return the friendly name of this input -//------------------------------------------------- - -std::string sound_stream_input::name() const +std::vector sound_stream::sources() const { - // start with our owning stream's name - std::ostringstream str; - util::stream_format(str, "%s", m_owner->name()); - - // if we have a source, indicate where the sound comes from by device name and tag - if (valid()) - util::stream_format(str, " <- %s", m_native_source->name()); - return str.str(); + std::vector 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; } - -//------------------------------------------------- -// set_source - wire up the output source for -// our consumption -//------------------------------------------------- - -void sound_stream_input::set_source(sound_stream_output *source) +std::vector sound_stream::targets() const { - m_native_source = source; - if (m_resampler_source != nullptr) - m_resampler_source->stream().set_input(0, &source->stream(), source->index()); + std::vector 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; } - -//------------------------------------------------- -// update - update our source's stream to the -// current end time and return a view to its -// contents -//------------------------------------------------- - -read_stream_view sound_stream_input::update(attotime start, attotime end) +void sound_stream::register_state() { - // shouldn't get here unless valid - sound_assert(valid()); + // create a unique tag for saving + m_state_tag = string_format("%d", m_device.machine().sound().unique_id()); + auto &save = m_device.machine().save(); - // pick an optimized resampler - sound_stream_output &source = m_native_source->optimize_resampler(m_resampler_source); + 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 - // if not using our own resampler, keep it up to date in case we need to invoke it later - if (m_resampler_source != nullptr && &source != m_resampler_source) - m_resampler_source->set_end_time(end); + m_output_buffer.register_save_state(m_device, "stream.sound_stream.output_buffer", m_state_tag.c_str()); - // update the source, returning a view of the needed output over the start and end times - return source.stream().update_view(start, end, source.index()).apply_gain(m_gain * m_user_gain * source.gain()); + 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"); } -//------------------------------------------------- -// apply_sample_rate_changes - tell our sources -// to apply any sample rate changes, informing -// them of our current rate -//------------------------------------------------- - -void sound_stream_input::apply_sample_rate_changes(u32 updatenum, u32 downstream_rate) +void sound_stream::compute_dependants() { - // shouldn't get here unless valid - sound_assert(valid()); - - // if we have a resampler, tell it (and it will tell the native source) - if (m_resampler_source != nullptr) - m_resampler_source->stream().apply_sample_rate_changes(updatenum, downstream_rate); - - // otherwise, just tell the native source directly - else - m_native_source->stream().apply_sample_rate_changes(updatenum, downstream_rate); + m_dependant_streams.clear(); + for(const route_bw &r : m_bw_routes) + r.m_source->add_dependants(m_dependant_streams); } +void sound_stream::add_dependants(std::vector &deps) +{ + 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); +} -//************************************************************************** -// SOUND STREAM -//************************************************************************** +//**// Stream sample rate -//------------------------------------------------- -// sound_stream - private common constructor -//------------------------------------------------- +void sound_stream::set_sample_rate(u32 new_rate) +{ + m_input_adaptive = m_output_adaptive = false; + internal_set_sample_rate(new_rate); +} -sound_stream::sound_stream(device_t &device, u32 inputs, u32 outputs, u32 output_base, u32 sample_rate, sound_stream_flags flags) : - m_device(device), - m_next(nullptr), - m_sample_rate((sample_rate < SAMPLE_RATE_MINIMUM) ? (SAMPLE_RATE_MINIMUM - 1) : (sample_rate < SAMPLE_RATE_OUTPUT_ADAPTIVE) ? sample_rate : 48000), - m_pending_sample_rate(SAMPLE_RATE_INVALID), - m_last_sample_rate_update(0), - m_input_adaptive(sample_rate == SAMPLE_RATE_INPUT_ADAPTIVE), - m_output_adaptive(sample_rate == SAMPLE_RATE_OUTPUT_ADAPTIVE), - m_synchronous((flags & STREAM_SYNCHRONOUS) != 0), - m_resampling_disabled((flags & STREAM_DISABLE_INPUT_RESAMPLING) != 0), - m_sync_timer(nullptr), - m_last_update_end_time(attotime::zero), - m_input(inputs), - m_input_view(inputs), - m_empty_buffer(100), - m_output_base(output_base), - m_output(outputs), - m_output_view(outputs) +void sound_stream::internal_set_sample_rate(u32 new_rate) { - sound_assert(outputs > 0); + 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(); - // create a name - m_name = m_device.name(); - m_name += " '"; - m_name += m_device.tag(); - m_name += "'"; + } else + m_sample_rate = new_rate; +} - // create a unique tag for saving - std::string 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", state_tag.c_str(), 0, NAME(m_sample_rate)); - save.save_item(&m_device, "stream.sound_stream", state_tag.c_str(), 0, NAME(m_last_update_end_time)); - save.register_postload(save_prepost_delegate(FUNC(sound_stream::postload), this)); - save.register_presave(save_prepost_delegate(FUNC(sound_stream::presave), this)); +bool sound_stream::try_solving_frequency() +{ + if(frequency_is_solved()) + return false; - // initialize all inputs - for (unsigned int inputnum = 0; inputnum < m_input.size(); inputnum++) - { - // allocate a resampler stream if needed, and get a pointer to its output - sound_stream_output *resampler = nullptr; - if (!m_resampling_disabled) - { - m_resampler_list.push_back(std::make_unique(m_device)); - resampler = &m_resampler_list.back()->m_output[0]; + 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; - // add the new input - m_input[inputnum].init(*this, inputnum, state_tag.c_str(), resampler); + m_sample_rate = freqfw > freqbw ? freqfw : freqbw; + return true; } - - // initialize all outputs - for (unsigned int outputnum = 0; outputnum < m_output.size(); outputnum++) - m_output[outputnum].init(*this, outputnum, state_tag.c_str()); - - // create an update timer for synchronous streams - if (synchronous()) - m_sync_timer = m_device.timer_alloc(FUNC(sound_stream::sync_update), this); - - // force an update to the sample rates - sample_rate_changed(); } -//------------------------------------------------- -// sound_stream - constructor -//------------------------------------------------- +//**// Stream flow and updates -sound_stream::sound_stream(device_t &device, u32 inputs, u32 outputs, u32 output_base, u32 sample_rate, stream_update_delegate callback, sound_stream_flags flags) : - sound_stream(device, inputs, outputs, output_base, sample_rate, flags) +void sound_stream::init() { - m_callback_ex = std::move(callback); -} - + // 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); -//------------------------------------------------- -// ~sound_stream - destructor -//------------------------------------------------- + m_output_buffer.set_buffer_size(bsize); -sound_stream::~sound_stream() -{ + m_samples_to_update = 0; + m_started = true; + if(synchronous()) + reprime_sync_timer(); } - -//------------------------------------------------- -// set_sample_rate - set the sample rate on a -// given stream -//------------------------------------------------- - -void sound_stream::set_sample_rate(u32 new_rate) +u64 sound_stream::get_current_sample_index() const { - // we will update this on the next global update - if (new_rate != sample_rate()) - m_pending_sample_rate = new_rate; + attotime now = m_device.machine().time(); + return now.m_seconds * m_sample_rate + ((now.m_attoseconds / 1000000000) * m_sample_rate) / 1000000000; } - -//------------------------------------------------- -// set_input - configure a stream's input -//------------------------------------------------- - -void sound_stream::set_input(int index, sound_stream *input_stream, int output_index, float gain) +void sound_stream::update() { - LOG("stream_set_input(%p, '%s', %d, %p, %d, %f)\n", (void *)this, m_device.tag(), - index, (void *)input_stream, output_index, gain); + if(!is_active()) + return; - // 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())); + // 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(); - // 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())); + if(m_samples_to_update <= 0) + return; - // wire it up - m_input[index].set_source((input_stream != nullptr) ? &input_stream->m_output[output_index] : nullptr); - m_input[index].set_gain(gain); + // If there's anything to do, well, do it, starting with the dependencies + for(auto &stream : m_dependant_streams) + stream->update_nodeps(); - // update sample rates now that we know the input - sample_rate_changed(); + do_update(); } - -//------------------------------------------------- -// update - force a stream to update to -// the current emulated time -//------------------------------------------------- - -void sound_stream::update() +void sound_stream::update_nodeps() { - // ignore any update requests if we're already up to date - attotime start = m_output[0].end_time(); - attotime end = m_device.machine().time(); - if (start >= end) + if(!is_active()) + return; + + // 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(m_samples_to_update <= 0) return; - // regular update then - update_view(start, end); + // If there's anything to do, well, do it + do_update(); } +void sound_stream::create_resamplers() +{ + if(!is_active()) { + for(auto &r : m_bw_routes) + r.m_resampler = nullptr; + return; + } -//------------------------------------------------- -// update_view - force a stream to update to -// the current emulated time and return a view -// to the generated samples from the given -// output number -//------------------------------------------------- + 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; +} -read_stream_view sound_stream::update_view(attotime start, attotime end, u32 outputnum) +void sound_stream::lookup_history_sizes() { - sound_assert(start <= end); - sound_assert(outputnum < m_output.size()); + 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; + } - // clean up parameters for when the asserts go away - if (outputnum >= m_output.size()) - outputnum = 0; - if (start > end) - start = end; + m_output_buffer.set_history(history); +} - auto profile = g_profiler.start(PROFILER_SOUND); +u32 sound_stream::get_history_for_bw_route(const sound_stream *source, u32 channel) const +{ + 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; +} - // reposition our start to coincide with the current buffer end - attotime update_start = m_output[outputnum].end_time(); - if (update_start <= end) - { - // create views for all the outputs - for (unsigned int outindex = 0; outindex < m_output.size(); outindex++) - m_output_view[outindex] = m_output[outindex].view(update_start, end); - - // skip if nothing to do - u32 samples = m_output_view[0].samples(); - sound_assert(samples >= 0); - if (samples != 0 && m_sample_rate >= SAMPLE_RATE_MINIMUM) - { - sound_assert(!synchronous() || samples == 1); - - // ensure all input streams are up to date, and create views for them as well - for (unsigned int inputnum = 0; inputnum < m_input.size(); inputnum++) - { - if (m_input[inputnum].valid()) - m_input_view[inputnum] = m_input[inputnum].update(update_start, end); - else - m_input_view[inputnum] = empty_view(update_start, end); - sound_assert(m_input_view[inputnum].samples() > 0); - sound_assert(m_resampling_disabled || m_input_view[inputnum].sample_rate() == m_sample_rate); +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; } + } + } -#if (SOUND_DEBUG) - // clear each output view to NANs before we call the callback - for (unsigned int outindex = 0; outindex < m_output.size(); outindex++) - m_output_view[outindex].fill(NAN); -#endif - - // if we have an extended callback, that's all we need - m_callback_ex(*this, m_input_view, m_output_view); + // Prepare the output space (if any) + m_output_buffer.prepare_space(m_samples_to_update); -#if (SOUND_DEBUG) - // make sure everything was overwritten - for (unsigned int outindex = 0; outindex < m_output.size(); outindex++) - for (int sampindex = 0; sampindex < m_output_view[outindex].samples(); sampindex++) - m_output_view[outindex].get(sampindex); + // Call the callback + m_callback(*this); - for (unsigned int outindex = 0; outindex < m_output.size(); outindex++) - m_output[outindex].m_buffer.flush_wav(); -#endif - } - } + // Update the indexes + m_output_buffer.commit(m_samples_to_update); +} - // return the requested view - return read_stream_view(m_output_view[outputnum], start); +void sound_stream::sync(attotime now) +{ + m_sync_time = now; + m_output_buffer.sync(); } -//------------------------------------------------- -// apply_sample_rate_changes - if there is a -// pending sample rate change, apply it now -//------------------------------------------------- -void sound_stream::apply_sample_rate_changes(u32 updatenum, u32 downstream_rate) -{ - // grab the new rate and invalidate - u32 new_rate = (m_pending_sample_rate != SAMPLE_RATE_INVALID) ? m_pending_sample_rate : m_sample_rate; - m_pending_sample_rate = SAMPLE_RATE_INVALID; - // clamp to the minimum - 1 (anything below minimum means "off" and - // will not call the sound callback at all) - if (new_rate < SAMPLE_RATE_MINIMUM) - new_rate = SAMPLE_RATE_MINIMUM - 1; - // if we're input adaptive, override with the rate of our input - if (input_adaptive() && m_input.size() > 0 && m_input[0].valid()) - new_rate = m_input[0].source().stream().sample_rate(); - // if we're output adaptive, override with the rate of our output - if (output_adaptive()) - { - if (m_last_sample_rate_update == updatenum) - sound_assert(new_rate == m_sample_rate); - else - m_last_sample_rate_update = updatenum; - new_rate = downstream_rate; - } - // if something is different, process the change - if (new_rate != SAMPLE_RATE_INVALID && new_rate != m_sample_rate) - { - // update to the new rate and notify everyone -#if (SOUND_DEBUG) - printf("stream %s changing rates %d -> %d\n", name().c_str(), m_sample_rate, new_rate); -#endif - m_sample_rate = new_rate; - sample_rate_changed(); - } - // now call through our inputs and apply the rate change there - for (auto &input : m_input) - if (input.valid()) - input.apply_sample_rate_changes(updatenum, m_sample_rate); -} -//------------------------------------------------- -// print_graph_recursive - helper for debugging; -// prints info on this stream and then recursively -// prints info on all inputs -//------------------------------------------------- -#if (SOUND_DEBUG) -void sound_stream::print_graph_recursive(int indent, int index) -{ - osd_printf_info("%*s%s Ch.%d @ %d\n", indent, "", name(), index + m_output_base, sample_rate()); - for (int index = 0; index < m_input.size(); index++) - if (m_input[index].valid()) - { - if (m_input[index].m_resampler_source != nullptr) - m_input[index].m_resampler_source->stream().print_graph_recursive(indent + 2, m_input[index].m_resampler_source->index()); - else - m_input[index].m_native_source->stream().print_graph_recursive(indent + 2, m_input[index].m_native_source->index()); - } +attotime sound_stream::sample_to_time(u64 index) const +{ + attotime res = attotime::zero; + res.m_seconds = index / m_sample_rate; + u64 remain = index % m_sample_rate; + res.m_attoseconds = ((remain * 1000000000) / m_sample_rate) * 1000000000; + return res; } -#endif -//------------------------------------------------- -// sample_rate_changed - recompute sample -// rate data, and all streams that are affected -// by this stream -//------------------------------------------------- +//**// Synchronous stream updating -void sound_stream::sample_rate_changed() +void sound_stream::reprime_sync_timer() { - // if invalid, just punt - if (m_sample_rate == SAMPLE_RATE_INVALID) + if(!is_active()) return; - - // update all output buffers - for (auto &output : m_output) - output.sample_rate_changed(m_sample_rate); - - // if synchronous, prime the timer - if (synchronous()) - reprime_sync_timer(); + + u64 next_sample = m_output_buffer.write_sample() + 1; + attotime next_time = sample_to_time(next_sample); + next_time.m_attoseconds += 1000000000; // Go to the next nanosecond + m_sync_timer->adjust(next_time - m_device.machine().time()); } - -//------------------------------------------------- -// postload - save/restore callback -//------------------------------------------------- - -void sound_stream::postload() +void sound_stream::sync_update(s32) { - // set the end time of all of our streams to the value saved in m_last_update_end_time - for (auto &output : m_output) - output.set_end_time(m_last_update_end_time); - - // recompute the sample rate information - sample_rate_changed(); + update(); + reprime_sync_timer(); } -//------------------------------------------------- -// presave - save/restore callback -//------------------------------------------------- -void sound_stream::presave() +//**// 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() { - // save the stream end time - m_last_update_end_time = m_output[0].end_time(); -} + // 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_update)); -//------------------------------------------------- -// reprime_sync_timer - set up the next sync -// timer to go off just a hair after the end of -// the current sample period -//------------------------------------------------- + // 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_stream::reprime_sync_timer() -{ - attotime curtime = m_device.machine().time(); - attotime target = m_output[0].end_time() + attotime(0, 1); - m_sync_timer->adjust(target - curtime); + // mark the generation as "just starting" + m_osd_info.m_generation = 0xffffffff; } - -//------------------------------------------------- -// sync_update - timer callback to handle a -// synchronous stream -//------------------------------------------------- - -void sound_stream::sync_update(s32) +sound_manager::~sound_manager() { - update(); - reprime_sync_timer(); + if(m_effects_thread) { + m_effects_done = true; + m_effects_condition.notify_all(); + m_effects_thread->join(); + m_effects_thread = nullptr; + } } - -//------------------------------------------------- -// empty_view - return an empty view covering the -// given time period as a substitute for invalid -// inputs -//------------------------------------------------- - -read_stream_view sound_stream::empty_view(attotime start, attotime end) +sound_stream *sound_manager::stream_alloc(device_t &device, u32 inputs, u32 outputs, u32 sample_rate, stream_update_delegate callback, sound_stream_flags flags) { - // if our dummy buffer doesn't match our sample rate, update and clear it - if (m_empty_buffer.sample_rate() != m_sample_rate) - m_empty_buffer.set_sample_rate(m_sample_rate, false); - - // allocate a write view so that it can expand, and convert back to a read view - // on the return - return write_stream_view(m_empty_buffer, start, end); + m_stream_list.push_back(std::make_unique(device, inputs, outputs, sample_rate, callback, flags)); + return m_stream_list.back().get(); } +//**// Sound system initialization -//************************************************************************** -// RESAMPLER STREAM -//************************************************************************** +void sound_manager::before_devices_init() +{ + // 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(); -//------------------------------------------------- -// default_resampler_stream - derived sound_stream -// class that handles resampling -//------------------------------------------------- + m_machine.save().register_postload(save_prepost_delegate(FUNC(sound_manager::postload), this)); +} -default_resampler_stream::default_resampler_stream(device_t &device) : - sound_stream(device, 1, 1, 0, SAMPLE_RATE_OUTPUT_ADAPTIVE, stream_update_delegate(&default_resampler_stream::resampler_sound_update, this), STREAM_DISABLE_INPUT_RESAMPLING), - m_max_latency(0) +void sound_manager::postload() { - // create a name - m_name = "Default Resampler '"; - m_name += device.tag(); - m_name += "'"; + std::unique_lock 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; + } } - -//------------------------------------------------- -// resampler_sound_update - stream callback -// handler for resampling an input stream to the -// target sample rate of the output -//------------------------------------------------- - -void default_resampler_stream::resampler_sound_update(sound_stream &stream, std::vector const &inputs, std::vector &outputs) +void sound_manager::after_devices_init() { - sound_assert(inputs.size() == 1); - sound_assert(outputs.size() == 1); + // 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; + } - auto &input = inputs[0]; - auto &output = outputs[0]; + 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); + } - // if the input has an invalid rate, just fill with zeros - if (input.sample_rate() <= 1) - { - output.fill(0); - return; + // 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 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 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 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); } - // optimize_resampler ensures we should not have equal sample rates - sound_assert(input.sample_rate() != output.sample_rate()); + 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()); + } - // compute the stepping value and the inverse - stream_buffer::sample_t step = stream_buffer::sample_t(input.sample_rate()) / stream_buffer::sample_t(output.sample_rate()); - stream_buffer::sample_t stepinv = 1.0 / step; + // 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(); + } - // determine the latency we need to introduce, in input samples: - // 1 input sample for undersampled inputs - // 1 + step input samples for oversampled inputs - s64 latency_samples = 1 + ((step < 1.0) ? 0 : s32(step)); - if (latency_samples <= m_max_latency) - latency_samples = m_max_latency; - else - m_max_latency = latency_samples; - attotime latency = latency_samples * input.sample_period(); - - // clamp the latency to the start (only relevant at the beginning) - s32 dstindex = 0; - attotime output_start = output.start_time(); - auto numsamples = output.samples(); - while (latency > output_start && dstindex < numsamples) - { - output.put(dstindex++, 0); - output_start += output.sample_period(); + for(microphone_device &dev : microphone_device_enumerator(machine().root_device())) { + dev.set_id(m_microphones.size()); + m_microphones.emplace_back(microphone_info(dev)); } - if (dstindex >= numsamples) - return; - // create a rebased input buffer around the adjusted start time - read_stream_view rebased(input, output_start - latency); - sound_assert(rebased.start_time() + latency <= output_start); + // Allocate the buffer to pass for recording + m_record_buffer.resize(m_outputs_count * machine().sample_rate(), 0); + m_record_samples = 0; - // compute the fractional input start position - attotime delta = output_start - (rebased.start_time() + latency); - sound_assert(delta.seconds() == 0); - stream_buffer::sample_t srcpos = stream_buffer::sample_t(double(delta.attoseconds()) / double(rebased.sample_period_attoseconds())); - sound_assert(srcpos <= 1.0f); + // Have all streams create their initial resamplers + for(auto &stream : m_stream_list) + stream->create_resamplers(); - // input is undersampled: point sample except where our sample period covers a boundary - s32 srcindex = 0; - if (step < 1.0) - { - stream_buffer::sample_t cursample = rebased.get(srcindex++); - for ( ; dstindex < numsamples; dstindex++) - { - // if still within the current sample, just replicate - srcpos += step; - if (srcpos <= 1.0) - output.put(dstindex, cursample); - - // if crossing a sample boundary, blend with the neighbor - else - { - srcpos -= 1.0; - sound_assert(srcpos <= step + 1e-5); - stream_buffer::sample_t prevsample = cursample; - cursample = rebased.get(srcindex++); - output.put(dstindex, stepinv * (prevsample * (step - srcpos) + srcpos * cursample)); + // Then get the initial history sizes + for(auto &stream : m_stream_list) + stream->lookup_history_sizes(); + + m_effects_done = false; + + m_effects_thread = std::make_unique( + [this]{ run_effects(); }); +} + + +//**// Effects, input and output management + +void sound_manager::input_get(int id, sound_stream &stream) +{ + 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; + } } - sound_assert(srcindex <= rebased.samples()); } +} - // input is oversampled: sum the energy - else - { - float cursample = rebased.get(srcindex++); - for ( ; dstindex < numsamples; dstindex++) - { - // compute the partial first sample and advance - stream_buffer::sample_t scale = 1.0 - srcpos; - stream_buffer::sample_t sample = cursample * scale; - - // add in complete samples until we only have a fraction left - stream_buffer::sample_t remaining = step - scale; - while (remaining >= 1.0) - { - sample += rebased.get(srcindex++); - remaining -= 1.0; - } - - // add in the final partial sample - cursample = rebased.get(srcindex++); - sample += cursample * remaining; - output.put(dstindex, sample * stepinv); - - // our position is now the remainder - srcpos = remaining; - sound_assert(srcindex <= rebased.samples()); +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; } } } +void sound_manager::run_effects() +{ + std::unique_lock 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(); + } + // 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); -//************************************************************************** -// SOUND MANAGER -//************************************************************************** + 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; -//------------------------------------------------- -// sound_manager - constructor -//------------------------------------------------- + switch(step.m_mode) { + case mixing_step::CLEAR: + for(u32 sample = 0; sample != samples; sample++) { + *dest = 0; + dest += skip; + } + break; -sound_manager::sound_manager(running_machine &machine) : - m_machine(machine), - m_update_timer(nullptr), - m_update_number(0), - m_last_update(attotime::zero), - m_finalmix_leftover(0), - m_samples_this_update(0), - m_finalmix(machine.sample_rate()), - m_leftmix(machine.sample_rate()), - m_rightmix(machine.sample_rate()), - m_compressor_scale(1.0), - m_compressor_counter(0), - m_compressor_enabled(machine.options().compressor()), - m_muted(0), - m_nosound_mode(machine.osd().no_sound()), - m_attenuation(0), - m_unique_id(0), - m_wavfile(), - m_first_reset(true) -{ - // count the mixers -#if VERBOSE - mixer_interface_enumerator iter(machine.root_device()); - LOG("total mixers = %d\n", iter.count()); -#endif + 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; + } - // 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)); + 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; + } + } + } - // register global states - machine.save().save_item(NAME(m_last_update)); + for(auto &si : m_speakers) + si.m_effects.back().m_buffer.sync(); - // set the starting attenuation - set_attenuation(machine.options().volume()); + // 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); + } +} - // 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); +std::string sound_manager::effect_chain_tag(s32 index) const +{ + return m_speakers[index].m_dev.tag(); } +std::vector sound_manager::effect_chain(s32 index) const +{ + std::vector res; + for(const auto &e : m_speakers[index].m_effects) + res.push_back(e.m_effect.get()); + return res; +} -//------------------------------------------------- -// sound_manager - destructor -//------------------------------------------------- +std::vector sound_manager::default_effect_chain() const +{ + std::vector res; + for(const auto &e : m_default_effects) + res.push_back(e.get()); + return res; +} -sound_manager::~sound_manager() +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(); } -//------------------------------------------------- -// stream_alloc - allocate a new stream with the -// new-style callback and flags -//------------------------------------------------- -sound_stream *sound_manager::stream_alloc(device_t &device, u32 inputs, u32 outputs, u32 sample_rate, stream_update_delegate callback, sound_stream_flags flags) -{ - // determine output base - u32 output_base = 0; - for (auto &stream : m_stream_list) - if (&stream->device() == &device) - output_base += stream->output_count(); - m_stream_list.push_back(std::make_unique(device, inputs, outputs, output_base, sample_rate, callback, flags)); - return m_stream_list.back().get(); -} + + + + //------------------------------------------------- @@ -1146,9 +1024,9 @@ sound_stream *sound_manager::stream_alloc(device_t &device, u32 inputs, u32 outp bool sound_manager::start_recording(std::string_view filename) { - if (m_wavfile) + if(m_wavfile) return false; - m_wavfile = util::wav_open(filename, machine().sample_rate(), 2); + m_wavfile = util::wav_open(filename, machine().sample_rate(), m_outputs_count); return bool(m_wavfile); } @@ -1172,480 +1050,1414 @@ void sound_manager::stop_recording() //------------------------------------------------- -// set_attenuation - set the global volume +// mute - mute sound output //------------------------------------------------- -void sound_manager::set_attenuation(float attenuation) +void sound_manager::mute(bool mute, u8 reason) { - // currently OSD only supports integral attenuation - m_attenuation = int(attenuation); - machine().osd().set_mastervolume(m_muted ? -32 : m_attenuation); + if(mute) + m_muted |= reason; + else + m_muted &= ~reason; } //------------------------------------------------- -// indexed_mixer_input - return the mixer -// device and input index of the global mixer -// input +// reset - reset all sound chips //------------------------------------------------- -bool sound_manager::indexed_mixer_input(int index, mixer_input &info) const +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()) { - // scan through the mixers until we find the indexed input - for (device_mixer_interface &mixer : mixer_interface_enumerator(machine().root_device())) - { - if (index < mixer.inputs()) - { - info.mixer = &mixer; - info.stream = mixer.input_to_stream_input(index, info.inputnum); - sound_assert(info.stream != nullptr); - return true; - } - index -= mixer.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())); +} - // didn't locate - info.mixer = nullptr; - return false; +sound_manager::microphone_info::microphone_info(microphone_device &dev) : m_dev(dev) +{ + m_channels = dev.outputs(); +} + +void sound_manager::reset() +{ + LOG_OUTPUT_FUNC("Sound reset\n"); } //------------------------------------------------- -// samples - fills the specified buffer with -// 16-bit stereo audio samples generated during -// the current frame +// pause - pause sound output //------------------------------------------------- -void sound_manager::samples(s16 *buffer) +void sound_manager::pause() { - for (int sample = 0; sample < m_samples_this_update * 2; sample++) - *buffer++ = m_finalmix[sample]; + mute(true, MUTE_REASON_PAUSE); } //------------------------------------------------- -// mute - mute sound output +// resume - resume sound output //------------------------------------------------- -void sound_manager::mute(bool mute, u8 reason) +void sound_manager::resume() { - bool old_muted = m_muted; - if (mute) - m_muted |= reason; - else - m_muted &= ~reason; - - if(old_muted != (m_muted != 0)) - set_attenuation(m_attenuation); + mute(false, MUTE_REASON_PAUSE); } -//------------------------------------------------- -// recursive_remove_stream_from_orphan_list - -// remove the given stream from the orphan list -// and recursively remove all our inputs -//------------------------------------------------- +//**// Configuration management -void sound_manager::recursive_remove_stream_from_orphan_list(sound_stream *which) +void sound_manager::config_load(config_type cfg_type, config_level cfg_level, util::xml::data_node const *parentnode) { - m_orphan_stream_list.erase(which); - for (int inputnum = 0; inputnum < which->input_count(); inputnum++) - { - auto &input = which->input(inputnum); - if (input.valid()) - recursive_remove_stream_from_orphan_list(&input.source().stream()); + // If no config file, ignore + if(!parentnode) + return; + + switch(cfg_type) { + case config_type::INIT: + break; + + case config_type::CONTROLLER: + break; + + case config_type::DEFAULT: { + // In the global config, get the default effect chain configuration + + util::xml::data_node const *efl_node = parentnode->get_child("default_audio_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[m_default_effects[id-1]->type()] == type) + m_default_effects[id-1]->config_load(ef_node); + } + break; + } + + 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; + } + } + + // 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); + + 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(m_machine.root_device().subdevice(device_tag)); + if(intf) + intf->set_user_output_gain(lv_node->get_attribute_float("gain", 1.0)); + } + + 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(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)); + } + + + // 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(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(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; + } + + case config_type::FINAL: + break; } } //------------------------------------------------- -// apply_sample_rate_changes - recursively -// update sample rates throughout the system +// config_save - save data to the configuration +// file //------------------------------------------------- -void sound_manager::apply_sample_rate_changes() +void sound_manager::config_save(config_type cfg_type, util::xml::data_node *parentnode) { - // update sample rates if they have changed - for (speaker_device &speaker : speaker_device_enumerator(machine().root_device())) - { - int stream_out; - sound_stream *stream = speaker.output_to_stream_output(0, stream_out); - - // due to device removal, some speakers may end up with no outputs; just skip those - if (stream != nullptr) - { - sound_assert(speaker.outputs() == 1); - stream->apply_sample_rate_changes(m_update_number, machine().sample_rate()); + 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); } + break; + } + + 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); + } + } + + // 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(&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)); + } + } + + // 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; } } -//------------------------------------------------- -// reset - reset all sound chips -//------------------------------------------------- -void sound_manager::reset() +//**// Mapping between speakers/microphones and OSD endpoints + +sound_manager::config_mapping &sound_manager::config_get_sound_io(sound_io_device *dev) { - // reset all the sound chips - for (device_sound_interface &sound : sound_interface_enumerator(machine().root_device())) - sound.device().reset(); + 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(); +} - // apply any sample rate changes now - apply_sample_rate_changes(); +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 --; +} - // on first reset, identify any orphaned streams - if (m_first_reset) - { - m_first_reset = false; +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(name, db)); +} - // put all the streams on the orphan list to start - for (auto &stream : m_stream_list) - m_orphan_stream_list[stream.get()] = 0; +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 --; +} - // then walk the graph like we do on update and remove any we touch - for (speaker_device &speaker : speaker_device_enumerator(machine().root_device())) - { - int dummy; - sound_stream *const output = speaker.output_to_stream_output(0, dummy); - if (output) - recursive_remove_stream_from_orphan_list(output); +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("", db)); +} - m_speakers.emplace_back(speaker); +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 --; +} + +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; } +} -#if (SOUND_DEBUG) - // dump the sound graph when we start up - for (speaker_device &speaker : speaker_device_enumerator(machine().root_device())) - { - int index; - sound_stream *output = speaker.output_to_stream_output(0, index); - if (output != nullptr) - output->print_graph_recursive(0, index); +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 --; +} + +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; } +} - // dump the orphan list as well - if (m_orphan_stream_list.size() != 0) - { - osd_printf_info("\nOrphaned streams:\n"); - for (auto &stream : m_orphan_stream_list) - osd_printf_info(" %s\n", stream.first->name()); +void sound_manager::config_set_volume_sound_io_connection_node(sound_io_device *dev, std::string name, float db) +{ + 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; } -#endif - } } +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 --; +} -//------------------------------------------------- -// pause - pause sound output -//------------------------------------------------- +void sound_manager::internal_config_set_volume_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 == "") { + nmap.second = db; + return; + } +} -void sound_manager::pause() + +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) { - mute(true, MUTE_REASON_PAUSE); + internal_config_add_sound_io_channel_connection_node(dev, guest_channel, name, node_channel, db); + m_osd_info.m_generation --; } +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) +{ + 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(guest_channel, name, node_channel, db)); +} -//------------------------------------------------- -// resume - resume sound output -//------------------------------------------------- +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 --; +} -void sound_manager::resume() +void sound_manager::internal_config_add_sound_io_channel_connection_default(sound_io_device *dev, u32 guest_channel, u32 node_channel, float db) { - mute(false, MUTE_REASON_PAUSE); + 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(guest_channel, "", node_channel, db)); } +void sound_manager::config_remove_sound_io_channel_connection_node(sound_io_device *dev, u32 guest_channel, std::string name, u32 node_channel) +{ + internal_config_remove_sound_io_channel_connection_node(dev, guest_channel, name, node_channel); + m_osd_info.m_generation --; +} -//------------------------------------------------- -// config_load - read and apply data from the -// configuration file -//------------------------------------------------- +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; + } +} -void sound_manager::config_load(config_type cfg_type, config_level cfg_level, util::xml::data_node const *parentnode) +void sound_manager::config_remove_sound_io_channel_connection_default(sound_io_device *dev, u32 guest_channel, u32 node_channel) { - // we only care system-specific configuration - if ((cfg_type != config_type::SYSTEM) || !parentnode) - return; + internal_config_remove_sound_io_channel_connection_default(dev, guest_channel, node_channel); + m_osd_info.m_generation --; +} - // master volume attenuation - if (util::xml::data_node const *node = parentnode->get_child("attenuation")) - { - // treat source INI files or more specific as higher priority than CFG - // FIXME: leaky abstraction - this depends on a front-end implementation detail - if ((OPTION_PRIORITY_NORMAL + 5) > machine().options().get_entry(OPTION_VOLUME)->priority()) - set_attenuation(std::clamp(int(node->get_attribute_int("value", 0)), -32, 0)); - } +void sound_manager::internal_config_remove_sound_io_channel_connection_default(sound_io_device *dev, u32 guest_channel, 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) == "" && std::get<2>(*i) == node_channel) { + config.m_channel_mappings.erase(i); + return; + } +} - // iterate over channel nodes - for (util::xml::data_node const *node = parentnode->get_child("channel"); node != nullptr; node = node->get_next_sibling("channel")) - { - mixer_input info; - if (indexed_mixer_input(node->get_attribute_int("index", -1), info)) - { - // note that this doesn't disallow out-of-range values - float value = node->get_attribute_float("value", std::nanf("")); - - if (!std::isnan(value)) - info.stream->input(info.inputnum).set_user_gain(value); +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; } - } +} - // iterate over speaker panning nodes - for (util::xml::data_node const *node = parentnode->get_child("panning"); node != nullptr; node = node->get_next_sibling("panning")) - { - char const *const tag = node->get_attribute_string("tag", nullptr); - if (tag != nullptr) - { - for (speaker_device &speaker : speaker_device_enumerator(machine().root_device())) - { - if (!strcmp(tag, speaker.tag())) - { - float value = node->get_attribute_float("value", speaker.defpan()); - speaker.set_pan(value); +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::internal_config_set_volume_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) { + 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("", 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; + } + + 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; + } + } + + + // 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 void sound_manager::apply_osd_changes(std::vector &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; + } + + // 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 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::osd_information_update() +{ + // 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(m_osd_input_streams ); + apply_osd_changes(m_osd_output_streams); + } -//------------------------------------------------- -// config_save - save data to the configuration -// file -//------------------------------------------------- +} -void sound_manager::config_save(config_type cfg_type, util::xml::data_node *parentnode) +void sound_manager::generate_mapping() { - // we only save system-specific configuration - if (cfg_type != config_type::SYSTEM) - return; + 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 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); + } + } - // master volume attenuation - if (m_attenuation != machine().options().volume()) - { - if (util::xml::data_node *const node = parentnode->add_child("attenuation", nullptr)) - node->set_attribute_int("value", m_attenuation); - } + for(auto &nmap: node_to_remove) + internal_config_remove_sound_io_connection_node(&speaker.m_dev, nmap); + + std::vector> 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(std::get<0>(cmap), std::get<1>(cmap), std::get<2>(cmap))); + } + } - // iterate over mixer channels for per-channel volume - for (int mixernum = 0; ; mixernum++) - { - mixer_input info; - if (!indexed_mixer_input(mixernum, info)) - break; + 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)); + } - float const value = info.stream->input(info.inputnum).user_gain(); - if (value != 1.0f) - { - util::xml::data_node *const node = parentnode->add_child("channel", nullptr); - if (node) - { - node->set_attribute_int("index", mixernum); - node->set_attribute_float("value", value); + 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 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); } } - } - // iterate over speakers for panning - for (speaker_device &speaker : speaker_device_enumerator(machine().root_device())) - { - float const value = speaker.pan(); - if (value != speaker.defpan()) - { - util::xml::data_node *const node = parentnode->add_child("panning", nullptr); - if (node) - { - node->set_attribute("tag", speaker.tag()); - node->set_attribute_float("value", value); + for(auto &nmap: node_to_remove) + internal_config_remove_sound_io_connection_node(&mic.m_dev, nmap); + + std::vector> 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(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 sound_manager::find_channel_mapping(const std::array &position, const osd::audio_info::node_info *node) +{ + std::vector 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; +} -//------------------------------------------------- -// adjust_toward_compressor_scale - adjust the -// current scale factor toward the current goal, -// in small increments -//------------------------------------------------- -stream_buffer::sample_t sound_manager::adjust_toward_compressor_scale(stream_buffer::sample_t curscale, stream_buffer::sample_t prevsample, stream_buffer::sample_t rawsample) +void sound_manager::update_osd_streams() { - stream_buffer::sample_t proposed_scale = curscale; + std::unique_lock 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(node->m_sources); + nos.m_volumes.clear(); + nos.m_is_system_default = is_system_default; + return sid; + } - // if we want to get larger, increment by 0.01 - if (curscale < m_compressor_scale) - { - proposed_scale += 0.01f; - if (proposed_scale > m_compressor_scale) - proposed_scale = m_compressor_scale; - } + // 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(node->m_sinks); + nos.m_is_system_default = is_system_default; + return sid; + } - // otherwise, decrement by 0.01 - else - { - proposed_scale -= 0.01f; - if (proposed_scale < m_compressor_scale) - proposed_scale = m_compressor_scale; - } + // 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; + } - // compute the sample at the current scale and at the proposed scale - stream_buffer::sample_t cursample = rawsample * curscale; - stream_buffer::sample_t proposed_sample = rawsample * proposed_scale; + // Otherwise use the default method + return get_input_stream_for_node_and_device(node, dev, is_system_default, true); + }; - // if they trend in the same direction, it's ok to take the step - if ((cursample < prevsample && proposed_sample < prevsample) || (cursample > prevsample && proposed_sample > prevsample)) - curscale = proposed_scale; - // return the current scale - return curscale; -} + 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_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 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; + } -//------------------------------------------------- -// update - mix everything down to its final form -// and send it to the OSD layer -//------------------------------------------------- + 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); + } -void sound_manager::update(s32 param) -{ - LOG("sound_update\n"); - auto profile = g_profiler.start(PROFILER_SOUND); + } else { + std::vector &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 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; + } - // determine the duration of this update - attotime update_period = machine().time() - m_last_update; - sound_assert(update_period.seconds() == 0); + 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); + } + } + } - // use that to compute the number of samples we need from the speakers - attoseconds_t sample_rate_attos = HZ_TO_ATTOSECONDS(machine().sample_rate()); - m_samples_this_update = update_period.attoseconds() / sample_rate_attos; + } else { + // All sources need to be merged per-destination, max one stream per destination + + std::map 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(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; + } - // recompute the end time to an even sample boundary - attotime endtime = m_last_update + attotime(0, m_samples_this_update * sample_rate_attos); + // 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(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; + } - // clear out the mix bufers - std::fill_n(&m_leftmix[0], m_samples_this_update, 0); - std::fill_n(&m_rightmix[0], m_samples_this_update, 0); + // 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_steps = m_output_mixing_steps; + for(const auto &nm : omap.m_node_mappings) { + const auto *node = find_node_info(nm.m_node); + u32 stream_index = get_output_stream_for_node(node, nm.m_is_system_default); + u32 umask = m_osd_output_streams[stream_index].m_unused_channels_mask; + float linear_volume = osd::db_to_linear(nm.m_db); + + for(u32 channel = 0; channel != channels; channel++) { + std::vector 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, + dev_index, + channel, + stream_index, + tchannel, + linear_volume + }); + umask &= ~(1 << tchannel); + } + } + m_osd_output_streams[stream_index].m_unused_channels_mask = umask; + } - // force all the speaker streams to generate the proper number of samples - for (speaker_device &speaker : m_speakers) - speaker.mix(&m_leftmix[0], &m_rightmix[0], m_last_update, endtime, m_samples_this_update, (m_muted & MUTE_REASON_SYSTEM)); + for(const auto &cm : omap.m_channel_mappings) { + const auto *node = find_node_info(cm.m_node); + u32 stream_index = get_output_stream_for_node(node, false); + u32 umask = m_osd_output_streams[stream_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, + dev_index, + cm.m_guest_channel, + stream_index, + cm.m_node_channel, + osd::db_to_linear(cm.m_db) + }); + m_osd_output_streams[stream_index].m_unused_channels_mask = umask & ~(1 << cm.m_node_channel); + } - // determine the maximum in this section - stream_buffer::sample_t curmax = 0; - for (int sampindex = 0; sampindex < m_samples_this_update; sampindex++) - { - auto sample = m_leftmix[sampindex]; - if (sample < 0) - sample = -sample; - if (sample > curmax) - curmax = sample; - - sample = m_rightmix[sampindex]; - if (sample < 0) - sample = -sample; - if (sample > curmax) - curmax = sample; + } else { + u32 channels = m_microphones[dev_index].m_channels; + std::vector &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 stream_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 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, + dev_index, + channel, + stream_index, + tchannel, + linear_volume + }); + m_osd_input_streams[stream_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 stream_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, + dev_index, + cm.m_guest_channel, + stream_index, + cm.m_node_channel, + osd::db_to_linear(cm.m_db) + }); + m_osd_input_streams[stream_index].m_unused_channels_mask &= ~(1 << cm.m_node_channel); + } + } + } } - // pull in current compressor scale factor before modifying - stream_buffer::sample_t lscale = m_compressor_scale; - stream_buffer::sample_t rscale = m_compressor_scale; + // 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); + } - // if we're above what the compressor will handle, adjust the compression - if (curmax * m_compressor_scale > 1.0) - { - m_compressor_scale = 1.0 / curmax; - m_compressor_counter = STREAMS_UPDATE_FREQUENCY / 5; + // 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); } - // if we're currently scaled, wait a bit to see if we can trend back toward 1.0 - else if (m_compressor_counter != 0) - m_compressor_counter--; + // 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); +} - // try to migrate toward 0 unless we're going to introduce clipping - else if (m_compressor_scale < 1.0 && curmax * 1.01 * m_compressor_scale < 1.0) - { - m_compressor_scale *= 1.01f; - if (m_compressor_scale > 1.0) - m_compressor_scale = 1.0; - } +void sound_manager::mapping_update() +{ + 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"); + } + } -#if (SOUND_DEBUG) - if (lscale != m_compressor_scale) - printf("scale=%.5f\n", m_compressor_scale); -#endif + generate_mapping(); - // track whether there are pending scale changes in left/right - stream_buffer::sample_t lprev = 0, rprev = 0; + 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); + } + } - // 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; - - // ensure that changing the compression won't reverse direction to reduce "pops" - stream_buffer::sample_t lsamp = m_leftmix[sampindex]; - if (lscale != m_compressor_scale && sample != m_finalmix_leftover) - lscale = adjust_toward_compressor_scale(lscale, lprev, lsamp); - - lprev = lsamp * lscale; - if (m_compressor_enabled) - lsamp = lprev; - - // clamp the left side - if (lsamp > 1.0) - lsamp = 1.0; - else if (lsamp < -1.0) - lsamp = -1.0; - finalmix[finalmix_offset++] = s16(lsamp * 32767.0); - - // ensure that changing the compression won't reverse direction to reduce "pops" - stream_buffer::sample_t rsamp = m_rightmix[sampindex]; - if (rscale != m_compressor_scale && sample != m_finalmix_leftover) - rscale = adjust_toward_compressor_scale(rscale, rprev, rsamp); - - rprev = rsamp * rscale; - if (m_compressor_enabled) - rsamp = rprev; - - // clamp the right side - if (rsamp > 1.0) - rsamp = 1.0; - else if (rsamp < -1.0) - rsamp = -1.0; - finalmix[finalmix_offset++] = s16(rsamp * 32767.0); + 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); + } + } } - m_finalmix_leftover = sample - m_samples_this_update * 1000; +} - // play the result - if (finalmix_offset > 0) - { - 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) - util::wav_add_data_16(*m_wavfile, finalmix, finalmix_offset); - } - // update any orphaned streams so they don't get too far behind - for (auto &stream : m_orphan_stream_list) - stream.first->update(); - // remember the update time - m_last_update = endtime; - m_update_number++; - // apply sample rate changes - apply_sample_rate_changes(); +//**// Global sound system update + +u64 sound_manager::rate_and_time_to_index(attotime time, u32 sample_rate) const +{ + return time.m_seconds * sample_rate + ((time.m_attoseconds / 100000000) * sample_rate) / 10000000000; +} + +void sound_manager::update(s32) +{ + auto profile = g_profiler.start(PROFILER_SOUND); + + if(m_osd_info.m_generation == 0xffffffff) + startup_cleanups(); + + mapping_update(); + streams_update(); // notify that new samples have been generated + m_last_sync_time = machine().time(); emulator_info::sound_hook(); } + +void sound_manager::streams_update() +{ + attotime now = machine().time(); + { + std::unique_lock 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(); + } + + for(sound_stream *stream : m_ordered_streams) + if(stream->device().type() != SPEAKER) + stream->sync(now); + + for(osd_input_stream &stream : m_osd_input_streams) + stream.m_buffer.sync(); + + 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); + + m_effects_condition.notify_all(); + +} + +//**// 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(); + auto *res = new audio_resampler(fs, ft); + m_resamplers[key].reset(res); + return res; +} + -- cgit v1.2.3-70-g09d2