Skip to content
Open
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
23 changes: 17 additions & 6 deletions OpenUtau.Core/Classic/ClassicRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@

namespace OpenUtau.Classic {
public class ClassicRenderer : IRenderer {
readonly IResampler? resamplerOverride;
readonly string rendererName;

public ClassicRenderer() : this(null, Renderers.CLASSIC) { }

public ClassicRenderer(IResampler? resampler, string rendererName) {
resamplerOverride = resampler;
this.rendererName = rendererName;
}
static readonly HashSet<string> supportedExp = new HashSet<string>(){
Ustx.DYN,
Ustx.PITD,
Expand Down Expand Up @@ -49,8 +58,10 @@ public RenderResult Layout(RenderPhrase phrase) {
};
}

public Task<RenderResult> Render(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender, RenderPhraseEvents? renderEvents = null) {
if (phrase.wavtool == SharpWavtool.nameConvergence || phrase.wavtool == SharpWavtool.nameSimple) {
public Task<RenderResult> Render(RenderPhrase phrase, Progress progress, int trackNo,
CancellationTokenSource cancellation, bool isPreRender,
RenderPhraseEvents? renderEvents = null) {
if (resamplerOverride != null || phrase.wavtool == SharpWavtool.nameConvergence || phrase.wavtool == SharpWavtool.nameSimple) {
return RenderInternal(phrase, progress, trackNo, cancellation, isPreRender);
} else {
return RenderExternal(phrase, progress, trackNo, cancellation, isPreRender);
Expand All @@ -60,7 +71,7 @@ public Task<RenderResult> Render(RenderPhrase phrase, Progress progress, int tra
public Task<RenderResult> RenderInternal(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender) {
var resamplerItems = new List<ResamplerItem>();
foreach (var phone in phrase.phones) {
resamplerItems.Add(new ResamplerItem(phrase, phone));
resamplerItems.Add(new ResamplerItem(phrase, phone, resamplerOverride));
}
var task = Task.Run(() => {
Parallel.ForEach(source: resamplerItems, parallelOptions: new ParallelOptions() {
Expand Down Expand Up @@ -99,7 +110,7 @@ public Task<RenderResult> RenderInternal(RenderPhrase phrase, Progress progress,
public Task<RenderResult> RenderExternal(RenderPhrase phrase, Progress progress, int trackNo, CancellationTokenSource cancellation, bool isPreRender) {
var resamplerItems = new List<ResamplerItem>();
foreach (var phone in phrase.phones) {
resamplerItems.Add(new ResamplerItem(phrase, phone));
resamplerItems.Add(new ResamplerItem(phrase, phone, resamplerOverride));
}
var task = Task.Run(() => {
string progressInfo = $"Track {trackNo + 1} : {phrase.wavtool} \"{string.Join(" ", phrase.phones.Select(p => p.phoneme))}\"";
Expand Down Expand Up @@ -140,13 +151,13 @@ public RenderPitchResult LoadRenderedPitch(RenderPhrase phrase) {
}

public UExpressionDescriptor[] GetSuggestedExpressions(USinger singer, URenderSettings renderSettings) {
var manifest= renderSettings.Resampler.Manifest;
var manifest = resamplerOverride?.Manifest ?? renderSettings.Resampler?.Manifest;
if (manifest == null) {
return new UExpressionDescriptor[] { };
}
return manifest.expressions.Values.ToArray();
}

public override string ToString() => Renderers.CLASSIC;
public override string ToString() => rendererName;
}
}
2 changes: 1 addition & 1 deletion OpenUtau.Core/Classic/ClassicSinger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ public override void Save() {
otoWatcher.Paused = false;
}
}

public void Dispose() {
otoWatcher?.Dispose();
otoWatcher = null;
Expand Down
2 changes: 1 addition & 1 deletion OpenUtau.Core/Classic/ExeResampler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
using Serilog;

namespace OpenUtau.Classic {
internal class ExeResampler : IResampler {
public class ExeResampler : IResampler {
public string Name { get; private set; }
public string FilePath { get; private set; }
public bool isLegalPlugin => _isLegalPlugin;
Expand Down
4 changes: 2 additions & 2 deletions OpenUtau.Core/Classic/ResamplerItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ public class ResamplerItem {

public ulong hash;

public ResamplerItem(RenderPhrase phrase, RenderPhone phone) {
public ResamplerItem(RenderPhrase phrase, RenderPhone phone, IResampler? resamplerOverride = null) {
this.phrase = phrase;
this.phone = phone;

resampler = ToolsManager.Inst.GetResampler(phone.resampler);
resampler = resamplerOverride ?? ToolsManager.Inst.GetResampler(phone.resampler);
inputFile = phone.oto.File;
inputTemp = VoicebankFiles.Inst.GetSourceTempPath(phrase.singer.Id, phone.oto, ".wav");
tone = phone.tone;
Expand Down
39 changes: 39 additions & 0 deletions OpenUtau.Core/Classic/ResamplerManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ namespace OpenUtau.Classic {
public class ResamplerManifest {
public Dictionary<string, UExpressionDescriptor> expressions = new Dictionary<string, UExpressionDescriptor> { };
public bool expressionFilter = false;
public RendererManifest renderer;
public AnalysisManifest analysis;

public ResamplerManifest() { }

public static ResamplerManifest Load(string path) {
var manifest = Yaml.DefaultDeserializer.Deserialize<ResamplerManifest>(
File.ReadAllText(path, encoding: Encoding.UTF8)
);
manifest.expressions ??= new Dictionary<string, UExpressionDescriptor>();
manifest.expressions = manifest.expressions
.GroupBy(kvp => kvp.Key.ToLower())
.ToDictionary(
Expand All @@ -25,4 +28,40 @@ public static ResamplerManifest Load(string path) {
return manifest;
}
}

public class RendererManifest {
public bool enabled = false;
public string id;
public string name;
public RendererBridgeManifest bridge;
public RendererCapabilitiesManifest capabilities;
}

public class RendererBridgeManifest {
public string assembly;
public string type;
public int apiVersion = 1;
}

public class RendererCapabilitiesManifest {
// These must match the corresponding runtime IRenderer properties.
public bool renderedPitch = false;
public bool realCurves = false;
// True means Render cooperatively observes its CancellationTokenSource.
public bool cancellation = false;
// Maximum simultaneous Render calls. Zero leaves scheduling to the host.
public int parallelism = 0;
}

public class AnalysisManifest {
public Dictionary<string, AnalysisFormatManifest> formats = new Dictionary<string, AnalysisFormatManifest>();
}

public class AnalysisFormatManifest {
public string name;
public string path;
public bool required = false;
public bool canGenerate = false;
public bool shared = false;
}
}
2 changes: 2 additions & 0 deletions OpenUtau.Core/Classic/ToolsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using OpenUtau.Core;
using OpenUtau.Core.Util;
using OpenUtau.Core.Render;
using Serilog;

namespace OpenUtau.Classic {
Expand Down Expand Up @@ -90,6 +91,7 @@ public void SearchResamplers() {
foreach (var resampler in resamplers) {
resamplersMap[resampler.ToString()] = resampler;
}
ExternalRendererRegistry.Discover(basePath);
}

public void SearchWavtools() {
Expand Down
18 changes: 14 additions & 4 deletions OpenUtau.Core/Commands/TrackCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,22 @@ public TrackChangeRenderSettingCommand(UProject project, UTrack track, URenderSe
}
public override string ToString() { return "Change render setting"; }
public override void Execute() {
track.RendererSettings = newSettings.Clone();
track.RendererSettings.Validate(track);
var settings = newSettings.Clone();
settings.Validate(track, fallbackUnavailableRenderer: false);
ReplaceSettings(settings);
}
public override void Unexecute() {
track.RendererSettings = oldSettings.Clone();
track.RendererSettings.Validate(track);
var settings = oldSettings.Clone();
settings.Validate(track);
ReplaceSettings(settings);
}
void ReplaceSettings(URenderSettings settings) {
if (!ReferenceEquals(track.RendererSettings.Renderer, settings.Renderer) &&
track.RendererSettings.Renderer is IDisposable disposable) {
disposable.Dispose();
}
track.RendererSettings = settings;
track.RegisterRendererExpressions(project);
}
}
}
3 changes: 3 additions & 0 deletions OpenUtau.Core/Pipeline/PhraseSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ public sealed class PhraseSource {
public readonly IRenderer Renderer;
public readonly string Resampler;
public readonly string Wavtool;
public readonly IReadOnlyDictionary<string, string> RendererSettings;
public readonly ClassicSinger ClassicSinger;
public readonly bool ModpSupported;

Expand All @@ -348,6 +349,8 @@ internal PhraseSource(
Renderer = track.RendererSettings.Renderer;
Resampler = track.RendererSettings.resampler;
Wavtool = track.RendererSettings.wavtool;
RendererSettings = new Dictionary<string, string>(
track.RendererSettings.rendererSettings ?? new Dictionary<string, string>());
ClassicSinger = Singer as ClassicSinger;
ModpSupported = track.TryGetExpDescriptor(project, Format.Ustx.MODP, out var modp)
&& Renderer.SupportsExpression(modp);
Expand Down
184 changes: 184 additions & 0 deletions OpenUtau.Core/Render/ExternalRendererPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenUtau.Classic;
using OpenUtau.Core.Ustx;
using OpenUtau.Core.Util;
using Serilog;

namespace OpenUtau.Core.Render {
public interface IExternalRendererIdentity {
string Id { get; }
string Name { get; }
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ExternalRendererAttribute : Attribute {
public string Id { get; }
public string Name { get; }
public USingerType SingerType { get; }

public ExternalRendererAttribute(string id, string name, USingerType singerType = USingerType.Classic) {
Id = id;
Name = name;
SingerType = singerType;
}
}

public sealed class RendererPluginMetadata {
public RendererCapabilitiesManifest Capabilities { get; init; } = new RendererCapabilitiesManifest();
public IReadOnlyDictionary<string, AnalysisFormatManifest> AnalysisFormats { get; init; }
= new Dictionary<string, AnalysisFormatManifest>();
public IReadOnlyDictionary<string, UExpressionDescriptor> Expressions { get; init; }
= new Dictionary<string, UExpressionDescriptor>();
public IReadOnlyDictionary<string, RendererSettingDescriptor> Settings { get; init; }
= new Dictionary<string, RendererSettingDescriptor>();
}

public enum RendererSettingType { Integer, Number, Boolean, Text, Choice }

public sealed class RendererSettingDescriptor {
public string Name { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public RendererSettingType Type { get; init; } = RendererSettingType.Text;
public string DefaultValue { get; init; } = string.Empty;
public double? Min { get; init; }
public double? Max { get; init; }
public double? Step { get; init; }
public IReadOnlyList<string> Choices { get; init; } = Array.Empty<string>();
}

/// <summary>
/// Versioned entry point implemented by external renderer bridge assemblies.
/// </summary>
public interface IOpenUtauRendererPlugin {
int ApiVersion { get; }
RendererPluginMetadata Metadata => new RendererPluginMetadata();
IRenderer CreateRenderer(RendererPluginContext context);
IRendererAnalysisProvider? CreateAnalysisProvider(RendererPluginContext context) => null;
}

public enum RendererAnalysisState {
Valid,
Missing,
Stale,
Invalid,
}

public sealed record RendererAnalysisRequest(
string Format, string SourceFile, string OutputFile, bool Overwrite);

public enum RendererAnalysisOutcome {
Generated,
AlreadyValid,
Failed,
}

public sealed record RendererAnalysisResult(
RendererAnalysisRequest Request,
RendererAnalysisOutcome Outcome,
string Message = null);

/// <summary>Owns engine-specific validation and generation of reusable source
/// analysis. The host handles paths, fallback timestamp checks and orchestration.</summary>
public interface IRendererAnalysisProvider {
Task<IReadOnlyList<RendererAnalysisResult>> GenerateAsync(
IReadOnlyList<RendererAnalysisRequest> requests,
IProgress<int> progress,
CancellationToken cancellation);
ValueTask<RendererAnalysisState> ValidateAsync(
RendererAnalysisRequest request,
CancellationToken cancellation);
}

public sealed class RendererPluginContext {
public int ApiVersion => ExternalRendererRegistry.ApiVersion;
public Version HostVersion => typeof(IRenderer).Assembly.GetName().Version ?? new Version();
public string RendererId { get; }
public string RendererName { get; }
public string PluginDirectory { get; }
public string ManifestPath { get; }
public string CacheDirectory => PathManager.Inst.CachePath;
public ILogger Logger { get; }
public ResamplerManifest Manifest { get; }
public RendererPluginMetadata Metadata { get; }
public RendererAnalysisService Analysis { get; }
public RendererCacheService Cache { get; }

public RendererPluginContext(
string rendererId,
string rendererName,
string pluginDirectory,
string manifestPath,
ResamplerManifest manifest,
RendererPluginMetadata metadata = null,
ILogger logger = null) {
RendererId = rendererId;
RendererName = rendererName;
PluginDirectory = pluginDirectory;
ManifestPath = manifestPath;
Manifest = manifest;
Metadata = metadata ?? new RendererPluginMetadata();
Logger = logger ?? Log.Logger;
Analysis = new RendererAnalysisService(Metadata.AnalysisFormats);
Cache = new RendererCacheService(rendererId);
}
}

/// <summary>Resolves renderer-declared source analysis files without coupling
/// plugins to OpenUtau's render-output cache.</summary>
public sealed class RendererAnalysisService {
readonly IReadOnlyDictionary<string, AnalysisFormatManifest> formats;

internal RendererAnalysisService(IReadOnlyDictionary<string, AnalysisFormatManifest> formats) {
this.formats = formats;
}

public IReadOnlyDictionary<string, AnalysisFormatManifest> Formats => formats;

public string GetPath(string format, string sourceFile) {
if (!formats.TryGetValue(format, out var descriptor)) {
throw new KeyNotFoundException($"Unknown renderer analysis format '{format}'.");
}
var fullSource = Path.GetFullPath(sourceFile);
var directory = Path.GetDirectoryName(fullSource) ?? string.Empty;
var stem = Path.GetFileNameWithoutExtension(fullSource);
return descriptor.path
.Replace("{wav_dir}", directory, StringComparison.Ordinal)
.Replace("{wav_stem}", stem, StringComparison.Ordinal)
.Replace("{wav_name}", Path.GetFileName(fullSource), StringComparison.Ordinal);
}

public RendererAnalysisState GetBasicState(string format, string sourceFile) {
var outputFile = GetPath(format, sourceFile);
if (!File.Exists(outputFile)) return RendererAnalysisState.Missing;
if (!File.Exists(sourceFile)) return RendererAnalysisState.Invalid;
return File.GetLastWriteTimeUtc(outputFile) < File.GetLastWriteTimeUtc(sourceFile)
? RendererAnalysisState.Stale
: RendererAnalysisState.Valid;
}

}

/// <summary>Provides namespaced final-output cache paths. Intermediate engine
/// state belongs in memory; reusable source analysis belongs beside the source.</summary>
public sealed class RendererCacheService {
readonly string rendererKey;
internal RendererCacheService(string rendererId) {
rendererKey = string.Concat(rendererId.Select(character =>
char.IsLetterOrDigit(character) || character is '-' or '_' ? character : '-'));
}

public string GetPhrasePath(RenderPhrase phrase, string extension = ".wav") {
if (string.IsNullOrEmpty(extension)) extension = ".wav";
if (!extension.StartsWith('.')) extension = "." + extension;
var path = Path.Combine(PathManager.Inst.CachePath,
$"renderer-{rendererKey}-{phrase.hash:x16}{extension}");
phrase.AddCacheFile(path);
return path;
}
}
}
Loading
Loading