diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..f7c752928
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,8 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+*.cs text diff=csharp
+*.cshtml text diff=html
+*.csx text diff=csharp
+*.sln text eol=crlf
+*.csproj text eol=crlf
diff --git a/.github/workflows/release-cleanup.yml b/.github/workflows/release-cleanup.yml
new file mode 100644
index 000000000..fbe4f53b5
--- /dev/null
+++ b/.github/workflows/release-cleanup.yml
@@ -0,0 +1,28 @@
+# This is a basic workflow to help you get started with Actions
+
+name: release-cleanup
+
+# Controls when the workflow will run
+on:
+ # Triggers the workflow on push or pull request events but only for the "master" branch
+ push:
+ branches: [ "master" ]
+
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+# A workflow run is made up of one or more jobs that can run sequentially or in parallel
+jobs:
+ # This workflow contains a single job called "build"
+ build:
+ # The type of runner that the job will run on
+ runs-on: ubuntu-latest
+
+ # Steps represent a sequence of tasks that will be executed as part of the job
+ steps:
+ - uses: dev-drprasad/delete-older-releases@v0.2.0
+ with:
+ keep_latest: 16
+ delete_tag_pattern: build
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Misc/release.xml b/Misc/release.xml
deleted file mode 100644
index ac82434d0..000000000
--- a/Misc/release.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
--
- {{version}}
- https://github.com/stakira/OpenUtau/releases/download/OpenUtau-Latest/OpenUtau.zip
- false
-
diff --git a/OpenUtau.Core/Analysis/Crepe/Crepe.cs b/OpenUtau.Core/Analysis/Crepe/Crepe.cs
new file mode 100644
index 000000000..3b4657d95
--- /dev/null
+++ b/OpenUtau.Core/Analysis/Crepe/Crepe.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.ML.OnnxRuntime;
+using Microsoft.ML.OnnxRuntime.Tensors;
+using NWaves.Operations;
+using NWaves.Signals;
+
+namespace OpenUtau.Core.Analysis.Crepe {
+ public class Crepe : IDisposable {
+ const int kModelSampleRate = 16000;
+ const int kFrameSize = 1024;
+ const int kActivationSize = 360;
+
+ InferenceSession session;
+ double[] centsMapping;
+ private bool disposedValue;
+
+ public Crepe() {
+ session = new InferenceSession(Resources.tiny);
+ centsMapping = Enumerable.Range(0, kActivationSize)
+ .Select(i => i * 20 + 1997.3794084376191)
+ .ToArray();
+ }
+
+ public double[] ComputeF0(DiscreteSignal signal, double stepMs, double threshold = 0.21) {
+ if (signal.SamplingRate != kModelSampleRate) {
+ var resampler = new Resampler();
+ signal = resampler.Resample(signal, kModelSampleRate);
+ }
+ var input = ToFrames(signal, stepMs);
+ int length = input.Dimensions[0];
+ var inputs = new List();
+ inputs.Add(NamedOnnxValue.CreateFromTensor("input", input));
+ var outputs = session.Run(inputs);
+ var activations = outputs.First().AsTensor().ToArray();
+ int[] path = new int[length];
+ GetPath(activations, path);
+ float[] confidences = new float[length];
+ double[] cents = new double[length];
+ double[] f0 = new double[length];
+ for (int i = 0; i < length; ++i) {
+ var frame = new ArraySegment(activations, i * kActivationSize, kActivationSize);
+ cents[i] = GetCents(frame, path[i]);
+ confidences[i] = frame[path[i]];
+ f0[i] = double.IsNormal(cents[i])
+ && double.IsNormal(confidences[i])
+ && confidences[i] > threshold
+ ? 10f * Math.Pow(2.0, cents[i] / 1200.0) : 0;
+ }
+ return f0;
+ }
+
+ Tensor ToFrames(DiscreteSignal signal, double stepMs) {
+ float[] paddedSamples = new float[signal.Length + kFrameSize];
+ Array.Copy(signal.Samples, 0, paddedSamples, kFrameSize / 2, signal.Length);
+ int hopSize = (int)(kModelSampleRate * stepMs / 1000);
+ int length = signal.Length / hopSize;
+ float[] frames = new float[length * kFrameSize];
+ for (int i = 0; i < length; ++i) {
+ Array.Copy(paddedSamples, i * hopSize,
+ frames, i * kFrameSize, kFrameSize);
+ NormalizeFrame(new ArraySegment(
+ frames, i * kFrameSize, kFrameSize));
+ }
+ return frames.ToTensor().Reshape(new int[] { length, kFrameSize });
+ }
+
+ void GetPath(float[] activations, int[] path) {
+ float[] prob = new float[kActivationSize];
+ float[] nextProb = new float[kActivationSize];
+ for (int i = 0; i < kActivationSize; ++i) {
+ prob[i] = (float)Math.Log(1.0 / kActivationSize);
+ }
+ float[,] transitions = new float[kActivationSize, kActivationSize];
+ int dist = 12;
+ for (int i = 0; i < kActivationSize; ++i) {
+ int low = Math.Max(0, i - dist);
+ int high = Math.Min(kActivationSize, i + dist);
+ float sum = 0;
+ for (int j = low; j < high; ++j) {
+ transitions[i, j] = dist - Math.Abs(i - j);
+ sum += transitions[i, j];
+ }
+ for (int j = low; j < high; ++j) {
+ transitions[i, j] = (float)Math.Log(transitions[i, j] / sum);
+ }
+ }
+ for (int i = 0; i < path.Length; ++i) {
+ var activ = new ArraySegment(activations, i * kActivationSize, kActivationSize);
+ Array.Clear(nextProb, 0, nextProb.Length);
+ for (int j = 0; j < kActivationSize; ++j) {
+ int low = Math.Max(0, j - dist);
+ int high = Math.Min(kActivationSize, j + dist);
+ float maxP = float.MinValue;
+ for (int k = low; k < high; ++k) {
+ float p = (float)(prob[k] + transitions[j, k] + Math.Log(activ[k]));
+ if (p > maxP) {
+ maxP = p;
+ }
+ }
+ nextProb[j] = maxP;
+ }
+ path[i] = ArgMax(nextProb);
+ }
+ }
+
+ double GetCents(ArraySegment activations, int index) {
+ int start = Math.Max(0, index - 4);
+ int end = Math.Min(activations.Count, index + 5);
+ double weightedSum = 0;
+ double weightSum = 0;
+ for (int i = start; i < end; ++i) {
+ weightedSum += activations[i] * centsMapping[i];
+ weightSum += activations[i];
+ }
+ return weightedSum / weightSum;
+ }
+
+ static int ArgMax(Span values) {
+ int index = -1;
+ float value = float.MinValue;
+ for (int i = 0; i < values.Length; ++i) {
+ if (value < values[i]) {
+ index = i;
+ value = values[i];
+ }
+ }
+ return index;
+ }
+
+ void NormalizeFrame(ArraySegment data) {
+ double avg = data.Average();
+ double std = Math.Sqrt(data.Average(d => Math.Pow(d - avg, 2)));
+ for (int i = 0; i < data.Count; ++i) {
+ data[i] = (float)((data[i] - avg) / std);
+ }
+ }
+
+ protected virtual void Dispose(bool disposing) {
+ if (!disposedValue) {
+ if (disposing) {
+ session.Dispose();
+ }
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose() {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/OpenUtau.Core/Analysis/Crepe/LICENSE.txt b/OpenUtau.Core/Analysis/Crepe/LICENSE.txt
new file mode 100644
index 000000000..93f465297
--- /dev/null
+++ b/OpenUtau.Core/Analysis/Crepe/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 Jong Wook Kim
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/OpenUtau.Core/Analysis/Crepe/Resources.Designer.cs b/OpenUtau.Core/Analysis/Crepe/Resources.Designer.cs
new file mode 100644
index 000000000..47d74af5c
--- /dev/null
+++ b/OpenUtau.Core/Analysis/Crepe/Resources.Designer.cs
@@ -0,0 +1,73 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace OpenUtau.Core.Analysis.Crepe {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OpenUtau.Core.Analysis.Crepe.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized resource of type System.Byte[].
+ ///
+ internal static byte[] tiny {
+ get {
+ object obj = ResourceManager.GetObject("tiny", resourceCulture);
+ return ((byte[])(obj));
+ }
+ }
+ }
+}
diff --git a/OpenUtau.Core/Analysis/Crepe/Resources.resx b/OpenUtau.Core/Analysis/Crepe/Resources.resx
new file mode 100644
index 000000000..56da25608
--- /dev/null
+++ b/OpenUtau.Core/Analysis/Crepe/Resources.resx
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ tiny.onnx;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/OpenUtau.Core/Analysis/Crepe/tiny.onnx b/OpenUtau.Core/Analysis/Crepe/tiny.onnx
new file mode 100644
index 000000000..6f8ac7b7f
Binary files /dev/null and b/OpenUtau.Core/Analysis/Crepe/tiny.onnx differ
diff --git a/OpenUtau.Core/Api/G2pPack.cs b/OpenUtau.Core/Api/G2pPack.cs
index b448d52c7..bb2c92873 100644
--- a/OpenUtau.Core/Api/G2pPack.cs
+++ b/OpenUtau.Core/Api/G2pPack.cs
@@ -1,10 +1,7 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
-using System.Text;
using System.Text.RegularExpressions;
-using SharpCompress.Archives;
using Microsoft.ML.OnnxRuntime;
using Microsoft.ML.OnnxRuntime.Tensors;
using OpenUtau.Core.Util;
diff --git a/OpenUtau.Core/Api/Phonemizer.cs b/OpenUtau.Core/Api/Phonemizer.cs
index 14ee2532e..10f8b5747 100644
--- a/OpenUtau.Core/Api/Phonemizer.cs
+++ b/OpenUtau.Core/Api/Phonemizer.cs
@@ -13,14 +13,17 @@ public class PhonemizerAttribute : Attribute {
public string Name { get; private set; }
public string Tag { get; private set; }
public string Author { get; private set; }
+ public string Language { get; private set; }
/// Name of phonemizer. Required.
- /// Use IETF language code + phonetic type as tag, e.g., "EN ARPA", "JP VCV", etc. Required.
+ /// Use IETF language code + phonetic type as tag, e.g., "EN ARPA", "JA VCV", etc. Required.
/// Author of this phonemizer.
- public PhonemizerAttribute(string name, string tag, string author = null) {
+ /// IETF language code of this phonemizer's singing language, e.g., "EN", "JA"
+ public PhonemizerAttribute(string name, string tag, string author = null, string language = null) {
Name = name;
Tag = tag;
Author = author;
+ Language = language;
}
}
@@ -52,14 +55,14 @@ public struct Note {
public int tone;
///
- /// Position of note in part. Measured in ticks.
- /// Use TickToMs() and MsToTick() to convert between ticks and milliseconds .
+ /// Position of note in project, measured in ticks.
+ /// Use timeAxis to convert between ticks and milliseconds .
///
public int position;
///
- /// Duration of note in part. Measured in ticks.
- /// Use TickToMs() and MsToTick() to convert between ticks and milliseconds .
+ /// Duration of note measured in ticks.
+ /// Use timeAxis to convert between ticks and milliseconds .
///
public int duration;
@@ -132,10 +135,10 @@ public struct Result {
public string Name { get; set; }
public string Tag { get; set; }
+ public string Language { get; set; }
protected double bpm;
- private int beatUnit;
- private int resolution;
+ protected TimeAxis timeAxis;
///
/// Sets the current singer. Called by OpenUtau when user changes the singer.
@@ -150,6 +153,12 @@ public struct Result {
///
public abstract void SetSinger(USinger singer);
+ ///
+ /// Uses the legacy bahaviour of further mapping phonemizer outputs.
+ /// Do not override for new phonemizers.
+ ///
+ public virtual bool LegacyMapping => false;
+
public virtual void SetUp(Note[][] notes) { }
///
@@ -171,27 +180,28 @@ public virtual void CleanUp() { }
/// Used by OpenUtau to set timing info for TickToMs() and MsToTick().
/// Not need to call this method from within a phonemizer.
///
- public void SetTiming(double bpm, int beatUnit, int resolution) {
- this.bpm = bpm;
- this.beatUnit = beatUnit;
- this.resolution = resolution;
+ public void SetTiming(TimeAxis timeAxis) {
+ this.timeAxis = timeAxis;
+ bpm = timeAxis.GetBpmAtTick(0);
}
public string DictionariesPath => PathManager.Inst.DictionariesPath;
public string PluginDir => PathManager.Inst.PluginsPath;
///
- /// Utility method to convert ticks to milliseconds.
+ /// Utility method to convert tick position to millisecond position.
///
+ [Obsolete] // TODO: update usages
protected double TickToMs(int tick) {
- return MusicMath.TickToMillisecond(tick, bpm, beatUnit, resolution);
+ return timeAxis.TickPosToMsPos(tick);
}
///
- /// Utility method to convert milliseconds to ticks.
+ /// Utility method to convert millisecond position to tick position.
///
+ [Obsolete] // TODO: update usages
protected int MsToTick(double ms) {
- return MusicMath.MillisecondToTick(ms, bpm, beatUnit, resolution);
+ return timeAxis.MsPosToTickPos(ms);
}
///
diff --git a/OpenUtau.Core/Api/PhonemizerFactory.cs b/OpenUtau.Core/Api/PhonemizerFactory.cs
index e7b2ae3f6..e6b0d8383 100644
--- a/OpenUtau.Core/Api/PhonemizerFactory.cs
+++ b/OpenUtau.Core/Api/PhonemizerFactory.cs
@@ -8,11 +8,13 @@ public class PhonemizerFactory {
public string name;
public string tag;
public string author;
+ public string language;
public Phonemizer Create() {
var phonemizer = Activator.CreateInstance(type) as Phonemizer;
phonemizer.Name = name;
phonemizer.Tag = tag;
+ phonemizer.Language = language;
return phonemizer;
}
@@ -32,6 +34,7 @@ public static PhonemizerFactory Get(Type type) {
name = attr.Name,
tag = attr.Tag,
author = attr.Author,
+ language = attr.Language,
};
factories[type] = factory;
}
diff --git a/OpenUtau.Core/Api/PhonemizerRunner.cs b/OpenUtau.Core/Api/PhonemizerRunner.cs
index 97de95916..50b465a5c 100644
--- a/OpenUtau.Core/Api/PhonemizerRunner.cs
+++ b/OpenUtau.Core/Api/PhonemizerRunner.cs
@@ -10,14 +10,13 @@
namespace OpenUtau.Api {
internal class PhonemizerRequest {
+ public USinger singer;
public UVoicePart part;
public long timestamp;
public int[] noteIndexes;
public Phonemizer.Note[][] notes;
public Phonemizer phonemizer;
- public double bpm;
- public int beatUnit;
- public int resolution;
+ public TimeAxis timeAxis;
}
internal class PhonemizerResponse {
@@ -75,6 +74,7 @@ void SendResponse(PhonemizerResponse response) {
response.part.SetPhonemizerResponse(response);
}
DocManager.Inst.Project.Validate(new ValidateOptions {
+ SkipTiming = true,
Part = response.part,
SkipPhonemizer = true,
});
@@ -85,7 +85,16 @@ void SendResponse(PhonemizerResponse response) {
static PhonemizerResponse Phonemize(PhonemizerRequest request) {
var notes = request.notes;
var phonemizer = request.phonemizer;
- phonemizer.SetTiming(request.bpm, request.beatUnit, request.resolution);
+ if (request.singer == null) {
+ return new PhonemizerResponse() {
+ noteIndexes = request.noteIndexes,
+ part = request.part,
+ phonemes = new Phonemizer.Phoneme[][] { },
+ timestamp = request.timestamp,
+ };
+ }
+ phonemizer.SetSinger(request.singer);
+ phonemizer.SetTiming(request.timeAxis);
try {
phonemizer.SetUp(notes);
} catch (Exception e) {
@@ -135,6 +144,14 @@ static PhonemizerResponse Phonemize(PhonemizerRequest request) {
}
};
}
+ if (phonemizer.LegacyMapping) {
+ for (var k = 0; k < phonemizerResult.phonemes.Length; k++) {
+ var phoneme = phonemizerResult.phonemes[k];
+ if (request.singer.TryGetMappedOto(phoneme.phoneme, notes[i][0].tone, out var oto)) {
+ phonemizerResult.phonemes[k].phoneme = oto.Alias;
+ }
+ }
+ }
for (var j = 0; j < phonemizerResult.phonemes.Length; j++) {
phonemizerResult.phonemes[j].position += notes[i][0].position;
}
diff --git a/OpenUtau.Core/Classic/ClassicRenderer.cs b/OpenUtau.Core/Classic/ClassicRenderer.cs
index b167c2efb..d8968ffc3 100644
--- a/OpenUtau.Core/Classic/ClassicRenderer.cs
+++ b/OpenUtau.Core/Classic/ClassicRenderer.cs
@@ -1,25 +1,31 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using NAudio.Wave;
+using OpenUtau.Core;
+using OpenUtau.Core.Format;
using OpenUtau.Core.Render;
using OpenUtau.Core.Ustx;
+using OpenUtau.Core.Util;
+using Serilog;
namespace OpenUtau.Classic {
public class ClassicRenderer : IRenderer {
static readonly HashSet supportedExp = new HashSet(){
- Core.Format.Ustx.DYN,
- Core.Format.Ustx.PITD,
- Core.Format.Ustx.CLR,
- Core.Format.Ustx.SHFT,
- Core.Format.Ustx.ENG,
- Core.Format.Ustx.VEL,
- Core.Format.Ustx.VOL,
- Core.Format.Ustx.ATK,
- Core.Format.Ustx.DEC,
- Core.Format.Ustx.MOD,
- Core.Format.Ustx.ALT,
+ Ustx.DYN,
+ Ustx.PITD,
+ Ustx.CLR,
+ Ustx.SHFT,
+ Ustx.ENG,
+ Ustx.VEL,
+ Ustx.VOL,
+ Ustx.ATK,
+ Ustx.DEC,
+ Ustx.MOD,
+ Ustx.ALT,
};
public USingerType SingerType => USingerType.Classic;
@@ -33,70 +39,99 @@ public bool SupportsExpression(UExpressionDescriptor descriptor) {
}
public RenderResult Layout(RenderPhrase phrase) {
- var firstPhone = phrase.phones.First();
- var lastPhone = phrase.phones.Last();
return new RenderResult() {
- leadingMs = firstPhone.preutterMs,
- positionMs = (phrase.position + firstPhone.position) * phrase.tickToMs,
- estimatedLengthMs = (lastPhone.duration + lastPhone.position - firstPhone.position) * phrase.tickToMs + firstPhone.preutterMs,
+ leadingMs = phrase.leadingMs,
+ positionMs = phrase.positionMs,
+ estimatedLengthMs = phrase.durationMs + phrase.leadingMs,
};
}
public Task Render(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender) {
+ if (phrase.wavtool == SharpWavtool.nameConvergence || phrase.wavtool == SharpWavtool.nameSimple) {
+ return RenderInternal(phrase, progress, cancellation, isPreRender);
+ } else {
+ return RenderExternal(phrase, progress, cancellation, isPreRender);
+ }
+ }
+
+ public Task RenderInternal(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender) {
var resamplerItems = new List();
foreach (var phone in phrase.phones) {
resamplerItems.Add(new ResamplerItem(phrase, phone));
}
var task = Task.Run(() => {
Parallel.ForEach(source: resamplerItems, parallelOptions: new ParallelOptions() {
- MaxDegreeOfParallelism = 2
+ MaxDegreeOfParallelism = Preferences.Default.NumRenderThreads
}, body: item => {
if (!cancellation.IsCancellationRequested && !File.Exists(item.outputFile)) {
- VoicebankFiles.CopySourceTemp(item.inputFile, item.inputTemp);
- item.resampler.DoResamplerReturnsFile(item, Serilog.Log.Logger);
+ if (!(item.resampler is WorldlineResampler)) {
+ VoicebankFiles.Inst.CopySourceTemp(item.inputFile, item.inputTemp);
+ }
+ lock (Renderers.GetCacheLock(item.outputFile)) {
+ item.resampler.DoResamplerReturnsFile(item, Log.Logger);
+ }
if (!File.Exists(item.outputFile)) {
- throw new InvalidDataException($"{item.resampler.Name} failed to resample \"{item.phone.phoneme}\"");
+ throw new InvalidDataException($"{item.resampler} failed to resample \"{item.phone.phoneme}\"");
+ }
+ if (!(item.resampler is WorldlineResampler)) {
+ VoicebankFiles.Inst.CopyBackMetaFiles(item.inputFile, item.inputTemp);
}
- VoicebankFiles.CopyBackMetaFiles(item.inputFile, item.inputTemp);
}
- progress.Complete(1, $"Resampling \"{item.phone.phoneme}\"");
+ progress.Complete(1, $"{item.resampler} \"{item.phone.phoneme}\"");
});
var result = Layout(phrase);
- result.samples = Concatenate(resamplerItems, cancellation);
+ var wavtool = new SharpWavtool(true);
+ result.samples = wavtool.Concatenate(resamplerItems, string.Empty, cancellation);
if (result.samples != null) {
- ApplyDynamics(phrase, result.samples);
+ Renderers.ApplyDynamics(phrase, result);
}
return result;
});
return task;
}
- float[] Concatenate(List resamplerItems, CancellationTokenSource cancellation) {
- var wavtool = new SharpWavtool(Core.Util.Preferences.Default.PhaseCompensation == 1);
- return wavtool.Concatenate(resamplerItems, cancellation);
- }
-
- void ApplyDynamics(RenderPhrase phrase, float[] samples) {
- const int interval = 5;
- if (phrase.dynamics == null) {
- return;
+ public Task RenderExternal(RenderPhrase phrase, Progress progress, CancellationTokenSource cancellation, bool isPreRender) {
+ var resamplerItems = new List();
+ foreach (var phone in phrase.phones) {
+ resamplerItems.Add(new ResamplerItem(phrase, phone));
}
- int pos = 0;
- for (int i = 0; i < phrase.dynamics.Length; ++i) {
- int endPos = (int)((i + 1) * interval * phrase.tickToMs / 1000 * 44100);
- float a = phrase.dynamics[i];
- float b = (i + 1) == phrase.dynamics.Length ? phrase.dynamics[i] : phrase.dynamics[i + 1];
- for (int j = pos; j < endPos; ++j) {
- samples[j] *= a + (b - a) * (j - pos) / (endPos - pos);
+ var task = Task.Run(() => {
+ string progressInfo = $"{phrase.wavtool} \"{string.Join(" ", phrase.phones.Select(p => p.phoneme))}\"";
+ progress.Complete(0, progressInfo);
+ var wavPath = Path.Join(PathManager.Inst.CachePath, $"cat-{phrase.hash:x16}.wav");
+ var result = Layout(phrase);
+ if (File.Exists(wavPath)) {
+ try {
+ using (var waveStream = Wave.OpenFile(wavPath)) {
+ result.samples = Wave.GetSamples(waveStream.ToSampleProvider().ToMono(1, 0));
+ }
+ } catch (Exception e) {
+ Log.Error(e, "Failed to render.");
+ }
}
- pos = endPos;
- }
+ if (result.samples == null) {
+ foreach (var item in resamplerItems) {
+ VoicebankFiles.Inst.CopySourceTemp(item.inputFile, item.inputTemp);
+ }
+ var wavtool = ToolsManager.Inst.GetWavtool(phrase.wavtool);
+ result.samples = wavtool.Concatenate(resamplerItems, wavPath, cancellation);
+ foreach (var item in resamplerItems) {
+ VoicebankFiles.Inst.CopyBackMetaFiles(item.inputFile, item.inputTemp);
+ }
+ }
+ progress.Complete(phrase.phones.Length, progressInfo);
+ if (result.samples != null) {
+ Renderers.ApplyDynamics(phrase, result);
+ }
+ return result;
+ });
+ return task;
}
public RenderPitchResult LoadRenderedPitch(RenderPhrase phrase) {
return null;
}
- public override string ToString() => "CLASSIC";
+ public override string ToString() => Renderers.CLASSIC;
}
}
diff --git a/OpenUtau.Core/Classic/ClassicSinger.cs b/OpenUtau.Core/Classic/ClassicSinger.cs
index 9b745f64a..b3616bd6a 100644
--- a/OpenUtau.Core/Classic/ClassicSinger.cs
+++ b/OpenUtau.Core/Classic/ClassicSinger.cs
@@ -29,15 +29,15 @@ public class ClassicSinger : USinger {
public override string DefaultPhonemizer => voicebank.DefaultPhonemizer;
public override Encoding TextFileEncoding => voicebank.TextFileEncoding;
public override IList Subbanks => subbanks;
- public override Dictionary Otos => otos;
+ public override IList Otos => otos;
Voicebank voicebank;
List errors = new List();
byte[] avatarData;
- List OtoSets = new List();
- Dictionary otos = new Dictionary();
- Dictionary> phonetics;
+ List otoSets = new List();
List subbanks = new List();
+ List otos = new List();
+ Dictionary otoMap = new Dictionary();
OtoWatcher otoWatcher;
public ClassicSinger(Voicebank voicebank) {
@@ -63,6 +63,7 @@ public override void Reload() {
if (otoWatcher == null) {
otoWatcher = new OtoWatcher(this, Location);
}
+ OtoDirty = false;
} catch (Exception e) {
Log.Error(e, $"Failed to load {voicebank.File}");
}
@@ -94,13 +95,20 @@ void Load() {
.ToList();
var dummy = new USubbank(new Subbank());
- OtoSets.Clear();
+ otoSets.Clear();
otos.Clear();
+ otoMap.Clear();
errors.Clear();
foreach (var otoSet in voicebank.OtoSets) {
var uSet = new UOtoSet(otoSet, voicebank.BasePath);
- OtoSets.Add(uSet);
+ otoSets.Add(uSet);
foreach (var oto in otoSet.Otos) {
+ if (!oto.IsValid) {
+ if (!string.IsNullOrEmpty(oto.Error)) {
+ errors.Add(oto.Error);
+ }
+ continue;
+ }
UOto? uOto = null;
for (var i = 0; i < patterns.Count; i++) {
var m = patterns[i].Match(oto.Alias);
@@ -113,20 +121,17 @@ void Load() {
if (uOto == null) {
uOto = new UOto(oto, uSet, dummy);
}
- if (!otos.ContainsKey(oto.Alias)) {
- otos.Add(oto.Alias, uOto);
+ otos.Add(uOto);
+ if (!otoMap.ContainsKey(oto.Alias)) {
+ otoMap.Add(oto.Alias, uOto);
} else {
//Errors.Add($"oto conflict {Otos[oto.Alias].Set}/{oto.Alias} and {otoSet.Name}/{oto.Alias}");
}
}
- errors.AddRange(otoSet.Errors);
}
- phonetics = otos.Values
- .GroupBy(oto => oto.Phonetic, oto => oto)
- .ToDictionary(g => g.Key, g => g.OrderByDescending(oto => oto.Prefix.Length + oto.Suffix.Length).ToList());
Task.Run(() => {
- otos.Values
+ otoMap.Values
.ToList()
.ForEach(oto => {
oto.SearchTerms.Add(oto.Alias.ToLowerInvariant().Replace(" ", ""));
@@ -135,22 +140,38 @@ void Load() {
});
}
+ public override void Save() {
+ try {
+ otoWatcher.Paused = true;
+ foreach (var oto in Otos) {
+ oto.WriteBack();
+ }
+ VoicebankLoader.WriteOtoSets(voicebank);
+ } finally {
+ otoWatcher.Paused = false;
+ }
+ }
+
+ public override bool TryGetOto(string phoneme, out UOto oto) {
+ if (otoMap.TryGetValue(phoneme, out oto)) {
+ return true;
+ }
+ return false;
+ }
+
public override bool TryGetMappedOto(string phoneme, int tone, out UOto oto) {
oto = default;
var subbank = subbanks.Find(subbank => string.IsNullOrEmpty(subbank.Color) && subbank.toneSet.Contains(tone));
- if (subbank != null && otos.TryGetValue($"{subbank.Prefix}{phoneme}{subbank.Suffix}", out oto)) {
+ if (subbank != null && otoMap.TryGetValue($"{subbank.Prefix}{phoneme}{subbank.Suffix}", out oto)) {
return true;
}
- if (otos.TryGetValue(phoneme, out oto)) {
- return true;
- }
- return false;
+ return TryGetOto(phoneme, out oto);
}
public override bool TryGetMappedOto(string phoneme, int tone, string color, out UOto oto) {
oto = default;
var subbank = subbanks.Find(subbank => subbank.Color == color && subbank.toneSet.Contains(tone));
- if (subbank != null && otos.TryGetValue($"{subbank.Prefix}{phoneme}{subbank.Suffix}", out oto)) {
+ if (subbank != null && otoMap.TryGetValue($"{subbank.Prefix}{phoneme}{subbank.Suffix}", out oto)) {
return true;
}
return TryGetMappedOto(phoneme, tone, out oto);
@@ -161,7 +182,7 @@ public override IEnumerable GetSuggestions(string text) {
text = text.ToLowerInvariant().Replace(" ", "");
}
bool all = string.IsNullOrEmpty(text);
- return otos.Values
+ return otoMap.Values
.Where(oto => all || oto.SearchTerms.Exists(term => term.Contains(text)));
}
diff --git a/OpenUtau.Core/Classic/ExeResampler.cs b/OpenUtau.Core/Classic/ExeResampler.cs
index 26cca49d9..34f6b5628 100644
--- a/OpenUtau.Core/Classic/ExeResampler.cs
+++ b/OpenUtau.Core/Classic/ExeResampler.cs
@@ -14,13 +14,13 @@ internal class ExeResampler : IResampler {
public string Name { get; private set; }
public string FilePath { get; private set; }
public bool isLegalPlugin => _isLegalPlugin;
-
+ readonly string _name;
readonly bool _isLegalPlugin = false;
public ExeResampler(string filePath, string basePath) {
if (File.Exists(filePath)) {
FilePath = filePath;
- Name = Path.GetRelativePath(basePath, filePath);
+ _name = Path.GetRelativePath(basePath, filePath);
_isLegalPlugin = true;
}
}
@@ -42,23 +42,12 @@ public string DoResamplerReturnsFile(ResamplerItem args, ILogger logger) {
var threadId = Thread.CurrentThread.ManagedThreadId;
string tmpFile = args.outputFile;
string ArgParam = FormattableString.Invariant(
- $"\"{args.inputTemp}\" \"{tmpFile}\" {MusicMath.GetToneName(args.tone)} {args.velocity} \"{BuildFlagsStr(args.flags)}\" {args.offset} {args.requiredLength} {args.consonant} {args.cutoff} {args.volume} {args.modulation} !{args.tempo} {Base64.Base64EncodeInt12(args.pitches)}");
+ $"\"{args.inputTemp}\" \"{tmpFile}\" {MusicMath.GetToneName(args.tone)} {args.velocity} \"{args.GetFlagsString()}\" {args.offset} {args.durRequired} {args.consonant} {args.cutoff} {args.volume} {args.modulation} !{args.tempo} {Base64.Base64EncodeInt12(args.pitches)}");
logger.Information($" > [thread-{threadId}] {FilePath} {ArgParam}");
ProcessRunner.Run(FilePath, ArgParam, logger);
return tmpFile;
}
- string BuildFlagsStr(Tuple[] flags) {
- var builder = new StringBuilder();
- foreach (var flag in flags) {
- builder.Append(flag.Item1);
- if (flag.Item2.HasValue) {
- builder.Append(flag.Item2.Value);
- }
- }
- return builder.ToString();
- }
-
[DllImport("libc", SetLastError = true)]
private static extern int chmod(string pathname, int mode);
@@ -70,6 +59,6 @@ public void CheckPermissions() {
chmod(FilePath, mode);
}
- public override string ToString() => Name;
+ public override string ToString() => _name;
}
}
diff --git a/OpenUtau.Core/Classic/ExeWavtool.cs b/OpenUtau.Core/Classic/ExeWavtool.cs
new file mode 100644
index 000000000..fd7469a86
--- /dev/null
+++ b/OpenUtau.Core/Classic/ExeWavtool.cs
@@ -0,0 +1,155 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using NAudio.Wave;
+using OpenUtau.Core;
+using OpenUtau.Core.Render;
+using OpenUtau.Core.Util;
+using Serilog;
+
+namespace OpenUtau.Classic {
+ class ExeWavtool : IWavtool {
+ static object tempBatLock = new object();
+
+ readonly StringBuilder sb = new StringBuilder();
+ readonly string filePath;
+ readonly string name;
+
+ public ExeWavtool(string filePath, string basePath) {
+ this.filePath = filePath;
+ name = Path.GetRelativePath(basePath, filePath);
+ }
+
+ public float[] Concatenate(List resamplerItems, string tempPath, CancellationTokenSource cancellation) {
+ if (cancellation.IsCancellationRequested) {
+ return null;
+ }
+ PrepareHelper();
+ string batPath = Path.Combine(PathManager.Inst.CachePath, "temp.bat");
+ lock (tempBatLock) {
+ using (var stream = File.Open(batPath, FileMode.Create)) {
+ using (var writer = new StreamWriter(stream, new UTF8Encoding(false))) {
+ WriteSetUp(writer, resamplerItems, tempPath);
+ for (var i = 0; i < resamplerItems.Count; i++) {
+ WriteItem(writer, resamplerItems[i], i, resamplerItems.Count);
+ }
+ WriteTearDown(writer);
+ }
+ }
+ ProcessRunner.Run(batPath, "", Log.Logger, workDir: PathManager.Inst.CachePath, timeoutMs: 5 * 60 * 1000);
+ }
+ if (string.IsNullOrEmpty(tempPath) || File.Exists(tempPath)) {
+ using (var wavStream = Core.Format.Wave.OpenFile(tempPath)) {
+ return Core.Format.Wave.GetSamples(wavStream.ToSampleProvider().ToMono(1, 0));
+ }
+ }
+ return new float[0];
+ }
+
+ void PrepareHelper() {
+ string tempHelper = Path.Join(PathManager.Inst.CachePath, "temp_helper.bat");
+ lock (Renderers.GetCacheLock(tempHelper)) {
+ if (!File.Exists(tempHelper)) {
+ using (var stream = File.Open(tempHelper, FileMode.Create)) {
+ using (var writer = new StreamWriter(stream, new UTF8Encoding(false))) {
+ WriteHelper(writer);
+ }
+ }
+ }
+ }
+ }
+
+ void WriteHelper(StreamWriter writer) {
+ // writes temp_helper.bat
+ writer.WriteLine("@if exist %temp% goto A");
+ writer.WriteLine("@\"%resamp%\" %1 %temp% %2 %vel% %flag% %5 %6 %7 %8 %params%");
+ writer.WriteLine(":A");
+ writer.WriteLine("@\"%tool%\" \"%output%\" %temp% %stp% %3 %env%");
+ }
+
+ void WriteSetUp(StreamWriter writer, List resamplerItems, string tempPath) {
+ string globalFlags = "";
+
+ writer.WriteLine("@rem project=");
+ writer.WriteLine("@set loadmodule=");
+ writer.WriteLine($"@set tempo={resamplerItems[0].tempo}");
+ writer.WriteLine($"@set samples={44100}");
+ writer.WriteLine($"@set oto={PathManager.Inst.CachePath}");
+ writer.WriteLine($"@set tool={filePath}");
+ string tempFile = Path.GetRelativePath(PathManager.Inst.CachePath, tempPath);
+ writer.WriteLine($"@set output={tempFile}");
+ writer.WriteLine("@set helper=temp_helper.bat");
+ writer.WriteLine($"@set cachedir={PathManager.Inst.CachePath}");
+ writer.WriteLine($"@set flag=\"{globalFlags}\"");
+ writer.WriteLine("@set env=0 5 35 0 100 100 0");
+ writer.WriteLine("@set stp=0");
+ writer.WriteLine("");
+ writer.WriteLine("@del \"%output%\" 2>nul");
+ writer.WriteLine("@mkdir \"%cachedir%\" 2>nul");
+ writer.WriteLine("");
+ }
+
+ void WriteItem(StreamWriter writer, ResamplerItem item, int index, int total) {
+ writer.WriteLine($"@set resamp={item.resampler.FilePath}");
+ writer.WriteLine($"@set params={item.volume} {item.modulation} !{item.tempo} {Base64.Base64EncodeInt12(item.pitches)}");
+ writer.WriteLine($"@set flag=\"{item.GetFlagsString()}\"");
+ writer.WriteLine($"@set env={GetEnvelope(item)}");
+ writer.WriteLine($"@set stp={item.skipOver}");
+ writer.WriteLine($"@set vel={item.velocity}");
+ string relOutputFile = Path.GetRelativePath(PathManager.Inst.CachePath, item.outputFile);
+ writer.WriteLine($"@set temp=\"%cachedir%\\{relOutputFile}\"");
+ string toneName = MusicMath.GetToneName(item.tone);
+ string dur = $"{item.phone.duration}@{item.phone.tempo}{(item.durCorrection >= 0 ? "+" : "")}{item.durCorrection}";
+ string relInputTemp = Path.GetRelativePath(PathManager.Inst.CachePath, item.inputTemp);
+ writer.WriteLine($"@echo {MakeProgressBar(index + 1, total)}");
+ writer.WriteLine($"@call %helper% \"%oto%\\{relInputTemp}\" {toneName} {dur} {item.preutter} {item.offset} {item.durRequired} {item.consonant} {item.cutoff} {index}");
+ }
+
+ string MakeProgressBar(int index, int total) {
+ const int kWidth = 40;
+ int fill = index * kWidth / total;
+ return $"{new string('#', fill)}{new string('-', kWidth - fill)}({index}/{total})";
+ }
+
+ string GetEnvelope(ResamplerItem item) {
+ var env = item.phone.envelope;
+ sb.Clear()
+ .Append(env[0].X - env[0].X).Append(' ')
+ .Append(env[1].X - env[0].X).Append(' ')
+ .Append(env[4].X - env[3].X).Append(' ')
+ .Append(env[0].Y).Append(' ')
+ .Append(env[1].Y).Append(' ')
+ .Append(env[3].Y).Append(' ')
+ .Append(env[4].Y).Append(' ')
+ .Append(item.overlap).Append(' ')
+ .Append(env[4].X - env[4].X).Append(' ')
+ .Append(env[2].X - env[1].X).Append(' ')
+ .Append(env[2].Y);
+ return sb.ToString();
+ }
+
+ void WriteTearDown(StreamWriter writer) {
+ writer.WriteLine("@if not exist \"%output%.whd\" goto E");
+ writer.WriteLine("@if not exist \"%output%.dat\" goto E");
+ writer.WriteLine("copy /Y \"%output%.whd\" /B + \"%output%.dat\" /B \"%output%\"");
+ writer.WriteLine("del \"%output%.whd\"");
+ writer.WriteLine("del \"%output%.dat\"");
+ writer.WriteLine(":E");
+ }
+
+ [DllImport("libc", SetLastError = true)]
+ private static extern int chmod(string pathname, int mode);
+
+ public void CheckPermissions() {
+ if (OS.IsWindows() || !File.Exists(filePath)) {
+ return;
+ }
+ int mode = (7 << 6) | (5 << 3) | 5;
+ chmod(filePath, mode);
+ }
+
+ public override string ToString() => name;
+ }
+}
diff --git a/OpenUtau.Core/Classic/Frq.cs b/OpenUtau.Core/Classic/Frq.cs
new file mode 100644
index 000000000..821d5bfe6
--- /dev/null
+++ b/OpenUtau.Core/Classic/Frq.cs
@@ -0,0 +1,71 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace OpenUtau.Classic {
+ public class Frq {
+ public const int kHopSize = 256;
+
+ public int hopSize;
+ public double averageF0;
+ public double[] f0 = new double[0];
+ public double[] amp = new double[0];
+
+ public void Load(Stream stream) {
+ using (var reader = new BinaryReader(stream)) {
+ string header = new string(reader.ReadChars(8));
+ if (header != "FREQ0003") {
+ throw new FormatException("FREQ0003 header not found.");
+ }
+ hopSize = reader.ReadInt32();
+ averageF0 = reader.ReadDouble();
+ _ = reader.ReadBytes(16); // blank
+ int length = reader.ReadInt32();
+ f0 = new double[length];
+ amp = new double[length];
+ for (int i = 0; i < length; i++) {
+ f0[i] = reader.ReadDouble();
+ amp[i] = reader.ReadDouble();
+ }
+ }
+ }
+
+ public void Save(Stream stream) {
+ using (var writer = new BinaryWriter(stream)) {
+ writer.Write(Encoding.ASCII.GetBytes("FREQ0003"));
+ writer.Write(hopSize);
+ writer.Write(averageF0);
+ writer.Write(0);
+ writer.Write(0);
+ writer.Write(0);
+ writer.Write(0);
+ writer.Write(f0.Length);
+ for (int i = 0; i < f0.Length; ++i) {
+ writer.Write(f0[i]);
+ writer.Write(amp[i]);
+ }
+ }
+ }
+
+ public static Frq Build(float[] samples, double[] f0) {
+ var frq = new Frq();
+ frq.hopSize = kHopSize;
+ frq.f0 = f0;
+ frq.averageF0 = frq.f0.Where(f => f > 0).DefaultIfEmpty(0).Average();
+
+ double ampMult = Math.Pow(2, 15);
+ frq.amp = new double[frq.f0.Length];
+ for (int i = 0; i < frq.amp.Length; ++i) {
+ double sum = 0;
+ int count = 0;
+ for (int j = frq.hopSize * i; j < frq.hopSize * (i + 1) && j < samples.Length; ++j) {
+ sum += Math.Abs(samples[j]);
+ count++;
+ }
+ frq.amp[i] = count == 0 ? 0 : sum * ampMult / count;
+ }
+ return frq;
+ }
+ }
+}
diff --git a/OpenUtau.Core/Classic/IResampler.cs b/OpenUtau.Core/Classic/IResampler.cs
index e4c7b65bd..27dc1c9a0 100644
--- a/OpenUtau.Core/Classic/IResampler.cs
+++ b/OpenUtau.Core/Classic/IResampler.cs
@@ -2,7 +2,6 @@
namespace OpenUtau.Classic {
public interface IResampler {
- string Name { get; }
string FilePath { get; }
float[] DoResampler(ResamplerItem args, ILogger logger);
string DoResamplerReturnsFile(ResamplerItem args, ILogger logger);
diff --git a/OpenUtau.Core/Classic/IWavtool.cs b/OpenUtau.Core/Classic/IWavtool.cs
new file mode 100644
index 000000000..03e6dd310
--- /dev/null
+++ b/OpenUtau.Core/Classic/IWavtool.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Threading;
+
+namespace OpenUtau.Classic {
+ public interface IWavtool {
+ //