This commit is contained in:
Gursukh Sembi 2026-01-30 09:45:30 +00:00 committed by GitHub
commit cfac5d27b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1457 additions and 120 deletions

View File

@ -0,0 +1,325 @@
<!-- SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later# NGS2 HLE Implementation -->
## Overview
**libSceNgs2** (Next Generation Sound 2) is the PlayStation 4's high-level audio library, responsible for audio synthesis, mixing, and effects processing. This HLE (High-Level Emulation) implementation provides the core functionality needed to render audio in PS4 games.
### Architecture
The NGS2 system follows a hierarchical structure:
```
System (OrbisNgs2Handle)
└── Racks (OrbisNgs2Handle)
└── Voices (OrbisNgs2Handle)
```
- **System**: The top-level audio context that manages sample rate, grain size, and all child racks
- **Rack**: A processing unit of a specific type (Sampler, Submixer, Mastering, etc.) containing multiple voices
- **Voice**: An individual audio channel that can be configured, played, paused, and stopped
### Audio Flow
```
Sampler Racks → Submixer Racks → Mastering Rack → Output Buffers
```
---
## Implementation Status
### ✅ Fully Implemented
| Feature | Description |
|---------|-------------|
| System Create/Destroy | System lifecycle with buffer allocation |
| System Query Buffer Size | Calculate required buffer sizes |
| Rack Create/Destroy | Sampler rack creation with voice allocation |
| Rack Query Buffer Size | Calculate rack buffer requirements |
| Voice Handle Retrieval | Get voice handles from rack |
| Voice State Management | State flags, play/pause/stop/kill/resume |
| PCM16 Playback | Decode and render 16-bit PCM audio |
| Streaming Audio | Ring buffer-based streaming with 3-slot circular buffer |
| One-Shot Playback | Single-buffer audio playback |
| Pitch Control | Variable playback speed via pitch ratio |
| Port Volume | Per-voice volume control |
| Sample Rate Conversion | Resampling with linear interpolation |
| Multi-channel Support | Mono to 8-channel audio |
| Pan Volume Matrix | Basic stereo panning calculations |
### 🚧 Partially Implemented (Stubbed)
| Feature | Status | Notes |
|---------|--------|-------|
| Mastering Rack | Stubbed | Params accepted but not processed |
| Submixer Rack | Stubbed | Created but no mixing logic |
| Matrix Levels | Stubbed | Returns identity matrix |
| Port Matrix | Stubbed | Param accepted, no effect |
| Port Delay | Stubbed | Param accepted, no effect |
| Voice Patch | Stubbed | Routing not implemented |
| Voice Callback | Stubbed | Callbacks not invoked |
| Envelope | Stubbed | Always returns height 1.0 |
| Peak Meter | Stubbed | Always returns peak 1.0 |
### ❌ Not Implemented
| Feature | Notes |
|---------|-------|
| ATRAC9 Decoding | Compressed audio codec (0x40) |
| Reverb Rack (0x4001) | Effects processing |
| Equalizer Rack (0x4002) | Frequency band adjustment |
| Custom Rack (0x4003) | User-defined processing modules |
| Filter Processing | Biquad, lowpass, etc. |
| Compressor/Limiter | Dynamics processing |
| Distortion | Audio distortion effect |
| Chorus/Delay | Time-based effects |
| LFE (Low Frequency Effects) | Subwoofer channel handling |
| 3D Geometry (sceNgs2Geom*) | Spatial audio positioning |
| FFT Functions | Frequency analysis |
| Stream API | sceNgs2Stream* functions |
| Job Scheduler | Multi-threaded processing |
| Report Handlers | Debug/profiling callbacks |
---
## Rack IDs
```
0x1000 = Sampler (index 0)
0x2000 = Submixer (index 2)
0x2001 = Submixer alt (index 3)
0x3000 = Mastering (index 1)
0x4001 = Reverb (index 4)
0x4002 = Equalizer (index 5)
0x4003 = Custom (index 6)
```
## Sample Rates (valid values)
```
11025, 12000, 22050, 24000, 44100, 48000, 88200, 96000, 176400, 192000
```
## Waveform Types
Valid if: `(type & 0xFFFFFFF8) == 0x80 || type == 0x40 || (type - 0x10) < 0xD`
- `0x40` = ATRAC9
- `0x10-0x1C` = PCM variants
- `0x80-0x87` = PCM variants
Render buffer types: `0x12, 0x13, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D`
## Voice Control Params (ID → size)
```
1 → 0x18 MatrixLevels
2 → 0x10 PortVolume
3 → 0x10 PortMatrix
4 → 0x10 PortDelay
5 → 0x18 Patch
6 → 0x0C Event
7 → 0x20 Callback
0xC001 → 0x20 Custom
```
## Voice Events (param ID 6)
```
0 = Reset (clears state)
1 = Pause (sets 0x200)
2 = Stop (sets 0x400)
3 = Kill (sets 0x800)
4 = Resume A (sets 0x1000)
5 = Resume B (sets 0x2000)
```
## State Flags (bitfield)
```
0x01 = Playing
0x200 = Paused
0x400 = Stopped
0x800 = Killed
0x1000 = Resume A
0x2000 = Resume B
```
Init sets: `flags = (flags & 0xDF000000) | 0x20000101`
GetStateFlags returns: `flags & 0xFF`
## Module IDs (Custom Racks)
```
0x10=Envelope 0x11=Compressor 0x12=Distortion 0x13=Filter
0x14=Chorus 0x15=Delay 0x16=Reverb 0x17=Sampler
0x18=PitchShift 0x19=Unknown 0x1A=Limiter 0x1B=UserFx
0x1C=Mixer 0x1D=Generator 0x1E=EQ 0x70=Passthrough
```
## Callback Flags
Valid: `0, 1, 2, 3`
## Key Errors
```
0x804a0230 = Invalid system handle
0x804a0260 = Invalid rack ID
0x804a0300 = Invalid voice handle
0x804a0303 = Invalid voice event
0x804a0308 = Invalid voice control ID
0x804a030a = Invalid voice control size
0x804a0402 = Invalid waveform type
```
---
## Implementation
### Existing Structures (DO NOT REDEFINE)
The following are already defined in ngs2 headers:
- `OrbisNgs2Handle` → ngs2_impl.h
- `OrbisNgs2RackDEBUG` → ngs2_impl.h
- `OrbisNgs2RackOption` → ngs2.h
- `OrbisNgs2SystemDEBUG` → ngs2_impl.h
- `OrbisNgs2SystemOption` → ngs2_impl.h
- `OrbisNgs2VoiceState` → ngs2.h
- `OrbisNgs2VoicePortDEBUG` → ngs2.h
- `OrbisNgs2VoiceMatrixDEBUG` → ngs2.h
- `OrbisNgs2VoiceParamHeader` → ngs2.h
- `OrbisNgs2Sampler*` → ngs2_sampler.h
- `OrbisNgs2Submixer*` → ngs2_submixer.h
- `OrbisNgs2Mastering*` → ngs2_mastering.h
- `OrbisNgs2Reverb*` → ngs2_reverb.h
- `OrbisNgs2Custom*` → ngs2_custom.h
- `OrbisNgs2Eq*` → ngs2_eq.h
- `HandleInternal` → ngs2_impl.h
- `SystemInternal` → ngs2_impl.h
- `StackBuffer` → ngs2_impl.h
### New Internal Structures (add to ngs2_impl.h)
```cpp
struct RackInternal {
HandleInternal handle;
OrbisNgs2RackDEBUG DEBUG; // use existing struct
SystemInternal* ownerSystem;
std::vector<std::unique_ptr<VoiceInternal>> voices;
u32 rackType;
};
struct VoiceInternal {
HandleInternal handle;
RackInternal* ownerRack;
u32 voiceIndex;
u32 stateFlags;
std::vector<OrbisNgs2VoicePortDEBUG> ports; // use existing struct
std::vector<OrbisNgs2VoiceMatrixDEBUG> matrices; // use existing struct
};
// Add field to existing SystemInternal:
std::vector<RackInternal*> racks;
```
### Phase 1: Handles
- `sceNgs2SystemCreate`: Allocate `SystemInternal`, return as handle
- `sceNgs2SystemDestroy`: Free system and all racks
### Phase 2: Racks
- `sceNgs2RackQueryBufferSize`: Return size based on rack type
- `sceNgs2RackCreate`: Allocate `RackInternal`, map rackId→index, add to system
- `sceNgs2RackGetVoiceHandle`: Return `rack->voices[voiceIndex]`
### Phase 3: Voices
- `sceNgs2VoiceControl`: Parse linked list by param ID, apply per switch above
- `sceNgs2VoiceGetState`: Copy voice state struct
- `sceNgs2VoiceGetStateFlags`: Return `stateFlags & 0xFF`
### Phase 4: Render
```cpp
s32 sceNgs2SystemRender(handle, bufferDEBUG, numBufferDEBUG) {
// Validate: numBufferDEBUG in 1-16, buffers non-null, types valid
// Process racks: Samplers → Submixers → Mastering
// Write output: size = grainSamples * channels * bytesPerSample
return ORBIS_OK;
}
```
### Minimal Stub
```cpp
// SystemCreate
auto* sys = new SystemInternal();
*outHandle = reinterpret_cast<OrbisNgs2Handle>(sys);
// RackCreate
auto* rack = new RackInternal();
rack->voices.resize(option->maxVoices);
*outHandle = reinterpret_cast<OrbisNgs2Handle>(rack);
// VoiceGetStateFlags
*out = 0; // Not playing
// SystemRender
for (u32 i = 0; i < numBufferDEBUG; i++)
memset(bufferDEBUG[i].buffer, 0, bufferDEBUG[i].bufferSize);
```
## Files
| File | Purpose |
|------|---------|
| ngs2.cpp | Main API entry points and render loop |
| ngs2.h | Public API structures and types |
| ngs2_impl.cpp | System and rack lifecycle management |
| ngs2_impl.h | Core types (SystemInternal, HandleInternal) |
| ngs2_internal.h | Internal structures (VoiceInternal, RackInternal, RingBufferSlot) |
| ngs2_sampler.h | Sampler rack structures |
| ngs2_mastering.h | Mastering rack structures |
| ngs2_submixer.h | Submixer rack structures |
| ngs2_reverb.h | Reverb rack structures |
| ngs2_custom.h | Custom rack and module structures |
| ngs2_eq.h | Equalizer structures |
| ngs2_pan.h | Pan work and param structures |
| ngs2_geom.h | 3D geometry/spatial audio structures |
| ngs2_report.h | Report handler structures |
| ngs2_error.h | Error code definitions |
---
## Areas for Improvement
### High Priority
1. **ATRAC9 Decoding**: Many games use ATRAC9 compressed audio. Integration with an ATRAC9 decoder is critical for broader game compatibility.
2. **Mastering Rack Processing**: Currently stubbed - should apply gain, limiting, and LFE filtering to final output.
3. **Voice Routing (Patch)**: Voices should be able to route to submixers instead of directly to output.
4. **Streaming Buffer Gap**: Reduce/eliminate the audio gap between buffer consumption and refill. Currently there can be a brief silence when the ring buffer empties before the game refills it. Consider pre-buffering or predictive starvation signaling.
### Medium Priority
1. **Submixer Processing**: Implement actual mixing of multiple input voices with envelope and effects.
2. **Filter Implementation**: Biquad filters for EQ, lowpass, highpass processing.
3. **Envelope Processing**: ADSR envelopes for volume shaping.
4. **Better Resampling**: Current linear interpolation could be upgraded to sinc or polyphase for higher quality.
### Low Priority
1. **Reverb/Delay Effects**: Time-based audio effects for spatial depth.
2. **3D Geometry API**: Spatial positioning with distance attenuation and panning.
3. **Compressor/Limiter**: Dynamics processing for mastering.
4. **Peak Metering**: Accurate level measurement for debugging.
5. **Multi-threaded Rendering**: Job scheduler for parallel voice processing.
### Code Quality
1. **Thread Safety**: Add proper mutex locking for multi-threaded game access.
2. **Memory Management**: Consider using the game-provided buffer allocator instead of `new`.
3. **Waveform Block Repeats**: Loop handling for blocks with `numRepeats > 0`.
4. **Callback Invocation**: Actually invoke registered callbacks on buffer events.
---
## Audio Data Management
In the context of the NGS2 HLE implementation, audio data is processed primarily through the **Sampler Rack (0x1000)**. The system differentiates between short, memory-resident sounds (One-Shots) and longer, buffered content (Streaming) based on how the waveform data is supplied and managed during the `sceNgs2SystemRender` cycle.
#### One-Shot Playback
One-shots are typically used for UI sounds, sound effects, or short musical stings.
* **Memory Layout:** The entire audio payload (whether raw PCM or a complete compressed ATRAC9 block) is loaded into a contiguous block of RAM by the application before playback begins.
* **Voice Handling:** When the voice is triggered, the `VoiceInternal` structure maintains a read cursor relative to the start of this static memory region.
* **Rendering:** During the render pass, the system decodes or copies data starting from the current cursor position. If the sound is looped, the cursor simply jumps back to a defined loop-start offset upon reaching the end. Since the data is static, no synchronization with game-side file I/O is required.
#### Streamed Playback
Streaming is used for background music (BGM) or long speech tracks to conserve memory.
* **Ring Buffer Mechanism:** Instead of a linear buffer, the voice utilizes a fixed-size circular buffer (3 slots). The application (Producer) and the NGS2 renderer (Consumer) operate concurrently on this buffer.
* **The Game's Role:** Periodically fills sections of the buffer that have already been played, ensuring the "Write Head" stays ahead of the "Read Head."
* **NGS2's Role:** The `SystemRender` function consumes data from the "Read Head" position and signals starvation when buffers run low.
* **Starvation Handling:** When the ring buffer count drops to the threshold, the stateFlags `0x80` bit is set to signal the game to provide more data.
* **Decoding State:** For compressed formats like ATRAC9 (`0x40`), the decoder context must be preserved seamlessly across buffer transitions to prevent audio artifacts.

