Implement ADV History voice replay
This commit is contained in:
@@ -1290,7 +1290,8 @@ The write path is part of ordinary ADV execution:
|
||||
layout slot—not a T1 anchor id—although T1 entries structurally target these reset sites. Its ReadTextDB
|
||||
snapshot/commit work remains a second responsibility.
|
||||
- op `0x6e`'s glyph builder appends normal text chunks with the active geometry/font/color state.
|
||||
- voice op `0xc4` appends a `0x40000000` record containing its stored pair.
|
||||
- voice op `0xc4` appends a `0x40000000` record containing `{voice_id,0}`; History replay op `0x1bd`
|
||||
uses the same writer with `{voice_id,1}` when recording is enabled.
|
||||
- op `0x1d2` appends a `0x20000000` record with operand 1 as the value and operand 2 as the metadata type.
|
||||
It was previously misclassified as a safe statement marker; 17,323 corpus uses make this a foundational
|
||||
correction.
|
||||
@@ -1301,7 +1302,8 @@ The write path is part of ordinary ADV execution:
|
||||
|
||||
The read side is the previously identified op-`0x1d0..0x1d4` family. Op `0x1d0` returns a logical entry's
|
||||
layout slot and first record index; op `0x1d1` renders records until the next group boundary; op `0x1d3`
|
||||
finds typed metadata; op `0x1d4` finds the voice pair; op `0x1bd` replays its voice id.
|
||||
finds typed metadata; op `0x1d4` finds the voice pair. `HISTORY.BIN` dispatches pair variant 1 through op
|
||||
`0x1bd` and variant 0 through ordinary op `0xc4`.
|
||||
|
||||
History is independent of `RT.DAT`, but native full save fidelity does serialize the live backlog.
|
||||
`text_history_serialize@0x451d00` writes the index and packed records/strings after context/numbered-save
|
||||
@@ -1399,8 +1401,16 @@ surfaces `0xc0/0xc1`; that existing exit boundary now tells the host to discard
|
||||
The semantic text/index records remain untouched, so reopening History rebuilds fresh rows while exiting
|
||||
cannot leave the old labels above the resumed ADV page.
|
||||
|
||||
History's remaining work is stored voice replay through `0x1bd` and the `0xd3/0xd4/0xd5` smooth-scroll
|
||||
callback scheduler. None changes backlog ownership or requires choosing a save/profile backend.
|
||||
Stored voice replay now follows the native split. Ordinary op `0xc4` and History op `0x1bd` share the
|
||||
section-manifest resolver, Skip replacement queue, and Auto voice-pending state, but carry variants 0 and 1
|
||||
respectively through `IHost.PlayVoice`. Both append their pair to the backlog when recording is enabled;
|
||||
History's surrounding `0x1bb(0)` suppression prevents the replay from recording itself. Native
|
||||
`voice_play_indexed_asset@0x488330` stores that variant in the channel-12 sound-buffer state before starting
|
||||
playback. Its precise audible meaning remains unproven, so the Godot host retains it through its queue and
|
||||
timeline rather than inventing different playback behavior.
|
||||
|
||||
History's remaining work is the `0xd3/0xd4/0xd5` smooth-scroll callback scheduler and its small supporting
|
||||
gaps. None changes backlog ownership or requires choosing a save/profile backend.
|
||||
|
||||
The original dependency order was **Hide Window first** to establish reusable callback/coroutine input, then
|
||||
Read-message Skip, then History after both the input layer and message-completion seam exist. Hide Window is
|
||||
|
||||
@@ -138,14 +138,14 @@
|
||||
- **evidence:** Ghidra op 0xc2 handler 0x4204c0 sets run-state 0x200, arms the service timer, and calls bgm_fade_arm@0x464830; bgm_fade_tick@0x464960 interpolates current/target percent and applies volume, releasing at target 0. Native SC0000 trace at 0x7c1/0x126c shows target 0, duration 3000, 1% ticks at about 30 ms.
|
||||
|
||||
### 0xc4 `play-voice` (play-voice, argc 1)
|
||||
- **summary:** Play a voice clip by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). While all-message Skip is active, retain/replace the queued voice id instead of starting it; playback resumes from the latest queued id after Skip clears. Same resolver rule as set-texture (NOT play-bgm, which is direct-name BGM{id:03d}).
|
||||
- **summary:** Play a voice clip by id with native playback/history variant 0; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). While all-message Skip is active, retain/replace the queued voice request instead of starting it; playback resumes from the latest queued request after Skip clears. Same resolver rule as set-texture (NOT play-bgm, which is direct-name BGM{id:03d}).
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Off-by-one disproven structurally: manifest interleaves graphics/voice (files[35]=EV049AA, [36]=MAN999, [37]=EV052CA, [38]=SYL0001), so files[base+id] lands voices on OGGs while files[base+id-1] would land them on .AGF graphics (silent) -- and they play, so the offset is exactly 0. Lily's lines are correctly form-gated (G[0xa57/0xa58/0xa59]) and stay silent when no form flag is seeded -- not a bug. Ghidra /v2 op_0xc4_handler@0x420610: when run_state_flags bit 0x08000000 is clear it starts the voice immediately; while set it stores the latest id/zero arg at ctx+0x6dbf4/+0x6dbf8. adv_interpreter_tick starts and clears that deferred voice when Skip/read-skip input is no longer active.
|
||||
- **evidence:** By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Off-by-one disproven structurally: manifest interleaves graphics/voice (files[35]=EV049AA, [36]=MAN999, [37]=EV052CA, [38]=SYL0001), so files[base+id] lands voices on OGGs while files[base+id-1] would land them on .AGF graphics (silent) -- and they play, so the offset is exactly 0. Lily's lines are correctly form-gated (G[0xa57/0xa58/0xa59]) and stay silent when no form flag is seeded -- not a bug. Ghidra /v2 op_0xc4_handler@0x420610: when run_state_flags bit 0x08000000 is clear it calls voice_play_indexed_asset@0x488330 with variant 0 and records pair {id,0}; while Skip is active it stores the latest id/zero variant at ctx+0x6dbf4/+0x6dbf8. adv_interpreter_tick starts and clears that deferred voice when Skip/read-skip input is no longer active.
|
||||
|
||||
### 0x1bd `play-history-voice` (u0041D910, argc 1)
|
||||
- **summary:** (voice_id) - replay a voice id selected from the retained ADV text history, preserving normal Skip and Auto-voice state behavior.
|
||||
- **summary:** Replay a voice id selected from retained ADV text history using native playback/history variant 1, preserving normal Skip and Auto-voice state behavior.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x1bd_play_history_voice@0x420920 stops/replaces the active voice, starts operand 1 through the native voice service when Skip is inactive (or queues it while Skip is active), records the replay in the message voice state when enabled, and sets adv_auto_voice_pending when playback exists. HISTORY.BIN obtains the id from retained text-record metadata before invoking this opcode.
|
||||
- **evidence:** Ghidra /v2: op_0x1bd_play_history_voice@0x420920 stops/replaces the active voice and calls voice_play_indexed_asset@0x488330 with operand 1 plus playback variant 1 when Skip is inactive (or queues that pair while Skip is active). When text-history recording is enabled it appends voice pair {id,1}; HISTORY.BIN suppresses recording with op 0x1bb while its UI is active, preventing replay clicks from recursively entering the backlog. The handler also sets adv_auto_voice_pending when playback exists. voice_play_indexed_asset retains the variant in sound-buffer state before starting channel 12; its exact audible meaning remains unproven.
|
||||
|
||||
## compute
|
||||
|
||||
|
||||
@@ -1811,3 +1811,27 @@ Validation: engine 198/198, zero-warning Godot build, threaded `SELFTEST OK`, an
|
||||
|
||||
**Next:** manually recheck the five History rows and overlay cleanup against the original. If they match, proceed
|
||||
with stored History voice replay (`0x1bd`), followed by `0xd3/0xd4/0xd5` smooth-scroll fidelity.
|
||||
|
||||
### ADV History stored voice replay implemented (2026-07-19)
|
||||
|
||||
The manual row/cleanup recheck passed, and stored History voice replay now uses the existing voice resolver,
|
||||
message-Skip replacement queue, and Auto voice-pending service. Native RE exposed a meaningful second value
|
||||
that the earlier one-id host API erased: ordinary `0xc4` calls the indexed voice service and records
|
||||
`{voice_id,0}`, while `0x1bd` uses and records `{voice_id,1}`. `IHost.PlayVoice` now carries this playback
|
||||
variant explicitly; Godot preserves it in queued requests and diagnostics while continuing to play the same
|
||||
resolved OGG until the native variant's audible interpretation is proven.
|
||||
|
||||
The generic opcode still appends `{voice_id,1}` when history capture is enabled, matching the native handler.
|
||||
During the real History menu, the existing `0x1bb(0)` suppression boundary prevents that replay from becoming
|
||||
a new backlog record. Focused tests cover the variant, Auto state, normal recording, and suppressed replay.
|
||||
HISTORY is now 70/78 distinct opcodes and 842/854 instructions handled or safe-noop; its eight remaining
|
||||
effectful gaps total 12 instructions.
|
||||
|
||||
Validation: 199 non-DirectShow engine cases pass, the isolated DirectShow movie case passes, opcode tests and
|
||||
lint are clean, vm0 RECOVER passes, the Godot build has zero warnings, and threaded `SELFTEST OK`. The full
|
||||
suite's DirectShow case still times out only when run among the complete suite; this pre-existing multimedia
|
||||
test interaction is unrelated to the History voice path. The `/v2` Ghidra image names/comments the shared
|
||||
indexed voice service and is saved.
|
||||
|
||||
**Next:** investigate and implement the `0xd3/0xd4/0xd5` smooth-scroll callback/interpolation family as a
|
||||
separate fidelity slice, including the remaining scheduler support it depends upon.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
@@ -10,6 +11,18 @@ public class HistoryDataOpsTests
|
||||
private static Operand G(int address) => new(T_GINT, address);
|
||||
private static Operand L(int address) => new(T_LINT, address);
|
||||
|
||||
private sealed class HistoryVoiceHost : RecordingHost
|
||||
{
|
||||
public AdvAutoWaitState AutoState;
|
||||
|
||||
public override void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback,
|
||||
Func<AdvAutoWaitState> autoWaitState)
|
||||
{
|
||||
AutoState = autoWaitState();
|
||||
Waits++;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyInlineIntArrayWritesConsecutiveSignedDwords()
|
||||
{
|
||||
@@ -161,4 +174,49 @@ public class HistoryDataOpsTests
|
||||
Assert.False(history.TryFindVoicePair(0, out long voiceId, out long voiceArgument));
|
||||
Assert.Equal((-1L, -1L), (voiceId, voiceArgument));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayHistoryVoiceUsesNativeVariantAndAutoServices()
|
||||
{
|
||||
var history = new AdvTextHistory();
|
||||
var script = ScriptAssembler.Assemble(Table, "HISTORY_VOICE_REPLAY",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x1bd, new[] { I(77) }),
|
||||
(0x72, new[] { I(1) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new HistoryVoiceHost();
|
||||
var vm = new VirtualMachine(script, Table, host, textHistory: history);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
Assert.Equal(new long[] { 77 }, host.Voices);
|
||||
Assert.Equal(new[] { (77L, 1) }, host.VoiceRequests);
|
||||
Assert.True(host.AutoState.VoicePending);
|
||||
var voice = Assert.Single(history.Records);
|
||||
Assert.Equal(AdvTextHistoryRecordKind.Voice, voice.Kind);
|
||||
Assert.Equal((77L, 1L), (voice.Value, voice.AuxValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SuppressedHistoryReplayDoesNotRecordItself()
|
||||
{
|
||||
var history = new AdvTextHistory();
|
||||
history.SetRecordingEnabled(false);
|
||||
var script = ScriptAssembler.Assemble(Table, "HISTORY_VOICE_REPLAY_SUPPRESSED",
|
||||
new List<(int, Operand[])>
|
||||
{
|
||||
(0x1bd, new[] { I(88) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(script, Table, host, textHistory: history);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(new[] { (88L, 1) }, host.VoiceRequests);
|
||||
Assert.Empty(history.Records);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ internal class RecordingHost : IHost
|
||||
public readonly List<AdvWaitIndicatorConfig> WaitIndicators = new();
|
||||
public readonly List<long> SleptDurations = new();
|
||||
public readonly List<(long Resource, int Channel)> SfxLoads = new();
|
||||
public readonly List<long> Voices = new();
|
||||
public readonly List<(long Id, int PlaybackVariant)> VoiceRequests = new();
|
||||
public readonly List<int> SfxStarts = new();
|
||||
public readonly List<int> SfxReleases = new();
|
||||
public readonly List<(int Target, long Duration)> BgmFades = new();
|
||||
@@ -101,7 +103,12 @@ internal class RecordingHost : IHost
|
||||
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
|
||||
public void PlayBgm(long id) { }
|
||||
public void PlayVoice(long id) { }
|
||||
public void PlayVoice(long id) => Voices.Add(id);
|
||||
public void PlayVoice(long id, int playbackVariant)
|
||||
{
|
||||
Voices.Add(id);
|
||||
VoiceRequests.Add((id, playbackVariant));
|
||||
}
|
||||
public void LoadSoundEffect(long resourceId, int channel) => SfxLoads.Add((resourceId, channel));
|
||||
public void StartSoundEffect(int channel) => SfxStarts.Add(channel);
|
||||
public void ReleaseSoundEffect(int channel) => SfxReleases.Add(channel);
|
||||
|
||||
@@ -72,6 +72,9 @@ public interface IHost
|
||||
(int Width, int Height) GetTextureSize(int slot);
|
||||
void PlayBgm(long id);
|
||||
void PlayVoice(long id);
|
||||
// Native voice playback retains a second start argument: ordinary dialogue passes 0,
|
||||
// while History replay (0x1bd) passes 1. Existing non-audio hosts may ignore it.
|
||||
void PlayVoice(long id, int playbackVariant) => PlayVoice(id);
|
||||
void LoadSoundEffect(long resourceId, int channel) { }
|
||||
void StartSoundEffect(int channel) { }
|
||||
void ReleaseSoundEffect(int channel) { }
|
||||
|
||||
@@ -882,7 +882,12 @@ public sealed class VirtualMachine
|
||||
case "play-voice":
|
||||
_autoVoicePending = true;
|
||||
TextHistory.AppendVoice(Read(a[0]), 0, _advTextStyle);
|
||||
_host.PlayVoice(Read(a[0])); return pc + 1;
|
||||
_host.PlayVoice(Read(a[0]), 0); return pc + 1;
|
||||
case "play-history-voice": // 0x1bd: native voice start/history argument is one
|
||||
case "u0041D910":
|
||||
_autoVoicePending = true;
|
||||
TextHistory.AppendVoice(Read(a[0]), 1, _advTextStyle);
|
||||
_host.PlayVoice(Read(a[0]), 1); return pc + 1;
|
||||
case "play-sound-effect": // 0xb4 / semantics: sfx-load
|
||||
_host.LoadSoundEffect(Read(a[0]), (int)Read(a[1])); return pc + 1;
|
||||
case "u0041D050": // 0xb5 / semantics: sfx-start
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class GodotAdvHost : IHost
|
||||
private bool _advTextForceComplete;
|
||||
private readonly Dictionary<int, AdvWaitIndicatorConfig> _waitIndicators = new();
|
||||
private volatile bool _messageSkipActive;
|
||||
private AudioPayload? _queuedSkippedVoice;
|
||||
private (AudioPayload Audio, int PlaybackVariant)? _queuedSkippedVoice;
|
||||
private int _activeWaitLayout;
|
||||
private long _waitIndicatorStartedMs;
|
||||
private volatile bool _advPagePresentationSuspended;
|
||||
@@ -312,7 +312,7 @@ public sealed class GodotAdvHost : IHost
|
||||
|
||||
var queued = _queuedSkippedVoice;
|
||||
_queuedSkippedVoice = null;
|
||||
if (queued != null) DispatchVoice(queued);
|
||||
if (queued != null) DispatchVoice(queued.Value.Audio, queued.Value.PlaybackVariant);
|
||||
}
|
||||
|
||||
public void WakeInputCallbackService() => _inputCallbackSignal.Set();
|
||||
@@ -601,24 +601,30 @@ public sealed class GodotAdvHost : IHost
|
||||
if (audio != null) _main.CallDeferred("PlayBgm", audio.Bytes, audio.Name);
|
||||
}
|
||||
|
||||
public void PlayVoice(long id)
|
||||
public void PlayVoice(long id) => PlayVoice(id, 0);
|
||||
|
||||
public void PlayVoice(long id, int playbackVariant)
|
||||
{
|
||||
var asset = _res.Resolve(_scene, id);
|
||||
var audio = asset != null ? LoadAudio(asset) : null;
|
||||
_timeline?.Event("voice", new() { ["id"] = id, ["file"] = audio?.Name,
|
||||
["playback_variant"] = playbackVariant });
|
||||
if (audio == null) return;
|
||||
if (_messageSkipActive)
|
||||
{
|
||||
bool firstQueued = _queuedSkippedVoice == null;
|
||||
_queuedSkippedVoice = audio;
|
||||
_queuedSkippedVoice = (audio, playbackVariant);
|
||||
if (firstQueued) _main.CallDeferred("StopVoiceForMessageSkip");
|
||||
return;
|
||||
}
|
||||
DispatchVoice(audio);
|
||||
DispatchVoice(audio, playbackVariant);
|
||||
}
|
||||
|
||||
private void DispatchVoice(AudioPayload audio)
|
||||
private void DispatchVoice(AudioPayload audio, int playbackVariant)
|
||||
{
|
||||
int generation = _main.QueueVoicePlayback();
|
||||
// Godot's stream player has no matching AGE start-mode control. Retain the native
|
||||
// variant through dispatch/timeline so that distinction is not erased at the VM seam.
|
||||
_main.CallDeferred("PlayVoice", audio.Bytes, audio.Name, generation);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ INFERRED: dict[int, dict] = {
|
||||
0x1b8: dict(name='get-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(out) - read an Auto-message delay from engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'),
|
||||
0x1b9: dict(name='set-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(milliseconds) - write an Auto-message delay to engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'),
|
||||
0x1bc: dict(name='reset-message-voice-state', category='input', noop=False, confidence='high', source='investigation', summary='Reset the per-message queued-voice flag used by ADV Auto timing.'),
|
||||
0x1bd: dict(name='play-history-voice', category='audio', noop=False, confidence='high', source='investigation', summary='(voice_id) - replay a voice id selected from the retained ADV text history, preserving normal Skip and Auto-voice state behavior.'),
|
||||
0x1bd: dict(name='play-history-voice', category='audio', noop=False, confidence='high', source='investigation', summary='Replay a voice id selected from retained ADV text history using native playback/history variant 1, preserving normal Skip and Auto-voice state behavior.'),
|
||||
0x1bf: dict(name='call-end', category='marker', noop=True, confidence='med', source='inference', summary='zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker'),
|
||||
0x1ca: dict(name='set-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - set the engine setting `message:ReadTextSkip`, which skips only previously read text.'),
|
||||
0x1cb: dict(name='get-read-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - read the engine setting `message:ReadTextSkip`.'),
|
||||
|
||||
@@ -1946,12 +1946,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "play-voice"
|
||||
category = "audio"
|
||||
summary = "Play a voice clip by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). While all-message Skip is active, retain/replace the queued voice id instead of starting it; playback resumes from the latest queued id after Skip clears. Same resolver rule as set-texture (NOT play-bgm, which is direct-name BGM{id:03d})."
|
||||
summary = "Play a voice clip by id with native playback/history variant 0; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). While all-message Skip is active, retain/replace the queued voice request instead of starting it; playback resumes from the latest queued request after Skip clears. Same resolver rule as set-texture (NOT play-bgm, which is direct-name BGM{id:03d})."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Off-by-one disproven structurally: manifest interleaves graphics/voice (files[35]=EV049AA, [36]=MAN999, [37]=EV052CA, [38]=SYL0001), so files[base+id] lands voices on OGGs while files[base+id-1] would land them on .AGF graphics (silent) -- and they play, so the offset is exactly 0. Lily's lines are correctly form-gated (G[0xa57/0xa58/0xa59]) and stay silent when no form flag is seeded -- not a bug. Ghidra /v2 op_0xc4_handler@0x420610: when run_state_flags bit 0x08000000 is clear it starts the voice immediately; while set it stores the latest id/zero arg at ctx+0x6dbf4/+0x6dbf8. adv_interpreter_tick starts and clears that deferred voice when Skip/read-skip input is no longer active."
|
||||
evidence = "By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Off-by-one disproven structurally: manifest interleaves graphics/voice (files[35]=EV049AA, [36]=MAN999, [37]=EV052CA, [38]=SYL0001), so files[base+id] lands voices on OGGs while files[base+id-1] would land them on .AGF graphics (silent) -- and they play, so the offset is exactly 0. Lily's lines are correctly form-gated (G[0xa57/0xa58/0xa59]) and stay silent when no form flag is seeded -- not a bug. Ghidra /v2 op_0xc4_handler@0x420610: when run_state_flags bit 0x08000000 is clear it calls voice_play_indexed_asset@0x488330 with variant 0 and records pair {id,0}; while Skip is active it stores the latest id/zero variant at ctx+0x6dbf4/+0x6dbf8. adv_interpreter_tick starts and clears that deferred voice when Skip/read-skip input is no longer active."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
@@ -4057,12 +4057,12 @@ abi_source = "kelebek+decode-validated"
|
||||
[opcode.semantics]
|
||||
name = "play-history-voice"
|
||||
category = "audio"
|
||||
summary = "(voice_id) - replay a voice id selected from the retained ADV text history, preserving normal Skip and Auto-voice state behavior."
|
||||
summary = "Replay a voice id selected from retained ADV text history using native playback/history variant 1, preserving normal Skip and Auto-voice state behavior."
|
||||
noop_headless = false
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = "Ghidra /v2: op_0x1bd_play_history_voice@0x420920 stops/replaces the active voice, starts operand 1 through the native voice service when Skip is inactive (or queues it while Skip is active), records the replay in the message voice state when enabled, and sets adv_auto_voice_pending when playback exists. HISTORY.BIN obtains the id from retained text-record metadata before invoking this opcode."
|
||||
evidence = "Ghidra /v2: op_0x1bd_play_history_voice@0x420920 stops/replaces the active voice and calls voice_play_indexed_asset@0x488330 with operand 1 plus playback variant 1 when Skip is inactive (or queues that pair while Skip is active). When text-history recording is enabled it appends voice pair {id,1}; HISTORY.BIN suppresses recording with op 0x1bb while its UI is active, preventing replay clicks from recursively entering the backlog. The handler also sets adv_auto_voice_pending when playback exists. voice_play_indexed_asset retains the variant in sound-buffer state before starting channel 12; its exact audible meaning remains unproven."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
|
||||
Reference in New Issue
Block a user