diff --git a/src/libs/ngs2.cpp b/src/libs/ngs2.cpp index 11846b6a6..196f5ef51 100644 --- a/src/libs/ngs2.cpp +++ b/src/libs/ngs2.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -197,8 +198,8 @@ struct Ngs2WaveformFormat { uint32_t num_channels = 0; uint32_t sample_rate = 0; uint32_t config_data = 0; - uint32_t frame_margin = 0; uint32_t frame_offset = 0; + uint32_t frame_margin = 0; }; struct Ngs2WaveformBlock { @@ -388,18 +389,20 @@ struct Ngs2RackInternal { enum class Ngs2VoicePlayState : uint32_t { Empty = 0, Playing = 3, Paused = 5, Stopped = 0xb }; -// The 64-bit bypass mask precedes the filter type and FCQ parameters. struct Ngs2SamplerFilterParam { uint32_t index; uint32_t location; - uint64_t channel_mask; uint32_t type; + uint64_t channel_mask; float frequency; float q; float level; uint32_t reserved[3]; }; static_assert(sizeof(Ngs2SamplerFilterParam) == 48); +static_assert(offsetof(Ngs2SamplerFilterParam, type) == 8 && + offsetof(Ngs2SamplerFilterParam, channel_mask) == 16 && + offsetof(Ngs2SamplerFilterParam, frequency) == 24); struct Ngs2SamplerFilter { struct History { @@ -507,7 +510,6 @@ struct Ngs2VoiceInternal { Ngs2WaveformBlock info; uint32_t cursor = 0; size_t data_cursor = 0; - uint32_t skip_remaining = info.num_skip_samples; uint32_t repeated_count = 0; }; struct Module { @@ -535,14 +537,21 @@ struct Ngs2VoiceInternal { bool has_samples = false; std::unique_ptr decoder; - std::vector decoded_frame; - bool accepts_blocks = true; - uint32_t sample_rate = 0; - uint64_t sample_phase = 0; - uint64_t sample_step = 0; - uint64_t decoded_samples = 0; - uint64_t decoded_bytes = 0; - const uint8_t* waveform_end = nullptr; + std::vector decoded_frame; + std::vector next_decoded_frame; + std::vector compressed_input; + uint32_t frame_cursor = 0; + uint32_t frame_samples = 0; + uint32_t next_frame_samples = 0; + bool accepts_blocks = true; + uint32_t sample_rate = 0; + uint64_t sample_phase = 0; + uint64_t sample_step = 0; + uint64_t decoded_samples = 0; + uint64_t decoded_bytes = 0; + uint32_t duration_remaining = 0; + uint32_t skip_remaining = 0; + const uint8_t* waveform_end = nullptr; std::vector filters; const uint8_t* WaveformData() const { @@ -556,28 +565,44 @@ struct Ngs2VoiceInternal { channels * sizeof(int16_t)); } - void SetupSampler(const Ngs2WaveformFormat& format) { - EXIT_NOT_IMPLEMENTED(format.frame_margin != 0 || format.frame_offset != 0 || - format.sample_rate == 0 || format.num_channels == 0 || - (format.waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9 && - format.sample_rate != rack->ngs->option.sample_rate)); + void ResetSetupState() { SetEvent(4); std::ranges::fill(ports, Port {}); for (auto& matrix: matrices) { matrix.clear(); } - channels = format.num_channels; - sample_rate = format.sample_rate; - sample_phase = 0; - sample_step = uint64_t(sample_rate) << 32u; - decoded_samples = 0; - decoded_bytes = 0; - waveform_end = nullptr; - blocks.clear(); filters.clear(); + } + + void SetupSampler(const Ngs2WaveformFormat& format) { + const bool deferred = format.waveform_type == 0 && format.num_channels == 0 && + format.sample_rate == 0 && format.config_data == 0 && + format.frame_margin == 0 && format.frame_offset == 0; + EXIT_NOT_IMPLEMENTED(!deferred && + (format.frame_margin != 0 || format.frame_offset != 0 || + format.sample_rate == 0 || format.num_channels == 0)); + ResetSetupState(); + channels = format.num_channels; + sample_rate = format.sample_rate; + sample_phase = 0; + sample_step = uint64_t(sample_rate) << 32u; + decoded_samples = 0; + decoded_bytes = 0; + duration_remaining = 0; + skip_remaining = 0; + waveform_end = nullptr; + blocks.clear(); decoder.reset(); decoded_frame.clear(); + next_decoded_frame.clear(); + compressed_input.clear(); + frame_cursor = 0; + frame_samples = 0; + next_frame_samples = 0; accepts_blocks = true; + if (deferred) { + return; + } if (format.waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9) { decoder = std::make_unique(channels, format.sample_rate, Ajm::AjmSampleEncoding::Float, 0); @@ -594,6 +619,7 @@ struct Ngs2VoiceInternal { decoder->WriteCodecInfo(&info, sizeof(info), result); EXIT_NOT_IMPLEMENTED(info.frame_samples > std::numeric_limits::max()); decoded_frame.resize(info.frame_samples * channels); + compressed_input.reserve(info.super_frame_size); } else { EXIT_NOT_IMPLEMENTED(format.waveform_type != 0x12); } @@ -653,6 +679,13 @@ struct Ngs2VoiceEventParam { uint32_t event_id; }; +struct Ngs2SubmixerVoiceSetupParam { + Ngs2VoiceParamHeader header; + uint32_t num_io_channels; + uint32_t flags; +}; +static_assert(sizeof(Ngs2SubmixerVoiceSetupParam) == 16); + struct Ngs2VoicePatchParam { Ngs2VoiceParamHeader header; uint32_t port; @@ -749,6 +782,8 @@ static_assert(sizeof(Ngs2CustomMasteringVoiceState) == 16); static_assert(sizeof(Ngs2SamplerVoiceState) == 56); static_assert(sizeof(Ngs2CustomSamplerVoiceState) == 48); static_assert(sizeof(Ngs2WaveformFormat) == 24); +static_assert(offsetof(Ngs2WaveformFormat, frame_offset) == 16 && + offsetof(Ngs2WaveformFormat, frame_margin) == 20); static_assert(sizeof(Ngs2WaveformBlock) == 40); static_assert(sizeof(Ngs2WaveformInfo) == 232); @@ -1383,10 +1418,12 @@ struct Ngs2VoiceCallbackInfo { uint32_t flag, reserved; uintptr_t user; const void* block_data; - size_t size; - uint32_t repeats, attributes; + uint32_t size, repeats, attributes, reserved2; }; static_assert(sizeof(Ngs2VoiceCallbackInfo) == 56); +static_assert(offsetof(Ngs2VoiceCallbackInfo, size) == 40 && + offsetof(Ngs2VoiceCallbackInfo, repeats) == 44 && + offsetof(Ngs2VoiceCallbackInfo, attributes) == 48); static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) { auto& block = voice.blocks.front(); @@ -1405,8 +1442,9 @@ static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) { 0, block.info.user_data, block.data, - block.info.data_size, + static_cast(block.info.data_size), block.repeated_count, + 0, 0}; if (!repeat) { voice.blocks.pop_front(); @@ -1439,9 +1477,123 @@ static void Ngs2AdvancePcm(Ngs2VoiceInternal& voice) { } } +static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector& frame, + uint32_t& frame_samples) { + while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty()) { + auto& block = voice.blocks.front(); + if (block.info.num_samples != 0 && block.data_cursor == 0) { + voice.duration_remaining = block.info.num_samples; + voice.skip_remaining = block.info.num_skip_samples; + } + if (voice.duration_remaining == 0) { + Ngs2FinishBlock(voice); + continue; + } + const auto skip = + std::min(voice.skip_remaining, + static_cast(frame.size() / voice.channels)); + Ajm::AjmGaplessState gapless; + gapless.Set({voice.duration_remaining, static_cast(skip), 0}, true); + if (!voice.compressed_input.empty()) { + Ajm::AjmSidebandDecAt9CodecInfo info {}; + voice.decoder->WriteCodecInfo(&info, sizeof(info), {}); + EXIT_NOT_IMPLEMENTED(info.next_frame_size < voice.compressed_input.size()); + const auto bytes = + std::min(info.next_frame_size - voice.compressed_input.size(), + block.info.data_size - block.data_cursor); + voice.compressed_input.insert(voice.compressed_input.end(), + block.data + block.data_cursor, + block.data + block.data_cursor + bytes); + block.data_cursor += bytes; + voice.decoded_bytes += bytes; + if (voice.compressed_input.size() < info.next_frame_size) { + Ngs2FinishBlock(voice); + continue; + } + } + const bool pending = !voice.compressed_input.empty(); + const auto result = voice.decoder->Decode( + pending ? voice.compressed_input.data() : block.data + block.data_cursor, + pending ? voice.compressed_input.size() : block.info.data_size - block.data_cursor, + frame.data(), frame.size() * sizeof(float), false, + &gapless); + if (!pending && result.result == Ajm::AJM_RESULT_PARTIAL_INPUT && + result.input_consumed == 0) { + voice.compressed_input.insert(voice.compressed_input.end(), + block.data + block.data_cursor, + block.data + block.info.data_size); + voice.decoded_bytes += block.info.data_size - block.data_cursor; + block.data_cursor = block.info.data_size; + Ngs2FinishBlock(voice); + continue; + } + EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0); + if (pending) { + voice.compressed_input.erase(voice.compressed_input.begin(), + voice.compressed_input.begin() + result.input_consumed); + } else { + block.data_cursor += result.input_consumed; + voice.decoded_bytes += result.input_consumed; + } + voice.skip_remaining -= skip - gapless.current.skip_samples; + const auto decoded_samples = + static_cast(result.output_written / (sizeof(float) * voice.channels)); + if (decoded_samples != 0) { + EXIT_NOT_IMPLEMENTED(decoded_samples > voice.duration_remaining); + frame_samples = decoded_samples; + voice.duration_remaining -= decoded_samples; + } + if (voice.duration_remaining == 0 || + (block.data_cursor == block.info.data_size && voice.compressed_input.empty())) { + if (voice.duration_remaining == 0) { + voice.compressed_input.clear(); + } + Ngs2FinishBlock(voice); + } + if (decoded_samples != 0) { + return true; + } + } + return false; +} + +static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) { + if (voice.frame_cursor == voice.frame_samples) { + if (voice.next_frame_samples != 0) { + std::swap(voice.decoded_frame, voice.next_decoded_frame); + voice.frame_samples = voice.next_frame_samples; + voice.next_frame_samples = 0; + } else if (!Ngs2DecodeFrame(voice, voice.decoded_frame, voice.frame_samples)) { + return false; + } + voice.frame_cursor = 0; + } + return voice.state == Ngs2VoicePlayState::Playing && + voice.frame_cursor < voice.frame_samples; +} + +static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) { + const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u; + while (voice.state == Ngs2VoicePlayState::Playing && voice.sample_phase >= output_rate) { + if (!Ngs2CurrentAtracFrame(voice)) { + break; + } + ++voice.frame_cursor; + ++voice.decoded_samples; + voice.sample_phase -= output_rate; + } + if (voice.blocks.empty() && voice.frame_cursor == voice.frame_samples && + voice.next_frame_samples == 0 && !voice.accepts_blocks) { + voice.sample_phase = 0; + } +} + static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) { uint32_t output = 0; - while (voice.state == Ngs2VoicePlayState::Playing && output < grain && !voice.blocks.empty()) { + const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u; + while (voice.state == Ngs2VoicePlayState::Playing && output < grain && + (!voice.blocks.empty() || voice.frame_cursor < voice.frame_samples || + voice.next_frame_samples != 0)) { if (voice.decoder == nullptr) { Ngs2AdvancePcm(voice); if (voice.state != Ngs2VoicePlayState::Playing || voice.blocks.empty()) { @@ -1461,8 +1613,7 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) { next_block.info.num_skip_samples * voice.channels; } const float fraction = static_cast(voice.sample_phase) / - static_cast(uint64_t(voice.rack->ngs->option.sample_rate) - << 32u); + static_cast(output_rate); for (uint32_t c = 0; c < voice.channels; ++c) { voice.samples[c * grain + output] = std::lerp(static_cast(pcm[c]), static_cast(next[c]), fraction) / @@ -1472,44 +1623,39 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) { ++output; continue; } - auto& block = voice.blocks.front(); - auto count = std::min(grain - output, block.info.num_samples - block.cursor); - const auto skip = - std::min(block.skip_remaining, - static_cast(voice.decoded_frame.size() / voice.channels)); - Ajm::AjmGaplessState gapless; - gapless.Set({block.info.num_samples - block.cursor, static_cast(skip), 0}, - true); - const auto result = voice.decoder->Decode( - block.data + block.data_cursor, block.info.data_size - block.data_cursor, - voice.decoded_frame.data(), voice.decoded_frame.size() * sizeof(float), false, - &gapless); - EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0); - block.data_cursor += result.input_consumed; - block.skip_remaining -= skip - gapless.current.skip_samples; - const auto decoded_samples = - static_cast(result.output_written / (sizeof(float) * voice.channels)); - if (decoded_samples == 0) { - continue; + Ngs2AdvanceAtrac9(voice); + if (!Ngs2CurrentAtracFrame(voice)) { + break; } - EXIT_NOT_IMPLEMENTED(decoded_samples > count); - count = decoded_samples; - for (uint32_t c = 0; c < voice.channels; ++c) { - for (uint32_t i = 0; i < count; ++i) { - const auto index = i * voice.channels + c; - voice.samples[c * grain + output + i] = voice.decoded_frame[index]; + if (voice.frame_cursor + 1 == voice.frame_samples && voice.sample_phase != 0) { + if (voice.next_frame_samples == 0 && !voice.blocks.empty()) { + if (voice.next_decoded_frame.empty()) { + voice.next_decoded_frame.resize(voice.decoded_frame.size()); + } + Ngs2DecodeFrame(voice, voice.next_decoded_frame, voice.next_frame_samples); + } + if (voice.state != Ngs2VoicePlayState::Playing || + voice.frame_cursor >= voice.frame_samples) { + break; } } - output += count; - block.cursor += count; - voice.decoded_samples += count; - if (block.cursor == block.info.num_samples) { - voice.decoded_bytes += block.info.data_size; - Ngs2FinishBlock(voice); + const float* current = voice.decoded_frame.data() + voice.frame_cursor * voice.channels; + const float* next = voice.frame_cursor + 1 < voice.frame_samples + ? current + voice.channels + : voice.next_frame_samples != 0 ? voice.next_decoded_frame.data() + : current; + const float fraction = static_cast(voice.sample_phase) / + static_cast(output_rate); + for (uint32_t c = 0; c < voice.channels; ++c) { + voice.samples[c * grain + output] = std::lerp(current[c], next[c], fraction); } + voice.sample_phase += voice.sample_step; + ++output; } if (voice.decoder == nullptr) { Ngs2AdvancePcm(voice); + } else { + Ngs2AdvanceAtrac9(voice); } voice.has_samples = output != 0; } @@ -1519,6 +1665,11 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vectormatrices.at(port.matrix); - for (uint32_t dst = 0; dst < voice.channels; ++dst) { + const size_t output_channels = + matrix == nullptr + ? voice.channels + : std::min(voice.channels, matrix->size() / source->channels); + for (size_t dst = 0; dst < output_channels; ++dst) { for (uint32_t src = 0; src < source->channels; ++src) { const float level = port.volume * (matrix == nullptr ? (src == dst ? 1.0f : 0.0f) - : matrix->at(dst * source->channels + src)); + : (*matrix)[dst * source->channels + src]); for (uint32_t i = 0; i < grain; ++i) { voice.samples[dst * grain + i] += source->samples[src * grain + i] * level; @@ -1612,6 +1769,7 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff EXIT_NOT_IMPLEMENTED(buffer_info == nullptr || system_handle == 0 || num_buffer_info == 0); auto* ngs = reinterpret_cast(system_handle); Common::LockGuard lock(ngs->mutex); + std::vector> output_mix(num_buffer_info); std::vector voices; { Common::LockGuard racks_lock(g_racks_mutex); @@ -1633,9 +1791,6 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff } const auto grain = ngs->option.num_grain_samples; for (auto* voice: voices) { - if (voice->channels == 0) { - continue; - } Ngs2RenderVoice(*voice, voices, grain); if (voice->rack->type != Ngs2RackType::Mastering || !voice->has_samples) { continue; @@ -1643,10 +1798,21 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff EXIT_NOT_IMPLEMENTED(voice->output_id >= num_buffer_info); const auto& output = buffer_info[voice->output_id]; EXIT_NOT_IMPLEMENTED(output.buffer == nullptr); - EXIT_NOT_IMPLEMENTED(output.waveform_type != 0x18 || + EXIT_NOT_IMPLEMENTED((output.waveform_type != 0x12 && output.waveform_type != 0x18) || output.num_channels != voice->channels); - EXIT_NOT_IMPLEMENTED(output.buffer_size < grain * output.num_channels * sizeof(float)); - auto* pcm = static_cast(output.buffer); + EXIT_NOT_IMPLEMENTED(output.buffer_size < grain * output.num_channels * + (output.waveform_type == 0x12 ? sizeof(int16_t) + : sizeof(float))); + float* pcm = nullptr; + if (output.waveform_type == 0x12) { + auto& mix = output_mix[voice->output_id]; + if (mix.empty()) { + mix.resize(grain * output.num_channels); + } + pcm = mix.data(); + } else { + pcm = static_cast(output.buffer); + } for (uint32_t dst = 0; dst < output.num_channels; ++dst) { for (uint32_t src = 0; src < voice->channels; ++src) { const float level = voice->output_matrix.empty() @@ -1658,6 +1824,18 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff } } } + for (uint32_t i = 0; i < num_buffer_info; ++i) { + const auto& mix = output_mix[i]; + if (mix.empty()) { + continue; + } + auto* pcm = static_cast(buffer_info[i].buffer); + for (size_t sample = 0; sample < mix.size(); ++sample) { + pcm[sample] = static_cast(std::clamp( + std::lrintf(std::clamp(mix[sample], -1.0f, 1.0f) * 32768.0f), -32768l, + 32767l)); + } + } for (auto* voice: voices) { // No envelope is configured, so a release stop finishes in this render. if (voice->state == Ngs2VoicePlayState::Stopped) { @@ -1682,6 +1860,23 @@ static bool Ngs2FourCcEquals(const uint8_t* data, const char* four_cc) { return std::memcmp(data, four_cc, 4) == 0; } +static bool Ngs2GetAtrac9CodecInfo(std::array config, + Atrac9CodecInfo& codec) { + if (config[0] != 0xfe || (config[1] & 1u) != 0 || ((config[1] >> 1u) & 7u) >= 6u) { + return false; + } + void* decoder = Atrac9GetHandle(); + const bool valid = decoder != nullptr && Atrac9InitDecoder(decoder, config.data()) == 0 && + Atrac9GetCodecInfo(decoder, &codec) == 0 && codec.channels > 0 && + codec.samplingRate > 0 && codec.superframeSize > 0 && + codec.framesInSuperframe > 0 && codec.frameSamples > 0 && + codec.superframeSize % codec.framesInSuperframe == 0; + if (decoder != nullptr) { + Atrac9ReleaseHandle(decoder); + } + return valid; +} + static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformInfo* info) { static constexpr uint8_t ATRAC9_GUID[16] = {0xd2, 0x42, 0xe1, 0x47, 0xba, 0x36, 0x8d, 0x4d, 0x88, 0xfc, 0x61, 0x65, @@ -1695,7 +1890,7 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI } const uint64_t riff_end64 = 8ull + Ngs2ReadLe32(bytes + 4); - if (riff_end64 < 12 || riff_end64 > data_size) { + if (riff_end64 < 12) { return NGS2_ERROR_INVALID_WAVEFORM_DATA; } const auto riff_end = static_cast(riff_end64); @@ -1705,11 +1900,13 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI size_t waveform_offset = 0; uint32_t waveform_size = 0; - for (size_t offset = 12; offset + 8 <= riff_end;) { + for (size_t offset = 12; offset + 8 <= std::min(riff_end, data_size);) { const auto* chunk = bytes + offset; const auto chunk_size = static_cast(Ngs2ReadLe32(chunk + 4)); const auto payload = offset + 8; - if (chunk_size > riff_end - payload) { + const auto is_data = Ngs2FourCcEquals(chunk, "data"); + const uint64_t next = static_cast(payload) + chunk_size + (chunk_size & 1u); + if (next > riff_end || (next > data_size && !is_data)) { return NGS2_ERROR_INVALID_WAVEFORM_DATA; } @@ -1723,7 +1920,7 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI return NGS2_ERROR_INVALID_WAVEFORM_FORMAT; } fact = bytes + payload; - } else if (Ngs2FourCcEquals(chunk, "data")) { + } else if (is_data) { if (payload > std::numeric_limits::max()) { return NGS2_ERROR_INVALID_WAVEFORM_DATA; } @@ -1731,9 +1928,8 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI waveform_size = static_cast(chunk_size); } - const uint64_t next = static_cast(payload) + chunk_size + (chunk_size & 1u); - if (next > riff_end) { - return NGS2_ERROR_INVALID_WAVEFORM_DATA; + if (next > data_size) { + break; } offset = static_cast(next); } @@ -1748,20 +1944,8 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI std::array config {}; std::memcpy(config.data(), format + 44, config.size()); - if (config[0] != 0xfe || (config[1] & 1u) != 0 || ((config[1] >> 1u) & 7u) >= 6u) { - return NGS2_ERROR_INVALID_WAVEFORM_FORMAT; - } - Atrac9CodecInfo codec {}; - void* decoder = Atrac9GetHandle(); - const bool valid_codec = - decoder != nullptr && Atrac9InitDecoder(decoder, config.data()) == 0 && - Atrac9GetCodecInfo(decoder, &codec) == 0 && codec.channels > 0 && - codec.samplingRate > 0 && codec.superframeSize > 0 && codec.framesInSuperframe > 0 && - codec.frameSamples > 0 && codec.superframeSize % codec.framesInSuperframe == 0; - if (decoder != nullptr) { - Atrac9ReleaseHandle(decoder); - } + const bool valid_codec = Ngs2GetAtrac9CodecInfo(config, codec); const uint64_t frame_samples = static_cast(codec.frameSamples) * static_cast(codec.framesInSuperframe); if (!valid_codec || codec.channels != Ngs2ReadLe16(format + 2) || @@ -1819,6 +2003,32 @@ int KYTY_SYSV_ABI Ngs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32 EXIT_NOT_IMPLEMENTED(block == nullptr); std::memset(block, 0, sizeof(Ngs2WaveformBlock)); + if (format == nullptr || format->num_channels == 0 || format->num_channels > 8 || + format->sample_rate == 0 || format->sample_rate > 192000) { + return NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + } + if (format->waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9) { + std::array config { + static_cast(format->config_data >> 24u), + static_cast(format->config_data >> 16u), + static_cast(format->config_data >> 8u), + static_cast(format->config_data)}; + Atrac9CodecInfo codec {}; + if (!Ngs2GetAtrac9CodecInfo(config, codec) || + codec.channels != format->num_channels || codec.samplingRate != format->sample_rate) { + return NGS2_ERROR_INVALID_WAVEFORM_FORMAT; + } + const uint64_t frame_samples = uint64_t(codec.frameSamples) * codec.framesInSuperframe; + const uint64_t skip = sample_pos % frame_samples; + block->data_offset = uint64_t(sample_pos / frame_samples) * codec.superframeSize; + block->data_size = num_samples == 0 + ? 0 + : ((skip + num_samples + frame_samples - 1) / frame_samples) * + codec.superframeSize; + block->num_skip_samples = num_samples == 0 ? 0 : static_cast(skip); + } else { + return NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT; + } block->num_samples = num_samples; return OK; } @@ -2076,7 +2286,21 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH } break; } - case 0x2000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Submixer); break; + case 0x2000: { + EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Submixer); + if ((param->id & 0xffffu) == 0) { + EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2SubmixerVoiceSetupParam)); + const auto& setup = + *reinterpret_cast(param); + EXIT_NOT_IMPLEMENTED( + setup.num_io_channels == 0 || + setup.num_io_channels > voice->rack->option.submixer.max_channels || + setup.flags != 0); + voice->ResetSetupState(); + voice->channels = setup.num_io_channels; + } + break; + } case 0x2001: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Reverb); break; case 0x3000: { EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Mastering); @@ -2145,18 +2369,32 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH }; const auto& blocks = *reinterpret_cast(param); EXIT_NOT_IMPLEMENTED(!voice->accepts_blocks || - (blocks.flags != 0 && blocks.flags != 1 && - blocks.flags != 0x11 && - blocks.flags != 0x4)); - if (blocks.flags == 0x4) { + (blocks.flags & ~0x17u) != 0); + if ((blocks.flags & 0x4u) != 0) { voice->blocks.clear(); - voice->sample_phase = 0; - voice->waveform_end = nullptr; + voice->sample_phase = 0; + voice->duration_remaining = 0; + voice->skip_remaining = 0; + voice->compressed_input.clear(); + voice->frame_cursor = 0; + voice->frame_samples = 0; + voice->next_frame_samples = 0; + voice->waveform_end = nullptr; } voice->accepts_blocks = (blocks.flags & 1u) != 0; + const bool only_data = (blocks.flags & 2u) != 0; + EXIT_NOT_IMPLEMENTED(only_data && voice->decoder == nullptr); for (uint32_t i = 0; i < blocks.count; ++i) { - const auto& block = blocks.blocks[i]; - EXIT_NOT_IMPLEMENTED(block.num_samples == 0); + auto block = blocks.blocks[i]; + if (!only_data && block.num_samples == 0 && block.data_size == 0) { + continue; + } + EXIT_NOT_IMPLEMENTED(!only_data && block.num_samples == 0); + if (only_data) { + block.num_repeats = 0; + block.num_skip_samples = 0; + block.num_samples = 0; + } EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 && (voice->rack->type != Ngs2RackType::Sampler || voice->decoder != nullptr)); @@ -2179,12 +2417,12 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH } break; } - case 5: { - const float ratio = *reinterpret_cast(param + 1); - EXIT_NOT_IMPLEMENTED(!std::isfinite(ratio) || ratio < 0.0f || ratio > 4.0f || - (voice->decoder != nullptr && ratio != 1.0f)); + case 5: { + const float ratio = *reinterpret_cast(param + 1); + EXIT_NOT_IMPLEMENTED(!std::isfinite(ratio)); + const float clamped_ratio = std::clamp(ratio, 0.0f, 4.0f); voice->sample_step = static_cast(std::llround( - static_cast(voice->sample_rate) * ratio * 4294967296.0)); + static_cast(voice->sample_rate) * clamped_ratio * 4294967296.0)); break; } case 0xa: { diff --git a/tests/Ngs2SamplerTests.cpp b/tests/Ngs2SamplerTests.cpp index bd9b7c547..3641c7e75 100644 --- a/tests/Ngs2SamplerTests.cpp +++ b/tests/Ngs2SamplerTests.cpp @@ -88,6 +88,194 @@ void TestStreamingResample() { "streaming progress does not match consumed source data"); } +void TestAtrac9ResampleAcrossFrames() { + const auto make_voice = [] { + auto f = std::make_unique(24000); + f->voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0}); + f->voice.state = Ngs2VoicePlayState::Playing; + f->voice.decoded_frame[0] = 0.0f; + f->voice.decoded_frame[1] = 0.5f; + f->voice.frame_samples = 2; + f->voice.next_decoded_frame.resize(f->voice.decoded_frame.size()); + f->voice.next_decoded_frame[0] = 1.0f; + f->voice.next_decoded_frame[1] = 1.5f; + f->voice.next_frame_samples = 2; + f->voice.accepts_blocks = false; + return f; + }; + auto whole = make_voice(); + whole->Render(8); + const std::vector expected {0.0f, 0.25f, 0.5f, 0.75f, + 1.0f, 1.25f, 1.5f, 1.5f}; + Check(whole->voice.samples == expected && whole->voice.decoded_samples == 4 && + whole->voice.state == Ngs2VoicePlayState::Empty, + "24 kHz ATRAC9 interpolation or source progress failed at frame boundary"); + auto split = make_voice(); + split->Render(3); + const auto first = split->voice.samples; + split->Render(5); + std::vector joined = first; + joined.insert(joined.end(), split->voice.samples.begin(), split->voice.samples.end()); + Check(joined == expected && split->voice.decoded_samples == 4, + "ATRAC9 resampling changed across render grains"); + auto pitched = make_voice(); + struct Pitch { + Ngs2VoiceParamHeader header {16, 0, 0x10000005}; + float ratio = 2.0f; + uint32_t reserved = 0; + } pitch; + Check(Ngs2VoiceControl(reinterpret_cast(&pitched->voice), &pitch.header) == OK, + "ATRAC9 pitch control failed"); + pitched->Render(4); + Check(pitched->voice.samples == std::vector {0.0f, 0.5f, 1.0f, 1.5f} && + pitched->voice.decoded_samples == 4, + "ATRAC9 pitch did not use source-rate phase advancement"); +} + +void TestAtrac9ResampleAfterStarvation() { + Fixture f(24000); + f.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0}); + f.voice.state = Ngs2VoicePlayState::Playing; + f.voice.accepts_blocks = true; + f.voice.decoded_frame[0] = 0.0f; + f.voice.decoded_frame[1] = 0.5f; + f.voice.frame_samples = 2; + f.Render(4); + Check(f.voice.samples == std::vector {0.0f, 0.25f, 0.5f, 0.5f} && + f.voice.decoded_samples == 2, + "ATRAC9 resampling lost the final sample before starvation"); + f.Render(2); + Check(!f.voice.has_samples && f.voice.state == Ngs2VoicePlayState::Playing && + f.voice.decoded_samples == 2, + "starved ATRAC9 stream consumed source samples"); + f.voice.next_decoded_frame.resize(f.voice.decoded_frame.size()); + f.voice.next_decoded_frame[0] = 1.0f; + f.voice.next_decoded_frame[1] = 1.5f; + f.voice.next_frame_samples = 2; + f.Render(4); + Check(f.voice.samples == std::vector {1.0f, 1.25f, 1.5f, 1.5f} && + f.voice.decoded_samples == 4, + "ATRAC9 refill lost source-rate phase"); +} + +void TestWaveformBlockGeometry() { + const Ngs2WaveformFormat format {NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0}; + Ngs2WaveformBlock block {}; + Check(Ngs2CalcWaveformBlock(&format, 0, 34534, &block) == OK && + block.data_offset == 0 && block.data_size == 13056 && + block.num_skip_samples == 0 && block.num_samples == 34534, + "ATRAC9 block calculation omitted encoded superframes"); + Check(Ngs2CalcWaveformBlock(&format, 513, 1, &block) == OK && + block.data_offset == 192 && block.data_size == 192 && + block.num_skip_samples == 1 && block.num_samples == 1, + "ATRAC9 block calculation lost the source offset"); + Check(Ngs2CalcWaveformBlock(&format, 0, 0, &block) == OK && block.data_size == 0 && + block.num_samples == 0, + "empty ATRAC9 block acquired encoded data"); + Fixture f(24000); + const int16_t silence = 0; + f.Queue(&silence, 0, 0, UINT32_MAX); + f.Render(1); + Check(f.voice.blocks.empty() && f.voice.state == Ngs2VoicePlayState::Empty, + "empty repeated block kept the sampler active"); +} + +void TestSubmixerSetupAndRouting() { + Fixture f(48000, 2); + Ngs2RackInternal submixer_rack {}; + submixer_rack.ngs = &f.system; + submixer_rack.type = Ngs2RackType::Submixer; + submixer_rack.option.submixer.max_channels = 8; + Ngs2VoiceInternal submixer; + submixer.rack = &submixer_rack; + submixer.ports.resize(1); + submixer.matrices.resize(1); + Ngs2RackInternal mastering_rack {}; + mastering_rack.type = Ngs2RackType::Mastering; + Ngs2VoiceInternal mastering; + mastering.rack = &mastering_rack; + mastering.channels = 2; + mastering.state = Ngs2VoicePlayState::Playing; + submixer.state = Ngs2VoicePlayState::Playing; + submixer.ports[0].dest = &mastering; + submixer.matrices[0] = {1.0f}; + f.voice.ports.push_back({&submixer, 0, 1.0f, -1}); + const int16_t pcm[] = {8192, -8192, 16384, -16384}; + f.Queue(pcm, 2, 0); + const std::vector voices {&f.voice, &submixer, &mastering}; + Ngs2RenderVoice(mastering, voices, 1); + Check(!submixer.has_samples && !mastering.has_samples && f.voice.decoded_samples == 0, + "unconfigured zero-channel submixer claimed to produce samples"); + Ngs2SubmixerVoiceSetupParam setup {{16, 0, 0x20000000}, 2, 0}; + Check(Ngs2VoiceControl(reinterpret_cast(&submixer), &setup.header) == OK && + submixer.channels == 2 && submixer.state == Ngs2VoicePlayState::Empty && + submixer.ports[0].dest == nullptr && submixer.matrices[0].empty(), + "submixer setup did not establish channels and clear old routing"); + submixer.state = Ngs2VoicePlayState::Playing; + submixer.ports[0] = {&mastering, 0, 1.0f, 0}; + submixer.matrices[0] = {1.0f, 0.0f, 0.0f, 1.0f}; + for (float expected: {0.25f, 0.5f}) { + for (auto* voice: voices) { + voice->rendered = false; + } + Ngs2RenderVoice(mastering, voices, 1); + Check(mastering.has_samples && mastering.samples[0] == expected && + mastering.samples[1] == -expected, + "submixer lost routed samples across render grains"); + } +} + +void TestPcm16RenderOutput() { + Ngs2Internal system; + system.option = Ngs2DefaultSystemOption(); + system.option.num_grain_samples = 4; + Ngs2RackOptionUnion sampler_option {}, mastering_option {}; + Ngs2FillDefaultRackOption(0x1000, &sampler_option); + Ngs2FillDefaultRackOption(0x3000, &mastering_option); + sampler_option.common.max_voices = 2; + sampler_option.common.max_ports = 1; + alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal) + std::byte sampler_storage[sizeof(Ngs2RackInternal) + 2 * sizeof(Ngs2VoiceInternal)]; + alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal) + std::byte mastering_storage[sizeof(Ngs2RackInternal) + sizeof(Ngs2VoiceInternal)]; + Ngs2ContextBufferInfo sampler_buffer {sampler_storage, sizeof(sampler_storage)}; + Ngs2ContextBufferInfo mastering_buffer {mastering_storage, sizeof(mastering_storage)}; + uintptr_t sampler_handle = 0, mastering_handle = 0; + const auto system_handle = reinterpret_cast(&system); + Check(Ngs2RackCreate(system_handle, 0x1000, &sampler_option.common, &sampler_buffer, + &sampler_handle) == OK && + Ngs2RackCreate(system_handle, 0x3000, &mastering_option.common, + &mastering_buffer, &mastering_handle) == OK, + "PCM16 render rack creation failed"); + auto* samplers = reinterpret_cast( + reinterpret_cast(sampler_handle) + 1); + auto& mastering = *reinterpret_cast( + reinterpret_cast(mastering_handle) + 1); + mastering.channels = 1; + mastering.state = Ngs2VoicePlayState::Playing; + const int16_t source[2][4] = {{16384, -16384, 32767, -32768}, + {8192, -8192, 32767, -32768}}; + for (uint32_t i = 0; i < 2; ++i) { + samplers[i].SetupSampler({0x12, 1, 48000}); + samplers[i].blocks.push_back( + {reinterpret_cast(source[i]), {0, sizeof(source[i]), 0, 0, 4}}); + samplers[i].ports[0].dest = &mastering; + samplers[i].SetEvent(1); + } + int16_t output_samples[4] = {}; + Ngs2RenderBufferInfo output {output_samples, sizeof(output_samples), 0x12, 1}; + Check(Ngs2SystemRender(system_handle, &output, 1) == OK && + output_samples[0] == 24576 && output_samples[1] == -24576 && + output_samples[2] == 32767 && output_samples[3] == -32768, + "PCM16 output failed to mix or saturate mastering voices"); + Check(Ngs2SystemRender(system_handle, &output, 1) == OK && + std::ranges::all_of(output_samples, [](int16_t sample) { return sample == 0; }), + "PCM16 output retained samples from the previous render"); + Check(Ngs2RackDestroy(mastering_handle, nullptr) == OK && + Ngs2RackDestroy(sampler_handle, nullptr) == OK, + "PCM16 render rack cleanup failed"); +} + void TestPitchLoopAndSkip() { Fixture f(24000); const int16_t pcm[] = {-30000, 8192, 16384}; @@ -106,6 +294,22 @@ void TestPitchLoopAndSkip() { } } +void TestPitchClamps() { + Fixture f(48000); + const int16_t pcm[] = {8192, 0, 0, 0, -8192}; + f.Queue(pcm, 5, 0); + struct Pitch { + Ngs2VoiceParamHeader header {16, 0, 0x10000005}; + float ratio = 5.0f; + uint32_t reserved = 0; + } pitch; + Check(Ngs2VoiceControl(reinterpret_cast(&f.voice), &pitch.header) == OK, + "out-of-range pitch was rejected"); + f.Render(2); + Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == -0.25f, + "pitch was not clamped to four times playback speed"); +} + void TestStarvationAndPause() { Fixture f(48000); const int16_t pcm[] = {16384, 8192}; @@ -149,6 +353,27 @@ void TestMonoRateAndRouting() { "22.05 kHz effect duration was incorrect"); } +void TestMatrixOutputChannels() { + Fixture f(48000, 2); + Ngs2RackInternal mastering_rack {}; + mastering_rack.type = Ngs2RackType::Mastering; + Ngs2VoiceInternal mastering; + mastering.rack = &mastering_rack; + mastering.channels = 8; + mastering.state = Ngs2VoicePlayState::Playing; + f.voice.matrices = {{1.0f, 0.0f, 0.0f, 1.0f}}; + f.voice.ports.push_back({&mastering, 0, 1.0f, 0}); + const int16_t pcm[] = {8192, -8192}; + f.Queue(pcm, 1, 0); + Ngs2RenderVoice(mastering, {&f.voice, &mastering}, 1); + Check(mastering.samples[0] == 0.25f && mastering.samples[1] == -0.25f, + "stereo matrix did not route its output channels"); + for (size_t channel = 2; channel < mastering.channels; ++channel) { + Check(mastering.samples[channel] == 0.0f, + "matrix routed audio to an unspecified output channel"); + } +} + void TestCustomPcmStillPlays() { Fixture f(48000, 1, Ngs2RackType::CustomSampler); const int16_t pcm[] = {8192, -16384}; @@ -190,6 +415,29 @@ void TestSamplerReuseResetsRouting() { } } +void TestDeferredSamplerSetup() { + Fixture f(48000); + struct Setup { + Ngs2VoiceParamHeader header {40, 0, 0x10000000}; + Ngs2WaveformFormat format; + uint32_t flags = 0, reserved = 0; + } setup; + Check(Ngs2VoiceControl(reinterpret_cast(&f.voice), &setup.header) == OK, + "deferred stream setup failed"); + Check(f.voice.state == Ngs2VoicePlayState::Empty && f.voice.channels == 0 && + f.voice.sample_rate == 0 && f.voice.decoder == nullptr, + "deferred stream setup did not clear the sampler"); + setup.format = {0x12, 1, 48000}; + Check(Ngs2VoiceControl(reinterpret_cast(&f.voice), &setup.header) == OK, + "sampler setup after deferred stream failed"); + const int16_t pcm = 8192; + f.Queue(&pcm, 1, 0); + f.voice.SetEvent(1); + f.Render(1); + Check(f.voice.samples[0] == 0.25f, + "sampler did not play after deferred stream setup"); +} + void TestPlayStateBeforeRender() { Fixture f(48000); const int16_t pcm[] = {8192, -16384}; @@ -270,7 +518,7 @@ void TestStopThenPlayBeforeRender() { void SetLowPass(Fixture& f, float frequency = 1000.0f, uint64_t mask = 0) { struct Param { Ngs2VoiceParamHeader header {56, 0, 0x1000000a}; - Ngs2SamplerFilterParam filter {0, 1, 0, 1, 1000.0f, 0.70710678f, 1.0f, {}}; + Ngs2SamplerFilterParam filter {0, 1, 1, 0, 1000.0f, 0.70710678f, 1.0f, {}}; } param; param.filter.frequency = frequency; param.filter.channel_mask = mask; @@ -366,8 +614,8 @@ void TestFilterControlLayout() { Fixture f(48000, 2); f.rack.option.sampler.max_filters = 8; // A 56-byte control command with nonzero trailing padding. - alignas(8) uint32_t words[] = {56, 0x1000000a, 0, 1, 0, 0, 1, - 0x45d58800, 0x3fd55555, 0x3f800000, 0, 0, 0, 0x13f}; + alignas(8) uint32_t words[] = {56, 0x1000000a, 0, 1, 1, 0, 0, + 0, 0x45d58800, 0x3fd55555, 0x3f800000, 0, 0, 0x13f}; Ngs2VoiceControl(reinterpret_cast(&f.voice), reinterpret_cast(words)); Check(f.voice.filters.size() == 1 && f.voice.filters[0].enabled, @@ -377,8 +625,8 @@ void TestFilterControlLayout() { const double expected = (1 - std::cos(omega)) / (2 * (1 + std::sin(omega) / (2 * q))); Check(std::abs(f.voice.filters[0].b0 - expected) < 1e-10, "FCQ fields were read at the wrong offsets"); - words[4] = 1; // Bypass channel 0 in the low half of the 64-bit mask. - words[5] = 1; // Bypass channel 32 in the high half. + words[6] = 1; // Bypass channel 0 in the low half of the 64-bit mask. + words[7] = 1; // Bypass channel 32 in the high half. Fixture wide(48000, 33); wide.rack.option.sampler.max_filters = 8; Ngs2VoiceControl(reinterpret_cast(&wide.voice), @@ -389,8 +637,8 @@ void TestFilterControlLayout() { Check(wide.voice.samples[0] == 0.5f && wide.voice.samples[32] == 0.5f && std::abs(wide.voice.samples[1] - expected * 0.5) < 1e-7, "channel mask was not decoded as a 64-bit bypass mask"); - words[6] = 0; // OFF ignores unused FCQ values and clears the old delay line. - words[7] = 0x7fc00000; + words[4] = 0; // OFF ignores unused FCQ values and clears the old delay line. + words[8] = 0x7fc00000; Ngs2VoiceControl(reinterpret_cast(&wide.voice), reinterpret_cast(words)); Check(!wide.voice.filters[0].enabled && wide.voice.filters[0].history.empty() && @@ -664,8 +912,14 @@ void TestFiniteFilterTail() { } // namespace int main() { + TestPcm16RenderOutput(); + TestSubmixerSetupAndRouting(); + TestWaveformBlockGeometry(); + TestAtrac9ResampleAfterStarvation(); + TestAtrac9ResampleAcrossFrames(); TestResamplingPhaseAfterStarvation(); TestSamplerReuseResetsRouting(); + TestDeferredSamplerSetup(); TestLoopCallbacksAndExit(); TestWaveformReadAddress(); TestFilterTailDuringStarvation(); @@ -679,8 +933,10 @@ int main() { TestCallbackPauseDuringLargeStep(); TestStreamingResample(); TestPitchLoopAndSkip(); + TestPitchClamps(); TestStarvationAndPause(); TestMonoRateAndRouting(); + TestMatrixOutputChannels(); TestCustomPcmStillPlays(); TestPlayStateBeforeRender(); TestStatePublication();