mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-01-30 19:13:45 +00:00
Merge dd1451f725 into 4f11a8c979
This commit is contained in:
commit
cfac5d27b1
325
src/core/libraries/ngs2/README.md
Normal file
325
src/core/libraries/ngs2/README.md
Normal 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.
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
200
src/core/libraries/ngs2/ngs2_internal.h
Normal file
200
src/core/libraries/ngs2/ngs2_internal.h
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user