Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .agent/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ Record meaningful technical decisions here. Use one entry per decision.

## Entries

- Date: 2026-08-30
- Decision: Add a persisted ruler display switch in the piano-key/ruler intersection. Waveform mode retains the cached envelope; render-status mode cancels envelope work and draws each visible phrase as an outlined pending block or filled completed block.
- Rationale: Phrase blocks expose the same progressive render lifecycle at substantially lower CPU and allocation cost during frequent mobile piano-roll movement, while the explicit button and changing icon keep the performance choice discoverable.
- Alternatives considered: Always draw both layers; replace the waveform permanently; infer completion from nonzero samples; rebuild a retained geometry for simple rectangles.
- Impacted areas: Mobile piano-roll view model, ruler canvas and layout, OPUM-specific preferences, and shared view constants. Synthesis and render lifecycle are unchanged.

- Date: 2026-08-30
- Decision: Display the active voice part's available rendered phrases as a one-sided peak envelope in the piano-roll ruler placeholder. Publish the request's initially empty mix when rendering starts, publish each completed phrase's audio interval, clear immediately on invalidation, and rebuild only the visible overscanned range into cached `StreamGeometry`.
- Rationale: Upstream `UVoicePart.Mix` is the waveform source, but upstream keeps the in-progress `WaveMix` private and assigns it to the part only after every phrase completes. Exposing lifecycle notifications without changing synthesis makes the UI match actual sample availability while pooled visible-range envelope generation and retained geometry keep high-frequency panning inexpensive.
- Alternatives considered: Port upstream's per-frame full-view `WriteableBitmap` renderer; wait for `PartRenderedNotification`; infer completion from renderer-specific cache files; poll private render state; draw both waveform halves.
- Impacted areas: Core render lifecycle notifications and mix publication, Mobile piano-roll ruler rendering, and shared view constants. Renderer algorithms and rendered samples are unchanged.

- Date: 2026-08-29
- Decision: Introduce an asynchronous Mobile-layer external URL launcher that accepts only absolute HTTP/HTTPS URLs and is injected through `ServiceHub`; implement native launchers for Android, Windows, Linux, macOS, iOS, and browser WASM.
- Rationale: A typed service keeps MVVM callers independent of platform APIs, reports launch failure without exceptions reaching commands, and fits the existing ServiceHub host-injection pattern. Restricting schemes to web URLs prevents About-page links from unexpectedly opening mail, telephone, or custom-scheme handlers.
Expand Down
21 changes: 21 additions & 0 deletions OpenUtau.Core/Commands/Notifications.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using OpenUtau.Core.Render;
using OpenUtau.Core.Ustx;

namespace OpenUtau.Core {
Expand Down Expand Up @@ -240,6 +241,26 @@ public class PreRenderNotification : UNotification {
public override string ToString() => $"Pre-render notification.";
}

public class PartRenderInvalidatedNotification : UNotification {
public PartRenderInvalidatedNotification(UVoicePart part) {
this.part = part;
}
public override string ToString() => "Part render invalidated.";
}

public class PhraseRenderedNotification : UNotification {
public readonly RenderPhrase phrase;
public readonly double audioStartMs;
public readonly double audioEndMs;
public PhraseRenderedNotification(UVoicePart part, RenderPhrase phrase, double audioStartMs, double audioEndMs) {
this.part = part;
this.phrase = phrase;
this.audioStartMs = audioStartMs;
this.audioEndMs = audioEndMs;
}
public override string ToString() => "Phrase rendered.";
}

public class PartRenderedNotification : UNotification {
public PartRenderedNotification(UVoicePart part) {
this.part = part;
Expand Down
7 changes: 6 additions & 1 deletion OpenUtau.Core/Render/RenderEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ private void RenderRequests(
if (requests.Length == 0 || cancellation.IsCancellationRequested) {
return;
}
foreach (RenderPartRequest request in requests) {
request.part.SetMix(request.mix);
DocManager.Inst.ExecuteCmd(new PartRenderInvalidatedNotification(request.part));
}
var tuples = requests
.SelectMany(req => req.phrases
.Zip(req.sources, (phrase, source) => Tuple.Create(phrase, source, req)))
Expand All @@ -259,8 +263,9 @@ private void RenderRequests(
break;
}
source.SetSamples(task.Result.samples);
DocManager.Inst.ExecuteCmd(new PhraseRenderedNotification(
request.part, phrase, source.offsetMs, source.EndMs));
if (request.sources.All(s => s.HasSamples)) {
request.part.SetMix(request.mix);
DocManager.Inst.ExecuteCmd(new PartRenderedNotification(request.part));
}
}
Expand Down
5 changes: 5 additions & 0 deletions OpenUtau.Core/Util/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ public class SerializablePreferences {
#region OpenUtau Mobile 特定选项
public double PlaybackRefreshRate = 20.0;

/// <summary>
/// 钢琴卷帘标尺是否以轻量矩形块显示分片渲染状态,而不是绘制波形。
/// </summary>
public bool RenderedPhraseStatusMode = false;

/// <summary>
/// Piano key behavior: 0=Silent, 1=SineWave, 2=SoundFont
/// </summary>
Expand Down
Loading
Loading