View File

@ -1,6 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cmath>
#include <cstring>
#include "common/logging/log.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/libs.h"
@ -9,11 +12,267 @@
#include "core/libraries/ngs2/ngs2_error.h"
#include "core/libraries/ngs2/ngs2_geom.h"
#include "core/libraries/ngs2/ngs2_impl.h"
#include "core/libraries/ngs2/ngs2_internal.h"
#include "core/libraries/ngs2/ngs2_pan.h"
#include "core/libraries/ngs2/ngs2_report.h"
#include "core/libraries/ngs2/ngs2_sampler.h"
namespace Libraries::Ngs2 {
// =============================================================================
// Audio Decoder Interface
// =============================================================================
// Waveform type constants
// Add new formats here as they are discovered/implemented
enum WaveformType : u32 {
PCM16 = 0x12, // 16-bit PCM little-endian (confirmed working)
// TODO: Add more formats
};
// Check if waveform type is supported
static bool IsWaveformTypeSupported(u32 waveform_type) {
switch (waveform_type) {
case WaveformType::PCM16:
return true;
// TODO: Add cases for new formats here
default:
return false;
}
}
// Get bytes per sample for a waveform type
static u32 GetBytesPerSample(u32 waveform_type, u32 num_channels) {
switch (waveform_type) {
case WaveformType::PCM16:
return 2 * num_channels;
// TODO: Add cases for new formats here
// case WaveformType::FLOAT:
// return 4 * num_channels;
default:
return 2 * num_channels;
}
}
// =============================================================================
// PCM16 Decoder
// =============================================================================
static float DecodePCM16Sample(const void* data, u32 sample_idx, u32 channel, u32 num_channels,
u32 total_samples) {
if (sample_idx >= total_samples) {
return 0.0f;
}
const s16* src_data = reinterpret_cast<const s16*>(data);
s16 sample = src_data[sample_idx * num_channels + channel];
return static_cast<float>(sample) / 32768.0f;
}
// =============================================================================
// Add new decoder functions here
// =============================================================================
// Example:
// static float DecodeFloatSample(const void* data, u32 sample_idx, u32 channel,
// u32 num_channels, u32 total_samples) {
// const float* src_data = reinterpret_cast<const float*>(data);
// return src_data[sample_idx * num_channels + channel];
// }
// =============================================================================
// Unified Sample Decoder
// =============================================================================
static float DecodeSample(const void* data, u32 sample_idx, u32 channel, u32 num_channels,
u32 total_samples, u32 waveform_type) {
switch (waveform_type) {
case WaveformType::PCM16:
return DecodePCM16Sample(data, sample_idx, channel, num_channels, total_samples);
// TODO: Add cases for new formats here
default:
return 0.0f;
}
}
// =============================================================================
// Voice Renderer
// =============================================================================
struct RenderContext {
const OrbisNgs2RenderBufferInfo* buffer_info;
u32 num_buffer_info;
u32 grain_samples;
u32 output_sample_rate;
};
static bool RenderVoice(VoiceInternal* voice, const RenderContext& ctx) {
if (!voice || !voice->isSetup) {
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Voice not setup or invalid");
return false;
}
// Check if voice is playing
if (!(voice->stateFlags & 0x01)) {
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Voice not playing");
return false;
}
// Check if voice is paused, stopped, or killed
if (voice->stateFlags & (0x200 | 0x400 | 0x800)) {
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Voice paused, stopped, or killed");
return false;
}
u32 waveform_type = voice->format.waveformType;
if (!IsWaveformTypeSupported(waveform_type)) {
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Unsupported waveform type: {:#x}", waveform_type);
return false;
}
// Get current slot from ring buffer
RingBufferSlot* current_slot = voice->getCurrentSlot();
if (!current_slot || !current_slot->valid || current_slot->consumed) {
LOG_DEBUG(Lib_Ngs2, "(STUBBED) No valid buffer in ring");
return false;
}
voice->currentBufferPtr = current_slot->basePtr;
u32 num_channels = voice->format.numChannels;
if (num_channels == 0 || num_channels > 8) {
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Invalid number of channels: {}", num_channels);
return false;
}
// Calculate sample rate ratio for resampling
u32 source_sample_rate = voice->format.sampleRate;
if (source_sample_rate == 0) {
source_sample_rate = 48000;
}
float pitch_ratio = voice->pitchRatio;
if (pitch_ratio <= 0.0f) {
pitch_ratio = 1.0f;
}
float sample_rate_ratio = (static_cast<float>(source_sample_rate) * pitch_ratio) /
static_cast<float>(ctx.output_sample_rate);
// Find matching output buffer and render
for (u32 buf_idx = 0; buf_idx < ctx.num_buffer_info; buf_idx++) {
const auto& buf_info = ctx.buffer_info[buf_idx];
if (!buf_info.buffer || buf_info.bufferSize == 0) {
continue;
}
const void* src_data = current_slot->data;
u32 total_samples = current_slot->numSamples;
if (total_samples == 0) {
continue;
}
// Determine output format
// Output buffer format detection - use raw values since we only know PCM16 for sure
float* dst_float = nullptr;
s16* dst_s16 = nullptr;
u32 dst_channels = 2;
bool output_is_float = false;
if (buf_info.waveformType == WaveformType::PCM16) {
dst_s16 = reinterpret_cast<s16*>(buf_info.buffer);
dst_channels = buf_info.numChannels > 0 ? buf_info.numChannels : num_channels;
output_is_float = false;
} else {
// Default to float output for unknown types
dst_float = reinterpret_cast<float*>(buf_info.buffer);
dst_channels = buf_info.numChannels > 0 ? buf_info.numChannels : 2;
output_is_float = true;
}
// Render samples
float current_pos = voice->samplePosFloat;
bool voice_stopped = false;
for (u32 out_sample = 0; out_sample < ctx.grain_samples && !voice_stopped; out_sample++) {
u32 sample_int = static_cast<u32>(current_pos);
float frac = current_pos - static_cast<float>(sample_int);
// Check if we've reached the end of current buffer
if (sample_int >= total_samples) {
voice->lastConsumedBuffer = current_slot->basePtr;
voice->totalDecodedSamples += total_samples;
voice->advanceReadIndex();
if (voice->getReadyBufferCount() <= VoiceInternal::STARVATION_THRESHOLD) {
voice->stateFlags |= 0x80;
}
current_slot = voice->getCurrentSlot();
if (current_slot && current_slot->valid && !current_slot->consumed) {
src_data = current_slot->data;
total_samples = current_slot->numSamples;
current_pos = 0.0f;
sample_int = 0;
frac = 0.0f;
voice->isStreaming = true;
voice->currentBufferPtr = current_slot->basePtr;
} else {
if (voice->isStreaming) {
current_pos = 0.0f;
break;
} else {
voice->stateFlags &= ~0x01;
voice->stateFlags |= 0x400;
voice_stopped = true;
break;
}
}
}
// Decode and interpolate samples for each output channel
for (u32 ch = 0; ch < dst_channels; ch++) {
u32 src_ch = ch < num_channels ? ch : 0;
float sample0 = DecodeSample(src_data, sample_int, src_ch, num_channels,
total_samples, waveform_type);
float sample1 = DecodeSample(src_data, sample_int + 1, src_ch, num_channels,
total_samples, waveform_type);
float sample = sample0 + frac * (sample1 - sample0);
// Apply port volume
sample *= voice->portVolume;
// Write to output buffer
u32 dst_idx = out_sample * dst_channels + ch;
if (output_is_float) {
if (dst_idx * sizeof(float) < buf_info.bufferSize) {
dst_float[dst_idx] += sample;
dst_float[dst_idx] = std::clamp(dst_float[dst_idx], -1.0f, 1.0f);
}
} else {
if (dst_idx * 2 < buf_info.bufferSize) {
s32 mixed = dst_s16[dst_idx] + static_cast<s32>(sample * 32768.0f);
dst_s16[dst_idx] = static_cast<s16>(std::clamp(mixed, -32768, 32767));
}
}
}
current_pos += sample_rate_ratio;
}
voice->samplePosFloat = current_pos;
voice->currentSamplePos = static_cast<u32>(current_pos);
return true;
}
return false;
}
// =============================================================================
// API Functions
// =============================================================================
// Ngs2
s32 PS4_SYSV_ABI sceNgs2CalcWaveformBlock(const OrbisNgs2WaveformFormat* format, u32 samplePos,
@ -51,30 +310,45 @@ s32 PS4_SYSV_ABI sceNgs2RackCreate(OrbisNgs2Handle systemHandle, u32 rackId,
const OrbisNgs2RackOption* option,
const OrbisNgs2ContextBufferInfo* bufferInfo,
OrbisNgs2Handle* outHandle) {
LOG_ERROR(Lib_Ngs2, "rackId = {}", rackId);
LOG_DEBUG(Lib_Ngs2, "rackId = {:#x}, maxVoices = {}", rackId, option ? option->maxVoices : 0);
if (!systemHandle) {
LOG_ERROR(Lib_Ngs2, "systemHandle is nullptr");
return ORBIS_NGS2_ERROR_INVALID_SYSTEM_HANDLE;
}
return ORBIS_OK;
auto* system = reinterpret_cast<SystemInternal*>(systemHandle);
return RackCreate(system, rackId, option, bufferInfo, outHandle);
}
s32 PS4_SYSV_ABI sceNgs2RackCreateWithAllocator(OrbisNgs2Handle systemHandle, u32 rackId,
const OrbisNgs2RackOption* option,
const OrbisNgs2BufferAllocator* allocator,
OrbisNgs2Handle* outHandle) {
LOG_ERROR(Lib_Ngs2, "rackId = {}", rackId);
LOG_DEBUG(Lib_Ngs2, "rackId = {:#x}, maxVoices = {}", rackId, option ? option->maxVoices : 0);
if (!systemHandle) {
LOG_ERROR(Lib_Ngs2, "systemHandle is nullptr");
return ORBIS_NGS2_ERROR_INVALID_SYSTEM_HANDLE;
}
return ORBIS_OK;
if (!outHandle) {
return ORBIS_NGS2_ERROR_INVALID_OUT_ADDRESS;
}
auto* system = reinterpret_cast<SystemInternal*>(systemHandle);
// Create rack with null buffer info (allocator version doesn't use pre-allocated buffer)
return RackCreate(system, rackId, option, nullptr, outHandle);
}
s32 PS4_SYSV_ABI sceNgs2RackDestroy(OrbisNgs2Handle rackHandle,
OrbisNgs2ContextBufferInfo* outBufferInfo) {
LOG_ERROR(Lib_Ngs2, "called");
return ORBIS_OK;
LOG_DEBUG(Lib_Ngs2, "called");
if (!rackHandle) {
return ORBIS_NGS2_ERROR_INVALID_RACK_HANDLE;
}
auto* rack = reinterpret_cast<RackInternal*>(rackHandle);
return RackDestroy(rack, outBufferInfo);
}
s32 PS4_SYSV_ABI sceNgs2RackGetInfo(OrbisNgs2Handle rackHandle, OrbisNgs2RackInfo* outInfo,
@ -90,7 +364,21 @@ s32 PS4_SYSV_ABI sceNgs2RackGetUserData(OrbisNgs2Handle rackHandle, uintptr_t* o
s32 PS4_SYSV_ABI sceNgs2RackGetVoiceHandle(OrbisNgs2Handle rackHandle, u32 voiceIndex,
OrbisNgs2Handle* outHandle) {
LOG_DEBUG(Lib_Ngs2, "(STUBBED) voiceIndex = {}", voiceIndex);
LOG_DEBUG(Lib_Ngs2, "voiceIndex = {}", voiceIndex);
if (!rackHandle) {
return ORBIS_NGS2_ERROR_INVALID_RACK_HANDLE;
}
auto* rack = reinterpret_cast<RackInternal*>(rackHandle);
if (voiceIndex >= rack->voices.size()) {
LOG_ERROR(Lib_Ngs2, "Invalid voice index {} (max {})", voiceIndex, rack->voices.size());
return ORBIS_NGS2_ERROR_INVALID_VOICE_INDEX;
}
if (outHandle) {
*outHandle = reinterpret_cast<OrbisNgs2Handle>(rack->voices[voiceIndex].get());
}
return ORBIS_OK;
}
@ -101,7 +389,48 @@ s32 PS4_SYSV_ABI sceNgs2RackLock(OrbisNgs2Handle rackHandle) {
s32 PS4_SYSV_ABI sceNgs2RackQueryBufferSize(u32 rackId, const OrbisNgs2RackOption* option,
OrbisNgs2ContextBufferInfo* outBufferInfo) {
LOG_ERROR(Lib_Ngs2, "rackId = {}", rackId);
LOG_DEBUG(Lib_Ngs2, "rackId = {:#x}, option = {}", rackId, static_cast<const void*>(option));
if (!outBufferInfo) {
return ORBIS_NGS2_ERROR_INVALID_OUT_ADDRESS;
}
u32 rack_index = RackIdToIndex(rackId);
if (rack_index == 0xFF) {
LOG_ERROR(Lib_Ngs2, "Invalid rack ID: {:#x}", rackId);
return ORBIS_NGS2_ERROR_INVALID_RACK_ID;
}
// Use defaults if option is NULL - based on libSceNgs2.c analysis
u32 max_voices = 1;
u32 max_ports = 1;
u32 max_matrices = 1;
if (option && option->size >= sizeof(OrbisNgs2RackOption)) {
max_voices = option->maxVoices > 0 ? option->maxVoices : 1;
max_ports = option->maxPorts > 0 ? option->maxPorts : 1;
max_matrices = option->maxMatrices > 0 ? option->maxMatrices : 1;
} else {
// Sampler rack (0x1000) defaults to 256 voices
if (rackId == 0x1000) {
max_voices = 256;
}
}
// Calculate required buffer size
size_t base_size = sizeof(RackInternal);
size_t voice_size = sizeof(VoiceInternal) * max_voices;
size_t port_size = sizeof(OrbisNgs2VoicePortInfo) * max_ports * max_voices;
size_t matrix_size = sizeof(OrbisNgs2VoiceMatrixInfo) * max_matrices * max_voices;
size_t total_size = base_size + voice_size + port_size + matrix_size;
total_size = (total_size + 0xFF) & ~0xFF; // Align to 256 bytes
outBufferInfo->hostBuffer = nullptr;
outBufferInfo->hostBufferSize = total_size;
std::memset(outBufferInfo->reserved, 0, sizeof(outBufferInfo->reserved));
LOG_DEBUG(Lib_Ngs2, "Required buffer size: {} bytes", total_size);
return ORBIS_OK;
}
@ -119,7 +448,7 @@ s32 PS4_SYSV_ABI sceNgs2SystemCreate(const OrbisNgs2SystemOption* option,
const OrbisNgs2ContextBufferInfo* bufferInfo,
OrbisNgs2Handle* outHandle) {
s32 result;
OrbisNgs2ContextBufferInfo localInfo;
OrbisNgs2ContextBufferInfo local_info;
if (!bufferInfo || !outHandle) {
if (!bufferInfo) {
result = ORBIS_NGS2_ERROR_INVALID_BUFFER_INFO;
@ -132,19 +461,19 @@ s32 PS4_SYSV_ABI sceNgs2SystemCreate(const OrbisNgs2SystemOption* option,
// TODO: Report errors?
} else {
// Make bufferInfo copy
localInfo.hostBuffer = bufferInfo->hostBuffer;
localInfo.hostBufferSize = bufferInfo->hostBufferSize;
local_info.hostBuffer = bufferInfo->hostBuffer;
local_info.hostBufferSize = bufferInfo->hostBufferSize;
for (int i = 0; i < 5; i++) {
localInfo.reserved[i] = bufferInfo->reserved[i];
local_info.reserved[i] = bufferInfo->reserved[i];
}
localInfo.userData = bufferInfo->userData;
local_info.userData = bufferInfo->userData;
result = SystemSetup(option, &localInfo, 0, outHandle);
result = SystemSetup(option, &local_info, 0, outHandle);
}
// TODO: API reporting?
LOG_INFO(Lib_Ngs2, "called");
LOG_DEBUG(Lib_Ngs2, "called");
return result;
}
@ -153,20 +482,19 @@ s32 PS4_SYSV_ABI sceNgs2SystemCreateWithAllocator(const OrbisNgs2SystemOption* o
OrbisNgs2Handle* outHandle) {
s32 result;
if (allocator && allocator->allocHandler != 0) {
OrbisNgs2BufferAllocHandler hostAlloc = allocator->allocHandler;
OrbisNgs2BufferAllocHandler host_alloc = allocator->allocHandler;
if (outHandle) {
OrbisNgs2BufferFreeHandler hostFree = allocator->freeHandler;
OrbisNgs2ContextBufferInfo bufferInfo;
result = SystemSetup(option, &bufferInfo, 0, 0);
OrbisNgs2BufferFreeHandler host_free = allocator->freeHandler;
OrbisNgs2ContextBufferInfo buffer_info;
result = SystemSetup(option, &buffer_info, 0, 0);
if (result >= 0) {
uintptr_t sysUserData = allocator->userData;
result = Core::ExecuteGuest(hostAlloc, &bufferInfo);
result = Core::ExecuteGuest(host_alloc, &buffer_info);
if (result >= 0) {
OrbisNgs2Handle* handleCopy = outHandle;
result = SystemSetup(option, &bufferInfo, hostFree, handleCopy);
OrbisNgs2Handle* handle_copy = outHandle;
result = SystemSetup(option, &buffer_info, host_free, handle_copy);
if (result < 0) {
if (hostFree) {
Core::ExecuteGuest(hostFree, &bufferInfo);
if (host_free) {
Core::ExecuteGuest(host_free, &buffer_info);
}
}
}
@ -179,7 +507,7 @@ s32 PS4_SYSV_ABI sceNgs2SystemCreateWithAllocator(const OrbisNgs2SystemOption* o
result = ORBIS_NGS2_ERROR_INVALID_BUFFER_ALLOCATOR;
LOG_ERROR(Lib_Ngs2, "Invalid system buffer allocator {}", (void*)allocator);
}
LOG_INFO(Lib_Ngs2, "called");
LOG_DEBUG(Lib_Ngs2, "called");
return result;
}
@ -189,7 +517,7 @@ s32 PS4_SYSV_ABI sceNgs2SystemDestroy(OrbisNgs2Handle systemHandle,
LOG_ERROR(Lib_Ngs2, "systemHandle is nullptr");
return ORBIS_NGS2_ERROR_INVALID_SYSTEM_HANDLE;
}
LOG_INFO(Lib_Ngs2, "called");
LOG_DEBUG(Lib_Ngs2, "called");
return ORBIS_OK;
}
@ -237,7 +565,7 @@ s32 PS4_SYSV_ABI sceNgs2SystemQueryBufferSize(const OrbisNgs2SystemOption* optio
s32 result;
if (outBufferInfo) {
result = SystemSetup(option, outBufferInfo, 0, 0);
LOG_INFO(Lib_Ngs2, "called");
LOG_DEBUG(Lib_Ngs2, "called");
} else {
result = ORBIS_NGS2_ERROR_INVALID_OUT_ADDRESS;
LOG_ERROR(Lib_Ngs2, "Invalid system buffer info {}", (void*)outBufferInfo);
@ -249,11 +577,56 @@ s32 PS4_SYSV_ABI sceNgs2SystemQueryBufferSize(const OrbisNgs2SystemOption* optio
s32 PS4_SYSV_ABI sceNgs2SystemRender(OrbisNgs2Handle systemHandle,
const OrbisNgs2RenderBufferInfo* aBufferInfo,
u32 numBufferInfo) {
LOG_DEBUG(Lib_Ngs2, "(STUBBED) numBufferInfo = {}", numBufferInfo);
if (!systemHandle) {
LOG_ERROR(Lib_Ngs2, "systemHandle is nullptr");
return ORBIS_NGS2_ERROR_INVALID_SYSTEM_HANDLE;
}
if (!aBufferInfo || numBufferInfo == 0 || numBufferInfo > 16) {
LOG_ERROR(Lib_Ngs2, "Invalid buffer info: ptr={}, count={}",
static_cast<const void*>(aBufferInfo), numBufferInfo);
return ORBIS_NGS2_ERROR_INVALID_BUFFER_ADDRESS;
}
auto* system = reinterpret_cast<SystemInternal*>(systemHandle);
// Clear all output buffers first
for (u32 i = 0; i < numBufferInfo; i++) {
if (aBufferInfo[i].buffer && aBufferInfo[i].bufferSize > 0) {
std::memset(aBufferInfo[i].buffer, 0, aBufferInfo[i].bufferSize);
}
}
// Setup render context
RenderContext ctx{};
ctx.buffer_info = aBufferInfo;
ctx.num_buffer_info = numBufferInfo;
ctx.grain_samples = system->numGrainSamples > 0 ? system->numGrainSamples : 256;
ctx.output_sample_rate = system->sampleRate > 0 ? system->sampleRate : 48000;
u32 voices_rendered = 0;
// Process each rack
for (auto* rack : system->racks) {
if (!rack) {
continue;
}
// Process sampler racks (0x1000)
if (rack->rackId == 0x1000) {
for (auto& voice : rack->voices) {
if (RenderVoice(voice.get(), ctx)) {
voices_rendered++;
}
}
}
// TODO: Process submixer racks (0x2000, 0x2001)
// TODO: Process mastering racks (0x3000)
// TODO: Process effect racks (0x4001, 0x4002, 0x4003)
}
system->renderCount++;
return ORBIS_OK;
}
@ -267,7 +640,7 @@ static s32 PS4_SYSV_ABI sceNgs2SystemResetOption(OrbisNgs2SystemOption* outOptio
}
*outOption = option;
LOG_INFO(Lib_Ngs2, "called");
LOG_DEBUG(Lib_Ngs2, "called");
return ORBIS_OK;
}
@ -309,13 +682,201 @@ s32 PS4_SYSV_ABI sceNgs2SystemUnlock(OrbisNgs2Handle systemHandle) {
s32 PS4_SYSV_ABI sceNgs2VoiceControl(OrbisNgs2Handle voiceHandle,
const OrbisNgs2VoiceParamHeader* paramList) {
LOG_ERROR(Lib_Ngs2, "called");
if (!voiceHandle) {
LOG_ERROR(Lib_Ngs2, "voiceHandle is nullptr");
return ORBIS_NGS2_ERROR_INVALID_VOICE_HANDLE;
}
auto* voice = reinterpret_cast<VoiceInternal*>(voiceHandle);
const OrbisNgs2VoiceParamHeader* current = paramList;
while (current != nullptr) {
switch (current->id) {
// Sampler rack-specific params (0x1000000X)
case 0x10000000: { // Sampler Voice Setup
auto* setup = reinterpret_cast<const OrbisNgs2SamplerVoiceSetupParam*>(current);
voice->format = setup->format;
voice->flags = setup->flags;
voice->isSetup = true;
break;
}
case 0x10000001: { // Sampler Waveform Blocks
auto* blocks =
reinterpret_cast<const OrbisNgs2SamplerVoiceWaveformBlocksParam*>(current);
u32 total_data_size = 0;
u32 total_samples = 0;
u32 num_channels = voice->format.numChannels > 0 ? voice->format.numChannels : 2;
if (blocks->numBlocks > 0 && blocks->aBlock) {
for (u32 i = 0; i < blocks->numBlocks; i++) {
total_data_size += blocks->aBlock[i].dataSize;
total_samples += blocks->aBlock[i].numSamples;
}
voice->waveformBlocks.resize(blocks->numBlocks);
for (u32 i = 0; i < blocks->numBlocks; i++) {
voice->waveformBlocks[i].dataOffset = blocks->aBlock[i].dataOffset;
voice->waveformBlocks[i].dataSize = blocks->aBlock[i].dataSize;
voice->waveformBlocks[i].numRepeats = blocks->aBlock[i].numRepeats;
voice->waveformBlocks[i].numSkipSamples = blocks->aBlock[i].numSkipSamples;
voice->waveformBlocks[i].numSamples = blocks->aBlock[i].numSamples;
voice->waveformBlocks[i].currentRepeat = 0;
}
}
const u8* base_ptr = reinterpret_cast<const u8*>(blocks->data);
u32 data_offset =
(blocks->numBlocks > 0 && blocks->aBlock) ? blocks->aBlock[0].dataOffset : 0;
const void* actual_data_ptr = base_ptr + data_offset;
bool is_first_setup = (voice->ringBufferCount == 0) && !voice->isStreaming;
if (is_first_setup) {
voice->resetRing();
voice->addToRing(blocks->data, actual_data_ptr, total_data_size, total_samples);
voice->lastConsumedBuffer = nullptr;
voice->currentBufferPtr = actual_data_ptr;
voice->stateFlags &= ~0x80;
voice->currentSamplePos = 0;
voice->samplePosFloat = 0.0f;
voice->currentBlockIndex = 0;
voice->isStreaming = false;
} else {
bool added =
voice->addToRing(blocks->data, actual_data_ptr, total_data_size, total_samples);
if (added) {
voice->isStreaming = true;
voice->stateFlags &= ~0x80;
}
}
break;
}
case 0x10000005: { // Sampler Pitch
auto* pitch = reinterpret_cast<const OrbisNgs2SamplerVoicePitchParam*>(current);
voice->pitchRatio = pitch->ratio;
break;
}
// Mastering rack-specific params (0x3000000X) - not yet implemented
case 0x30000000:
case 0x30000001:
case 0x30000002:
case 0x30000003:
case 0x30000004:
case 0x30000005:
case 0x30000006:
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Mastering param ID: {:#x}", current->id);
break;
// Generic voice params
case 0x06: { // Voice Event
auto* event = reinterpret_cast<const OrbisNgs2VoiceEventParam*>(current);
switch (event->eventId) {
case 0: // Reset
voice->stateFlags = (voice->stateFlags & 0xDF000000) | 0x20000101;
voice->totalDecodedSamples = 0;
voice->currentBufferPtr = nullptr;
voice->lastConsumedBuffer = nullptr;
voice->currentSamplePos = 0;
voice->samplePosFloat = 0.0f;
voice->currentBlockIndex = 0;
voice->isStreaming = false;
voice->waveformBlocks.clear();
voice->resetRing();
break;
case 1: // Pause
voice->stateFlags |= 0x200;
break;
case 2: // Stop
voice->stateFlags &= ~0x01;
voice->stateFlags |= 0x400;
break;
case 3: // Kill
voice->stateFlags &= ~0x01;
voice->stateFlags |= 0x800;
voice->resetRing();
break;
case 4: // Resume A
voice->stateFlags = (voice->stateFlags & ~0x200) | 0x1000 | 0x01;
break;
case 5: // Resume B
voice->stateFlags = (voice->stateFlags & ~0x200) | 0x2000 | 0x01;
break;
default:
LOG_WARNING(Lib_Ngs2, "Unknown voice event ID: {}", event->eventId);
break;
}
break;
}
case 0x01: { // Matrix Levels - not yet implemented
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Matrix levels param");
break;
}
case 0x02: { // Port Volume
auto* portVol = reinterpret_cast<const OrbisNgs2VoicePortVolumeParam*>(current);
voice->portVolume = portVol->level;
break;
}
case 0x03: { // Port Matrix - not yet implemented
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Port matrix param");
break;
}
case 0x04: { // Port Delay - not yet implemented
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Port delay param");
break;
}
case 0x05: { // Patch - not yet implemented
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Patch param");
break;
}
case 0x07: { // Callback - not yet implemented
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Callback param");
break;
}
default:
LOG_DEBUG(Lib_Ngs2, "(STUBBED) Unhandled voice param ID: {:#x}", current->id);
break;
}
// Move to next parameter
if (current->next == 0 || current->next == -1) {
break;
}
current = reinterpret_cast<const OrbisNgs2VoiceParamHeader*>(
reinterpret_cast<const u8*>(current) + current->next);
}
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNgs2VoiceGetMatrixInfo(OrbisNgs2Handle voiceHandle, u32 matrixId,
OrbisNgs2VoiceMatrixInfo* outInfo, size_t outInfoSize) {
LOG_ERROR(Lib_Ngs2, "matrixId = {}, outInfoSize = {}", matrixId, outInfoSize);
if (!voiceHandle) {
return ORBIS_NGS2_ERROR_INVALID_VOICE_HANDLE;
}
if (!outInfo || outInfoSize < sizeof(OrbisNgs2VoiceMatrixInfo)) {
return ORBIS_NGS2_ERROR_INVALID_OUT_ADDRESS;
}
auto* voice = reinterpret_cast<VoiceInternal*>(voiceHandle);
// Return default matrix info - identity matrix for stereo (1.0 on diagonal)
outInfo->numLevels = voice->format.numChannels * voice->format.numChannels;
if (outInfo->numLevels == 0) {
outInfo->numLevels = 4; // Default stereo 2x2
}
// Initialize to identity-like matrix
std::memset(outInfo->aLevel, 0, sizeof(outInfo->aLevel));
u32 channels = voice->format.numChannels > 0 ? voice->format.numChannels : 2;
for (u32 i = 0; i < channels && i < 8; i++) {
outInfo->aLevel[i * channels + i] = 1.0f;
}
return ORBIS_OK;
}
@ -333,12 +894,80 @@ s32 PS4_SYSV_ABI sceNgs2VoiceGetPortInfo(OrbisNgs2Handle voiceHandle, u32 port,
s32 PS4_SYSV_ABI sceNgs2VoiceGetState(OrbisNgs2Handle voiceHandle, OrbisNgs2VoiceState* outState,
size_t stateSize) {
LOG_ERROR(Lib_Ngs2, "stateSize = {}", stateSize);
if (!outState) {
LOG_ERROR(Lib_Ngs2, "Invalid voice state address");
return ORBIS_NGS2_ERROR_INVALID_OUT_ADDRESS;
}
// Only accept valid state sizes: 0x4 (basic) or 0x30 (sampler)
if (stateSize != sizeof(OrbisNgs2VoiceState) &&
stateSize != sizeof(OrbisNgs2SamplerVoiceState)) {
LOG_ERROR(Lib_Ngs2, "Invalid voice state size: {:#x}", stateSize);
return ORBIS_NGS2_ERROR_INVALID_OUT_SIZE;
}
if (!voiceHandle) {
LOG_ERROR(Lib_Ngs2, "Invalid voice handle");
// On invalid handle, zero out the state (LLE behavior)
if (stateSize == sizeof(OrbisNgs2VoiceState)) {
outState->stateFlags = 0;
} else {
std::memset(outState, 0, sizeof(OrbisNgs2SamplerVoiceState));
}
return ORBIS_NGS2_ERROR_INVALID_VOICE_HANDLE;
}
auto* voice = reinterpret_cast<VoiceInternal*>(voiceHandle);
if (stateSize == sizeof(OrbisNgs2VoiceState)) {
outState->stateFlags = voice->stateFlags;
return ORBIS_OK;
}
auto* sampler_state = reinterpret_cast<OrbisNgs2SamplerVoiceState*>(outState);
sampler_state->voiceState.stateFlags = voice->stateFlags;
if (voice->isStreaming && voice->getReadyBufferCount() <= VoiceInternal::STARVATION_THRESHOLD) {
sampler_state->userData = 1;
} else {
sampler_state->userData = 0;
}
sampler_state->envelopeHeight = 1.0f;
sampler_state->peakHeight = 1.0f;
sampler_state->reserved = 0;
sampler_state->numDecodedSamples = voice->totalDecodedSamples;
u32 bytes_per_sample = 2 * (voice->format.numChannels > 0 ? voice->format.numChannels : 2);
sampler_state->decodedDataSize = sampler_state->numDecodedSamples * bytes_per_sample;
sampler_state->waveformData = voice->currentBufferPtr;
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNgs2VoiceGetStateFlags(OrbisNgs2Handle voiceHandle, u32* outStateFlags) {
LOG_ERROR(Lib_Ngs2, "called");
if (!outStateFlags) {
LOG_ERROR(Lib_Ngs2, "Invalid voice state address");
return ORBIS_NGS2_ERROR_INVALID_OUT_ADDRESS;
}
if (!voiceHandle) {
LOG_ERROR(Lib_Ngs2, "Invalid voice handle");
*outStateFlags = 0; // LLE behavior: zero out on invalid handle
return ORBIS_NGS2_ERROR_INVALID_VOICE_HANDLE;
}
auto* voice = reinterpret_cast<VoiceInternal*>(voiceHandle);
u32 flags = voice->stateFlags & 0xFF;
if (voice->isStreaming && voice->getReadyBufferCount() <= VoiceInternal::STARVATION_THRESHOLD) {
flags |= 0x80;
}
*outStateFlags = flags;
return ORBIS_OK;
}
@ -380,14 +1009,52 @@ s32 PS4_SYSV_ABI sceNgs2GeomApply(const OrbisNgs2GeomListenerWork* listener,
s32 PS4_SYSV_ABI sceNgs2PanInit(OrbisNgs2PanWork* work, const float* aSpeakerAngle, float unitAngle,
u32 numSpeakers) {
LOG_ERROR(Lib_Ngs2, "unitAngle = {}, numSpeakers = {}", unitAngle, numSpeakers);
LOG_DEBUG(Lib_Ngs2, "unitAngle = {}, numSpeakers = {}", unitAngle, numSpeakers);
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNgs2PanGetVolumeMatrix(OrbisNgs2PanWork* work, const OrbisNgs2PanParam* aParam,
u32 numParams, u32 matrixFormat,
float* outVolumeMatrix) {
LOG_ERROR(Lib_Ngs2, "numParams = {}, matrixFormat = {}", numParams, matrixFormat);
if (!outVolumeMatrix) {
return ORBIS_NGS2_ERROR_INVALID_OUT_ADDRESS;
}
// matrixFormat: 1 = mono, 2 = stereo, etc.
u32 num_output_channels = matrixFormat;
if (num_output_channels == 0)
num_output_channels = 2;
if (num_output_channels > 8)
num_output_channels = 8;
// Initialize volume matrix to identity/center pan
// For stereo output (format 2), we create a simple center pan
for (u32 p = 0; p < numParams; p++) {
float* matrix = outVolumeMatrix + p * num_output_channels;
if (aParam && numParams > 0) {
// Use the pan angle to compute left/right levels
float angle = aParam[p].angle;
// Simple stereo panning: angle 0 = center, -1 = left, +1 = right
float left_level = 0.5f * (1.0f - angle);
float right_level = 0.5f * (1.0f + angle);
if (num_output_channels >= 2) {
matrix[0] = left_level;
matrix[1] = right_level;
for (u32 ch = 2; ch < num_output_channels; ch++) {
matrix[ch] = 0.0f;
}
} else {
matrix[0] = 1.0f;
}
} else {
for (u32 ch = 0; ch < num_output_channels; ch++) {
matrix[ch] = 1.0f / num_output_channels;
}
}
}
return ORBIS_OK;
}
@ -395,7 +1062,7 @@ s32 PS4_SYSV_ABI sceNgs2PanGetVolumeMatrix(OrbisNgs2PanWork* work, const OrbisNg
s32 PS4_SYSV_ABI sceNgs2ReportRegisterHandler(u32 reportType, OrbisNgs2ReportHandler handler,
uintptr_t userData, OrbisNgs2Handle* outHandle) {
LOG_INFO(Lib_Ngs2, "reportType = {}, userData = {}", reportType, userData);
LOG_DEBUG(Lib_Ngs2, "reportType = {}, userData = {}", reportType, userData);
if (!handler) {
LOG_ERROR(Lib_Ngs2, "handler is nullptr");
return ORBIS_NGS2_ERROR_INVALID_REPORT_HANDLE;
@ -408,7 +1075,7 @@ s32 PS4_SYSV_ABI sceNgs2ReportUnregisterHandler(OrbisNgs2Handle reportHandle) {
LOG_ERROR(Lib_Ngs2, "reportHandle is nullptr");
return ORBIS_NGS2_ERROR_INVALID_REPORT_HANDLE;
}
LOG_INFO(Lib_Ngs2, "called");
LOG_DEBUG(Lib_Ngs2, "called");
return ORBIS_OK;
}
@ -588,4 +1255,4 @@ void RegisterLib(Core::Loader::SymbolsResolver* sym) {
LIB_FUNCTION("AbYvTOZ8Pts", "libSceNgs2", 1, "libSceNgs2", sceNgs2VoiceRunCommands);
};
} // namespace Libraries::Ngs2
} // namespace Libraries::Ngs2

View File

@ -3,7 +3,9 @@
#include "ngs2_error.h"
#include "ngs2_impl.h"
#include "ngs2_internal.h"
#include <algorithm>
#include "common/logging/log.h"
#include "core/libraries/error_codes.h"
#include "core/libraries/kernel/kernel.h"
@ -12,8 +14,8 @@ using namespace Libraries::Kernel;
namespace Libraries::Ngs2 {
s32 HandleReportInvalid(OrbisNgs2Handle handle, u32 handleType) {
switch (handleType) {
s32 HandleReportInvalid(OrbisNgs2Handle handle, u32 handle_type) {
switch (handle_type) {
case 1:
LOG_ERROR(Lib_Ngs2, "Invalid system handle {}", handle);
return ORBIS_NGS2_ERROR_INVALID_SYSTEM_HANDLE;
@ -36,33 +38,33 @@ void* MemoryClear(void* buffer, size_t size) {
return memset(buffer, 0, size);
}
s32 StackBufferClose(StackBuffer* stackBuffer, size_t* outTotalSize) {
if (outTotalSize) {
*outTotalSize = stackBuffer->usedSize + stackBuffer->alignment;
s32 StackBufferClose(StackBuffer* stack_buffer, size_t* out_total_size) {
if (out_total_size) {
*out_total_size = stack_buffer->usedSize + stack_buffer->alignment;
}
return ORBIS_OK;
}
s32 StackBufferOpen(StackBuffer* stackBuffer, void* bufferStart, size_t bufferSize,
void** outBuffer, u8 flags) {
stackBuffer->top = outBuffer;
stackBuffer->base = bufferStart;
stackBuffer->size = (size_t)bufferStart;
stackBuffer->currentOffset = (size_t)bufferStart;
stackBuffer->usedSize = 0;
stackBuffer->totalSize = bufferSize;
stackBuffer->alignment = 8; // this is a fixed value
stackBuffer->flags = flags;
s32 StackBufferOpen(StackBuffer* stack_buffer, void* buffer_start, size_t buffer_size,
void** out_buffer, u8 flags) {
stack_buffer->top = out_buffer;
stack_buffer->base = buffer_start;
stack_buffer->size = (size_t)buffer_start;
stack_buffer->currentOffset = (size_t)buffer_start;
stack_buffer->usedSize = 0;
stack_buffer->totalSize = buffer_size;
stack_buffer->alignment = 8; // this is a fixed value
stack_buffer->flags = flags;
if (outBuffer != NULL) {
*outBuffer = NULL;
if (out_buffer != NULL) {
*out_buffer = NULL;
}
return ORBIS_OK;
}
s32 SystemCleanup(OrbisNgs2Handle systemHandle, OrbisNgs2ContextBufferInfo* outInfo) {
if (!systemHandle) {
s32 SystemCleanup(OrbisNgs2Handle system_handle, OrbisNgs2ContextBufferInfo* out_info) {
if (!system_handle) {
return ORBIS_NGS2_ERROR_INVALID_HANDLE;
}
@ -71,50 +73,66 @@ s32 SystemCleanup(OrbisNgs2Handle systemHandle, OrbisNgs2ContextBufferInfo* outI
return ORBIS_OK;
}
s32 SystemSetupCore(StackBuffer* stackBuffer, const OrbisNgs2SystemOption* option,
SystemInternal* outSystem) {
u32 maxGrainSamples = 512;
u32 numGrainSamples = 256;
u32 sampleRate = 48000;
s32 SystemSetupCore(StackBuffer* stack_buffer, const OrbisNgs2SystemOption* option,
SystemInternal* out_system) {
u32 max_grain_samples = 512;
u32 num_grain_samples = 256;
u32 sample_rate = 48000;
if (option) {
sampleRate = option->sampleRate;
maxGrainSamples = option->maxGrainSamples;
numGrainSamples = option->numGrainSamples;
sample_rate = option->sampleRate;
max_grain_samples = option->maxGrainSamples;
num_grain_samples = option->numGrainSamples;
}
if (maxGrainSamples < 64 || maxGrainSamples > 1024 || (maxGrainSamples & 63) != 0) {
LOG_ERROR(Lib_Ngs2, "Invalid system option (maxGrainSamples={},x64)", maxGrainSamples);
if (max_grain_samples < 64 || max_grain_samples > 1024 || (max_grain_samples & 63) != 0) {
LOG_ERROR(Lib_Ngs2, "Invalid system option (maxGrainSamples={},x64)", max_grain_samples);
return ORBIS_NGS2_ERROR_INVALID_MAX_GRAIN_SAMPLES;
}
if (numGrainSamples < 64 || numGrainSamples > 1024 || (numGrainSamples & 63) != 0) {
LOG_ERROR(Lib_Ngs2, "Invalid system option (numGrainSamples={},x64)", numGrainSamples);
if (num_grain_samples < 64 || num_grain_samples > 1024 || (num_grain_samples & 63) != 0) {
LOG_ERROR(Lib_Ngs2, "Invalid system option (numGrainSamples={},x64)", num_grain_samples);
return ORBIS_NGS2_ERROR_INVALID_NUM_GRAIN_SAMPLES;
}
if (sampleRate != 11025 && sampleRate != 12000 && sampleRate != 22050 && sampleRate != 24000 &&
sampleRate != 44100 && sampleRate != 48000 && sampleRate != 88200 && sampleRate != 96000 &&
sampleRate != 176400 && sampleRate != 192000) {
LOG_ERROR(Lib_Ngs2, "Invalid system option(sampleRate={}:44.1/48kHz series)", sampleRate);
if (sample_rate != 11025 && sample_rate != 12000 && sample_rate != 22050 &&
sample_rate != 24000 && sample_rate != 44100 && sample_rate != 48000 &&
sample_rate != 88200 && sample_rate != 96000 && sample_rate != 176400 &&
sample_rate != 192000) {
LOG_ERROR(Lib_Ngs2, "Invalid system option(sampleRate={}:44.1/48kHz series)", sample_rate);
return ORBIS_NGS2_ERROR_INVALID_SAMPLE_RATE;
}
if (outSystem) {
// dummy handle
outSystem->systemHandle = 1;
if (out_system) {
// Initialize system
std::memset(out_system, 0, sizeof(SystemInternal));
out_system->systemHandle = reinterpret_cast<OrbisNgs2Handle>(out_system);
out_system->sampleRate = sample_rate;
out_system->currentSampleRate = sample_rate;
out_system->maxGrainSamples = static_cast<u16>(max_grain_samples);
out_system->minGrainSamples = 64;
out_system->numGrainSamples = static_cast<u16>(num_grain_samples);
out_system->currentNumGrainSamples = num_grain_samples;
out_system->renderCount = 0;
out_system->rackCount = 0;
out_system->isActive = 1;
if (option && option->name[0] != '\0') {
std::strncpy(out_system->name, option->name, ORBIS_NGS2_SYSTEM_NAME_LENGTH - 1);
out_system->name[ORBIS_NGS2_SYSTEM_NAME_LENGTH - 1] = '\0';
}
}
return ORBIS_OK;
}
s32 SystemSetup(const OrbisNgs2SystemOption* option, OrbisNgs2ContextBufferInfo* hostBufferInfo,
OrbisNgs2BufferFreeHandler hostFree, OrbisNgs2Handle* outHandle) {
u8 optionFlags = 0;
StackBuffer stackBuffer;
SystemInternal setupResult;
void* systemList = NULL;
size_t requiredBufferSize = 0;
s32 SystemSetup(const OrbisNgs2SystemOption* option, OrbisNgs2ContextBufferInfo* host_buffer_info,
OrbisNgs2BufferFreeHandler host_free, OrbisNgs2Handle* out_handle) {
u8 option_flags = 0;
StackBuffer stack_buffer;
SystemInternal setup_result;
void* system_list = NULL;
size_t required_buffer_size = 0;
u32 result = ORBIS_NGS2_ERROR_INVALID_BUFFER_SIZE;
if (option) {
@ -122,66 +140,173 @@ s32 SystemSetup(const OrbisNgs2SystemOption* option, OrbisNgs2ContextBufferInfo*
LOG_ERROR(Lib_Ngs2, "Invalid system option size ({})", option->size);
return ORBIS_NGS2_ERROR_INVALID_OPTION_SIZE;
}
optionFlags = option->flags >> 31;
option_flags = option->flags >> 31;
}
// Init
StackBufferOpen(&stackBuffer, NULL, 0, NULL, optionFlags);
result = SystemSetupCore(&stackBuffer, option, 0);
StackBufferOpen(&stack_buffer, NULL, 0, NULL, option_flags);
result = SystemSetupCore(&stack_buffer, option, 0);
if (result < 0) {
return result;
}
StackBufferClose(&stackBuffer, &requiredBufferSize);
StackBufferClose(&stack_buffer, &required_buffer_size);
// outHandle unprovided
if (!outHandle) {
hostBufferInfo->hostBuffer = NULL;
hostBufferInfo->hostBufferSize = requiredBufferSize;
MemoryClear(&hostBufferInfo->reserved, sizeof(hostBufferInfo->reserved));
// out_handle unprovided
if (!out_handle) {
host_buffer_info->hostBuffer = NULL;
host_buffer_info->hostBufferSize = required_buffer_size;
MemoryClear(&host_buffer_info->reserved, sizeof(host_buffer_info->reserved));
return ORBIS_OK;
}
if (!hostBufferInfo->hostBuffer) {
LOG_ERROR(Lib_Ngs2, "Invalid system buffer address ({})", hostBufferInfo->hostBuffer);
if (!host_buffer_info->hostBuffer) {
LOG_ERROR(Lib_Ngs2, "Invalid system buffer address ({})", host_buffer_info->hostBuffer);
return ORBIS_NGS2_ERROR_INVALID_BUFFER_ADDRESS;
}
if (hostBufferInfo->hostBufferSize < requiredBufferSize) {
if (host_buffer_info->hostBufferSize < required_buffer_size) {
LOG_ERROR(Lib_Ngs2, "Invalid system buffer size ({}<{}[byte])",
hostBufferInfo->hostBufferSize, requiredBufferSize);
host_buffer_info->hostBufferSize, required_buffer_size);
return ORBIS_NGS2_ERROR_INVALID_BUFFER_SIZE;
}
// Setup
StackBufferOpen(&stackBuffer, hostBufferInfo->hostBuffer, hostBufferInfo->hostBufferSize,
&systemList, optionFlags);
result = SystemSetupCore(&stackBuffer, option, &setupResult);
StackBufferOpen(&stack_buffer, host_buffer_info->hostBuffer, host_buffer_info->hostBufferSize,
&system_list, option_flags);
// Allocate SystemInternal from the buffer
auto* system = new SystemInternal();
result = SystemSetupCore(&stack_buffer, option, system);
if (result < 0) {
delete system;
return result;
}
StackBufferClose(&stackBuffer, &requiredBufferSize);
StackBufferClose(&stack_buffer, &required_buffer_size);
// Copy buffer results
setupResult.bufferInfo = *hostBufferInfo;
setupResult.hostFree = hostFree;
// TODO
// setupResult.systemList = systemList;
system->bufferInfo = *host_buffer_info;
system->hostFree = host_free;
system->systemHandle = reinterpret_cast<OrbisNgs2Handle>(system);
OrbisNgs2Handle systemHandle = setupResult.systemHandle;
if (hostBufferInfo->hostBufferSize >= requiredBufferSize) {
*outHandle = systemHandle;
if (host_buffer_info->hostBufferSize >= required_buffer_size) {
*out_handle = system->systemHandle;
return ORBIS_OK;
}
SystemCleanup(systemHandle, 0);
delete system;
LOG_ERROR(Lib_Ngs2, "Invalid system buffer size ({}<{}[byte])", hostBufferInfo->hostBufferSize,
requiredBufferSize);
LOG_ERROR(Lib_Ngs2, "Invalid system buffer size ({}<{}[byte])",
host_buffer_info->hostBufferSize, required_buffer_size);
return ORBIS_NGS2_ERROR_INVALID_BUFFER_SIZE;
}
u32 RackIdToIndex(u32 rack_id) {
switch (rack_id) {
case 0x1000:
return 0; // Sampler
case 0x3000:
return 1; // Mastering
case 0x2000:
return 2; // Submixer
case 0x2001:
return 3; // Submixer alt
case 0x4001:
return 4; // Reverb
case 0x4002:
return 5; // Equalizer
case 0x4003:
return 6; // Custom
default:
return 0xFF;
}
}
s32 RackCreate(SystemInternal* system, u32 rack_id, const OrbisNgs2RackOption* option,
const OrbisNgs2ContextBufferInfo* buffer_info, OrbisNgs2Handle* out_handle) {
if (!system) {
LOG_ERROR(Lib_Ngs2, "RackCreate: Invalid system handle");
return ORBIS_NGS2_ERROR_INVALID_SYSTEM_HANDLE;
}
u32 rack_index = RackIdToIndex(rack_id);
if (rack_index == 0xFF) {
LOG_ERROR(Lib_Ngs2, "Invalid rack ID: {:#x}", rack_id);
return ORBIS_NGS2_ERROR_INVALID_RACK_ID;
}
auto* rack = new RackInternal();
rack->ownerSystem = system;
rack->rackType = rack_index;
rack->rackId = rack_id;
rack->handle.systemData = system;
// Setup rack info with defaults or from option
rack->info.rackHandle = reinterpret_cast<OrbisNgs2Handle>(rack);
rack->info.ownerSystemHandle = system->systemHandle;
rack->info.type = rack_index;
rack->info.rackId = rack_id;
rack->info.minGrainSamples = 64;
rack->info.stateFlags = 0;
// Use option values if provided, otherwise use defaults
if (option && option->size >= sizeof(OrbisNgs2RackOption)) {
std::strncpy(rack->info.name, option->name, ORBIS_NGS2_RACK_NAME_LENGTH - 1);
rack->info.name[ORBIS_NGS2_RACK_NAME_LENGTH - 1] = '\0';
rack->info.maxVoices = option->maxVoices > 0 ? option->maxVoices : 1;
rack->info.maxGrainSamples = option->maxGrainSamples > 0 ? option->maxGrainSamples : 512;
} else {
// Default values when option is NULL - based on libSceNgs2.c analysis
rack->info.name[0] = '\0';
// Sampler rack (0x1000) defaults to 0x100 (256) voices, others default to 1
if (rack_id == 0x1000) {
rack->info.maxVoices = 256; // Sampler default
} else {
rack->info.maxVoices = 1;
}
rack->info.maxGrainSamples = 512;
}
// Allocate voices
u32 num_voices = rack->info.maxVoices;
rack->voices.reserve(num_voices);
for (u32 i = 0; i < num_voices; i++) {
auto voice = std::make_unique<VoiceInternal>();
voice->ownerRack = rack;
voice->voiceIndex = i;
voice->handle.systemData = system;
voice->stateFlags = 0; // Not playing
rack->voices.push_back(std::move(voice));
}
system->racks.push_back(rack);
system->rackCount++;
if (out_handle) {
*out_handle = reinterpret_cast<OrbisNgs2Handle>(rack);
}
return ORBIS_OK;
}
s32 RackDestroy(RackInternal* rack, OrbisNgs2ContextBufferInfo* out_buffer_info) {
if (!rack) {
return ORBIS_NGS2_ERROR_INVALID_RACK_HANDLE;
}
SystemInternal* system = rack->ownerSystem;
if (system) {
auto it = std::find(system->racks.begin(), system->racks.end(), rack);
if (it != system->racks.end()) {
system->racks.erase(it);
system->rackCount--;
}
}
delete rack;
return ORBIS_OK;
}
} // namespace Libraries::Ngs2

View File

@ -3,6 +3,9 @@
#pragma once
#include <atomic>
#include <vector>
#include "common/types.h"
#include "core/libraries/kernel/threads/pthread.h"
namespace Libraries::Ngs2 {
@ -92,6 +95,10 @@ struct StackBuffer {
char padding[7];
};
// Forward declarations for types used before definition
struct RackInternal;
struct VoiceInternal;
struct SystemInternal {
// setup init
char name[ORBIS_NGS2_SYSTEM_NAME_LENGTH]; // 0
@ -154,6 +161,9 @@ struct SystemInternal {
u32 rackCount; // 336
float lastRenderRatio; // 340
float cpuLoad; // 344
// Rack management
std::vector<RackInternal*> racks;
};
struct HandleInternal {
@ -164,16 +174,26 @@ struct HandleInternal {
u32 handleID; // 28
};
s32 StackBufferClose(StackBuffer* stackBuffer, size_t* outTotalSize);
s32 StackBufferOpen(StackBuffer* stackBuffer, void* buffer, size_t bufferSize, void** outBuffer,
s32 StackBufferClose(StackBuffer* stack_buffer, size_t* out_total_size);
s32 StackBufferOpen(StackBuffer* stack_buffer, void* buffer, size_t buffer_size, void** out_buffer,
u8 flags);
s32 SystemSetupCore(StackBuffer* stackBuffer, const OrbisNgs2SystemOption* option,
SystemInternal* outSystem);
s32 SystemSetupCore(StackBuffer* stack_buffer, const OrbisNgs2SystemOption* option,
SystemInternal* out_system);
s32 HandleReportInvalid(OrbisNgs2Handle handle, u32 handleType);
s32 HandleReportInvalid(OrbisNgs2Handle handle, u32 handle_type);
void* MemoryClear(void* buffer, size_t size);
s32 SystemCleanup(OrbisNgs2Handle systemHandle, OrbisNgs2ContextBufferInfo* outInfo);
s32 SystemSetup(const OrbisNgs2SystemOption* option, OrbisNgs2ContextBufferInfo* hostBufferInfo,
OrbisNgs2BufferFreeHandler hostFree, OrbisNgs2Handle* outHandle);
s32 SystemCleanup(OrbisNgs2Handle system_handle, OrbisNgs2ContextBufferInfo* out_info);
s32 SystemSetup(const OrbisNgs2SystemOption* option, OrbisNgs2ContextBufferInfo* host_buffer_info,
OrbisNgs2BufferFreeHandler host_free, OrbisNgs2Handle* out_handle);
// Forward declarations for internal types
struct RackInternal;
struct SystemInternal;
struct OrbisNgs2RackOption;
u32 RackIdToIndex(u32 rack_id);
s32 RackCreate(SystemInternal* system, u32 rack_id, const OrbisNgs2RackOption* option,
const OrbisNgs2ContextBufferInfo* buffer_info, OrbisNgs2Handle* out_handle);
s32 RackDestroy(RackInternal* rack, OrbisNgs2ContextBufferInfo* out_buffer_info);
} // namespace Libraries::Ngs2

View File

@ -0,0 +1,200 @@
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <deque>
#include <memory>
#include <vector>
#include "ngs2.h"
#include "ngs2_impl.h"
namespace Libraries::Ngs2 {
// Forward declarations
struct RackInternal;
struct VoiceInternal;
// Waveform block for streaming support
struct WaveformBlockInternal {
u32 dataOffset; // Offset into the base data pointer
u32 dataSize; // Size of this block in bytes
u32 numRepeats; // Number of times to repeat this block (0 = once)
u32 numSkipSamples; // Samples to skip at start
u32 numSamples; // Total samples in this block
u32 currentRepeat; // Current repeat iteration
WaveformBlockInternal()
: dataOffset(0), dataSize(0), numRepeats(0), numSkipSamples(0), numSamples(0),
currentRepeat(0) {}
};
// Ring buffer slot for streaming
struct RingBufferSlot {
const void* basePtr; // Original base pointer provided by game
const void* data; // Actual data pointer (with offset applied)
u32 dataSize;
u32 numSamples;
bool valid; // Whether this slot contains valid data
bool consumed; // Whether this slot has been fully consumed
RingBufferSlot()
: basePtr(nullptr), data(nullptr), dataSize(0), numSamples(0), valid(false),
consumed(false) {}
void set(const void* base, const void* d, u32 ds, u32 ns) {
basePtr = base;
data = d;
dataSize = ds;
numSamples = ns;
valid = true;
consumed = false;
}
void markConsumed() {
consumed = true;
}
void reset() {
basePtr = nullptr;
data = nullptr;
dataSize = 0;
numSamples = 0;
valid = false;
consumed = false;
}
};
struct VoiceInternal {
HandleInternal handle;
RackInternal* ownerRack;
u32 voiceIndex;
u32 stateFlags;
std::vector<OrbisNgs2VoicePortInfo> ports;
std::vector<OrbisNgs2VoiceMatrixInfo> matrices;
// Sampler-specific data
OrbisNgs2WaveformFormat format;
float pitchRatio;
float portVolume; // Volume level for the voice (0.0 to 1.0+)
bool isSetup;
// Playback position (in samples, not bytes)
u32 currentSamplePos; // Integer sample position
float samplePosFloat; // Floating point position for resampling
// Block-based streaming support
std::vector<WaveformBlockInternal> waveformBlocks;
u32 currentBlockIndex; // Which block we're reading from
u32 flags;
bool isStreaming;
// Ring buffer for streaming audio
// The game provides buffers in a circular fashion (A->B->C->A->B->C...)
// We track which slots have data and which have been consumed
// Game typically refills with 3 buffers at once, so ring must accommodate that
static constexpr u32 MAX_RING_SLOTS = 3; // Support game's 3-buffer refill pattern
static constexpr u32 STARVATION_THRESHOLD = 0; // Signal when down to 1 buffer (game adds 3)
RingBufferSlot ringBuffer[MAX_RING_SLOTS];
u32 ringWriteIndex; // Next slot to write to (from game)
u32 ringReadIndex; // Current slot being read (by renderer)
u32 ringBufferCount; // Number of valid buffers in ring
const void* lastConsumedBuffer = nullptr; // Last buffer we finished reading (for game to check)
u64 totalDecodedSamples; // Total samples decoded so far
const void* currentBufferPtr = nullptr; // Add this
VoiceInternal()
: ownerRack(nullptr), voiceIndex(0), stateFlags(0), pitchRatio(1.0f), portVolume(1.0f),
isSetup(false), currentSamplePos(0), samplePosFloat(0.0f), currentBlockIndex(0), flags(0),
isStreaming(false), ringWriteIndex(0), ringReadIndex(0), ringBufferCount(0),
lastConsumedBuffer(nullptr) {
handle.selfPtr = &handle;
handle.systemData = nullptr;
handle.refCount = 1;
handle.handleType = 3; // Voice
handle.handleID = 0;
std::memset(&format, 0, sizeof(format));
for (u32 i = 0; i < MAX_RING_SLOTS; i++) {
ringBuffer[i].reset();
}
}
// Get the current buffer being read
RingBufferSlot* getCurrentSlot() {
// We use the consumed flag to determine if the current read head is valid data
if (ringBuffer[ringReadIndex].valid && !ringBuffer[ringReadIndex].consumed) {
return &ringBuffer[ringReadIndex];
}
return nullptr;
}
// Advance to the next buffer in the ring
void advanceReadIndex() {
if (!ringBuffer[ringReadIndex].valid)
return;
ringBuffer[ringReadIndex].consumed = true;
lastConsumedBuffer = ringBuffer[ringReadIndex].basePtr;
ringReadIndex = (ringReadIndex + 1) % MAX_RING_SLOTS;
if (ringBufferCount > 0) {
ringBufferCount--;
}
}
// Add a buffer to the ring
bool addToRing(const void* basePtr, const void* data, u32 dataSize, u32 numSamples) {
if (ringBufferCount >= MAX_RING_SLOTS) {
return false;
}
RingBufferSlot* slot = &ringBuffer[ringWriteIndex];
if (slot->valid && !slot->consumed) {
return false;
}
slot->set(basePtr, data, dataSize, numSamples);
ringWriteIndex = (ringWriteIndex + 1) % MAX_RING_SLOTS;
ringBufferCount++;
return true;
}
// Reset the ring buffer state
void resetRing() {
for (u32 i = 0; i < MAX_RING_SLOTS; i++) {
ringBuffer[i].reset();
}
ringWriteIndex = 0;
ringReadIndex = 0;
ringBufferCount = 0;
lastConsumedBuffer = nullptr;
currentBufferPtr = nullptr;
totalDecodedSamples = 0;
}
// Get number of buffers ready to read
u32 getReadyBufferCount() const {
return ringBufferCount;
}
};
struct RackInternal {
HandleInternal handle;
OrbisNgs2RackInfo info;
SystemInternal* ownerSystem;
std::vector<std::unique_ptr<VoiceInternal>> voices;
u32 rackType;
u32 rackId;
RackInternal() : ownerSystem(nullptr), rackType(0), rackId(0) {
handle.selfPtr = &handle;
handle.systemData = nullptr;
handle.refCount = 1;
handle.handleType = 2; // Rack
handle.handleID = 0;
std::memset(&info, 0, sizeof(info));
}
};
} // namespace Libraries::Ngs2