diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 9c830efdd..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "openutau-devcontainer", - "image": "mcr.microsoft.com/devcontainers/dotnet:6.0", // Any generic, debian-based image. - "features": { - "ghcr.io/devcontainers/features/desktop-lite:1": {} - }, - "customizations": { - "vscode": { - "extensions": [ - "ms-dotnettools.csdevkit" - ] - } - } -} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1ab210ae..c64d623fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,65 +1,159 @@ -on: workflow_dispatch +on: + workflow_dispatch: + inputs: + version: + description: "Version" + default: "0.0.0" + required: true + type: string + release: + type: boolean + default: false + description: "Release" + beta: + type: boolean + default: true + description: "Beta" + draft: + type: boolean + default: true + description: "Draft" + +env: + release-name: ${{ inputs.version }}${{ inputs.beta && ' Beta' || '' }} jobs: - pr-test: - runs-on: ${{ matrix.os.runs-on }} + build: + runs-on: ${{ matrix.arch.runs-on }} strategy: + fail-fast: false matrix: - os: - - runs-on: windows-latest - arch: win-x64 - rid: win-x64 - - runs-on: ubuntu-latest - arch: linux-x64 - rid: linux-x64 + arch: + - { name: win-x64, rid: win-x64, arch: x64, os: win, runs-on: windows-latest } + - { name: win-x86, rid: win-x86, arch: x86, os: win, runs-on: windows-latest } + - { name: win-arm64, rid: win-arm64, arch: arm64, os: win, runs-on: windows-latest } + - { name: osx-x64, rid: osx.10.14-x64, arch: x64, os: osx, runs-on: macos-13 } + - { name: osx-arm64, rid: osx-arm64, arch: arm64, os: osx, runs-on: macos-13 } + - { name: linux-x64, rid: linux-x64, arch: x64, os: linux, runs-on: ubuntu-latest } + - { name: linux-arm64, rid: linux-arm64, arch: arm64, os: linux, runs-on: ubuntu-latest } steps: + # Setup - uses: actions/checkout@v4 - uses: actions/setup-dotnet@v4 with: dotnet-version: "6.0.x" - - name: Restore - run: dotnet restore OpenUtau -r ${{ matrix.os.rid }} - - name: test + - uses: actions/setup-node@v4 + with: + node-version: 20 + - uses: justalemon/VersionPatcher@v0.7.1 + with: + version: ${{ inputs.version }} + csproj-files: "OpenUtau/*.csproj" + + # Test + - name: Test run: dotnet test OpenUtau.Test - - name: Download DirectML.dll - shell: powershell - run: Invoke-WebRequest -Uri "https://www.nuget.org/api/v2/package/Microsoft.AI.DirectML/1.12.0" -OutFile "Microsoft.AI.DirectML.nupkg" - if: matrix.os.arch == 'win-x64' - - name: Extract DirectML.dll + # Build + - name: Restore + run: dotnet restore OpenUtau -r ${{ matrix.arch.rid }} + + - name: Publish + run: dotnet publish OpenUtau -c Release -r ${{ matrix.arch.rid }} --self-contained true -o bin/${{ matrix.arch.name }}/ + if: ${{ matrix.arch.os != 'osx' }} + + # Create Zip + - name: DirectML shell: cmd run: | + curl -L https://www.nuget.org/api/v2/package/Microsoft.AI.DirectML/1.12.0 -o Microsoft.AI.DirectML.nupkg mkdir Microsoft.AI.DirectML tar -xf Microsoft.AI.DirectML.nupkg -C Microsoft.AI.DirectML - if: matrix.os.arch == 'win-x64' + copy /y Microsoft.AI.DirectML\bin\${{ matrix.arch.arch }}-${{ matrix.arch.os }}\DirectML.dll bin\${{ matrix.arch.name }}\ + if: ${{ matrix.arch.os == 'win' }} + + - name: Set executable permission + run: chmod +x bin/${{ matrix.arch.name }}/OpenUtau + if: ${{ matrix.arch.os == 'linux' }} + + - name: Zip + run: | + cd bin/${{ matrix.arch.name }} + 7z a ../../OpenUtau-${{ matrix.arch.name }}.zip * + if: ${{ matrix.arch.os != 'osx' }} - - name: Build non-mac - run: dotnet publish OpenUtau -c Release -r ${{ matrix.os.rid }} --self-contained true -o bin/${{ matrix.os.arch }}/ - if: matrix.os.arch != 'osx-x64' - - name: upload non-mac build - uses: actions/upload-artifact@v4 + # Create Installer + - name: Get VC Redist + run: | + curl https://aka.ms/vs/17/release/vc_redist.${{ matrix.arch.arch }}.exe -o vc_redist.${{ matrix.arch.arch }}.exe + if: ${{ matrix.arch.os == 'win' }} + + - name: Create Installer + uses: joncloud/makensis-action@v4.1 with: - name: OpenUtau-${{ matrix.os.arch }} - path: bin/${{ matrix.os.arch }} - if: matrix.os.arch != 'osx-x64' - - - name: Setup Node.js - uses: actions/setup-node@v4 - if: matrix.os.arch == 'osx-x64' - - name: Build mac + script-file: OpenUtau.nsi + arguments: "-DPRODUCT_VERSION=${{ inputs.version }} -DARCH=${{ matrix.arch.arch }}" + if: ${{ matrix.arch.os == 'win' }} + + # Create Dmg + - name: Create Dmg run: | - dotnet msbuild OpenUtau -t:BundleApp -p:Configuration=Release -p:RuntimeIdentifier=${{ matrix.os.rid }} -p:UseAppHost=true -p:OutputPath=../bin/${{ matrix.os.arch }}/ - cp OpenUtau/Assets/OpenUtau.icns bin/${{ matrix.os.arch }}/publish/OpenUtau.app/Contents/Resources/ + dotnet msbuild OpenUtau -t:BundleApp -p:Configuration=Release -p:RuntimeIdentifier=${{ matrix.arch.rid }} -p:UseAppHost=true -p:OutputPath=../bin/${{ matrix.arch.name }}/ npm install -g create-dmg - create-dmg bin/osx-x64/publish/OpenUtau.app - mv *.dmg OpenUtau-osx-x64.dmg - codesign -fvs - OpenUtau-osx-x64.dmg - if: matrix.os.arch == 'osx-x64' - - name: Upload mac build - uses: actions/upload-artifact@v4 + cp OpenUtau/Assets/OpenUtau.icns bin/${{ matrix.arch.name }}/publish/OpenUtau.app/Contents/Resources/ + create-dmg bin/${{ matrix.arch.name }}/publish/OpenUtau.app || true + mv *.dmg OpenUtau-${{ matrix.arch.name }}.dmg + codesign -fvs - OpenUtau-${{ matrix.arch.name }}.dmg + if: ${{ matrix.arch.os == 'osx' }} + + # Upload Artifacts + - uses: actions/upload-artifact@v4 + with: + name: OpenUtau-${{ matrix.arch.name }}.zip + path: OpenUtau-${{ matrix.arch.name }}.zip + if: ${{ !inputs.release && matrix.arch.os != 'osx' }} + + - uses: actions/upload-artifact@v4 + with: + name: OpenUtau-${{ matrix.arch.name }}.exe + path: OpenUtau-${{ matrix.arch.name }}.exe + if: ${{ !inputs.release && matrix.arch.os == 'win' }} + + - uses: actions/upload-artifact@v4 + with: + name: OpenUtau-osx-${{ matrix.arch.name }}.dmg + path: OpenUtau-osx-${{ matrix.arch.name }}.dmg + if: ${{ !inputs.release && matrix.arch.os == 'osx' }} + + # Appcast + - name: Appcast Windows + shell: cmd + run: | + python appcast.py -v=${{ inputs.version }} -o=windows -r=${{ matrix.arch.name }} -f=OpenUtau-${{ matrix.arch.name }}.zip + python appcast.py -v=${{ inputs.version }} -o=windows -r=${{ matrix.arch.name }}-installer -f=OpenUtau-${{ matrix.arch.name }}.exe + if: ${{ inputs.release && matrix.arch.os == 'win' }} + + - name: Appcast MacOS + run: | + python appcast.py -v=${{ inputs.version }} -o=macos -r=${{ matrix.arch.name }} -f=OpenUtau-${{ matrix.arch.name }}.dmg + if: ${{ inputs.release && matrix.arch.os == 'osx' }} + + - name: Appcast Linux + run: | + python appcast.py -v=${{ inputs.version }} -o=linux -r=${{ matrix.arch.name }} -f=OpenUtau-${{ matrix.arch.name }}.zip + if: ${{ inputs.release && matrix.arch.os == 'linux' }} + + # Release + - name: Release + uses: softprops/action-gh-release@v2 with: - name: OpenUtau-${{ matrix.os.arch }} - path: OpenUtau-osx-x64.dmg - if: matrix.os.arch == 'osx-x64' + tag_name: ${{ inputs.version }} + name: ${{ env.release-name }} + prerelease: ${{ inputs.beta }} + draft: ${{ inputs.draft }} + files: | + appcast.${{ matrix.arch.name }}*.xml + OpenUtau-${{ matrix.arch.name }}.* + if: ${{ inputs.release }} diff --git a/.github/workflows/release-cleanup.yml b/.github/workflows/release-cleanup.yml index b3a8973a9..c0396f6b3 100644 --- a/.github/workflows/release-cleanup.yml +++ b/.github/workflows/release-cleanup.yml @@ -4,9 +4,8 @@ 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" ] + schedule: + - cron: '15 10 * * 3' # every Wednesday at 10:15 UTC # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/OpenUtau.Core/Audio/AudioDevice.cs b/OpenUtau.Core/Audio/AudioDevice.cs deleted file mode 100644 index 022a3a047..000000000 --- a/OpenUtau.Core/Audio/AudioDevice.cs +++ /dev/null @@ -1,29 +0,0 @@ -using OpenUtau.Audio.Bindings; - -namespace OpenUtau.Audio { - readonly struct AudioDevice { - public int DeviceIndex { get; } - public string Name { get; } - public string HostApi { get; } - public int MaxInputChannels { get; } - public int MaxOutputChannels { get; } - public double DefaultLowInputLatency { get; } - public double DefaultLowOutputLatency { get; } - public double DefaultHighInputLatency { get; } - public double DefaultHighOutputLatency { get; } - public int DefaultSampleRate { get; } - public AudioDevice(PaBinding.PaDeviceInfo device, int deviceIndex) { - var hostApi = PaBinding.GetHostApiInfo(device.hostApi); - DeviceIndex = deviceIndex; - Name = device.name; - HostApi = hostApi.name; - MaxInputChannels = device.maxInputChannels; - MaxOutputChannels = device.maxOutputChannels; - DefaultLowInputLatency = device.defaultLowInputLatency; - DefaultLowOutputLatency = device.defaultLowOutputLatency; - DefaultHighInputLatency = device.defaultHighInputLatency; - DefaultHighOutputLatency = device.defaultHighOutputLatency; - DefaultSampleRate = (int)device.defaultSampleRate; - } - } -} diff --git a/OpenUtau.Core/Audio/AudioEngine.cs b/OpenUtau.Core/Audio/AudioEngine.cs deleted file mode 100644 index ae4d4cc61..000000000 --- a/OpenUtau.Core/Audio/AudioEngine.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using OpenUtau.Audio.Bindings; - -namespace OpenUtau.Audio { - class AudioEngine { - private const int FramesPerBuffer = 0; // paFramesPerBufferUnspecified - private const PaBinding.PaStreamFlags StreamFlags = PaBinding.PaStreamFlags.paNoFlag; - public readonly int channels; - public readonly int sampleRate; - public readonly double latency; - private readonly IntPtr stream; - private bool disposed; - - public readonly AudioDevice device; - - public AudioEngine(AudioDevice device, int channels, int sampleRate, double latency) { - this.device = device; - this.channels = channels; - this.sampleRate = sampleRate; - this.latency = latency; - - var parameters = new PaBinding.PaStreamParameters { - channelCount = channels, - device = device.DeviceIndex, - hostApiSpecificStreamInfo = IntPtr.Zero, - sampleFormat = PaBinding.PaSampleFormat.paFloat32, - suggestedLatency = latency - }; - - IntPtr stream; - - unsafe { - PaBinding.PaStreamParameters tempParameters; - var parametersPtr = new IntPtr(&tempParameters); - Marshal.StructureToPtr(parameters, parametersPtr, false); - - var code = PaBinding.Pa_OpenStream( - new IntPtr(&stream), - IntPtr.Zero, - parametersPtr, - sampleRate, - FramesPerBuffer, - StreamFlags, - null, - IntPtr.Zero - ); - - PaBinding.MaybeThrow(code); - } - - this.stream = stream; - - PaBinding.MaybeThrow(PaBinding.Pa_StartStream(stream)); - } - - public void Send(Span samples) { - unsafe { - fixed (float* buffer = samples) { - var frames = samples.Length / channels; - PaBinding.Pa_WriteStream(stream, (IntPtr)buffer, frames); - } - } - } - - public void Dispose() { - if (disposed || stream == IntPtr.Zero) { - return; - } - PaBinding.Pa_AbortStream(stream); - PaBinding.Pa_CloseStream(stream); - disposed = true; - } - } -} diff --git a/OpenUtau.Core/Audio/AudioFrame.cs b/OpenUtau.Core/Audio/AudioFrame.cs deleted file mode 100644 index 622c41144..000000000 --- a/OpenUtau.Core/Audio/AudioFrame.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace OpenUtau.Audio { - public sealed class AudioFrame { - public AudioFrame(double presentationTime, float[] data) { - PresentationTime = presentationTime; - Data = data; - } - public double PresentationTime { get; } - public float[] Data { get; } - } -} diff --git a/OpenUtau.Core/Audio/AudioStreamInfo.cs b/OpenUtau.Core/Audio/AudioStreamInfo.cs deleted file mode 100644 index 57bbaf4f3..000000000 --- a/OpenUtau.Core/Audio/AudioStreamInfo.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace OpenUtau.Audio { - public readonly struct AudioStreamInfo { - public AudioStreamInfo(int channels, int sampleRate, TimeSpan duration) { - Channels = channels; - SampleRate = sampleRate; - Duration = duration; - } - public int Channels { get; } - public int SampleRate { get; } - public TimeSpan Duration { get; } - } -} diff --git a/OpenUtau.Core/Audio/Bindings/PaBinding.Delegates.cs b/OpenUtau.Core/Audio/Bindings/PaBinding.Delegates.cs deleted file mode 100644 index bb99f80e9..000000000 --- a/OpenUtau.Core/Audio/Bindings/PaBinding.Delegates.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace OpenUtau.Audio.Bindings { - internal static partial class PaBinding { - private const string dllName = "portaudio"; - - public delegate PaStreamCallbackResult PaStreamCallback( - IntPtr input, - IntPtr output, - long frameCount, - IntPtr timeInfo, - PaStreamCallbackFlags statusFlags, - IntPtr userData - ); - - [DllImport(dllName)] public static extern int Pa_Initialize(); - [DllImport(dllName)] public static extern int Pa_Terminate(); - [DllImport(dllName)] public static extern IntPtr Pa_GetVersionInfo(); - [DllImport(dllName)] public static extern IntPtr Pa_GetErrorText(int code); - [DllImport(dllName)] public static extern int Pa_GetDefaultOutputDevice(); - [DllImport(dllName)] public static extern IntPtr Pa_GetDeviceInfo(int device); - [DllImport(dllName)] public static extern int Pa_GetDeviceCount(); - [DllImport(dllName)] public static extern int Pa_GetDefaultHostApi(); - [DllImport(dllName)] public static extern IntPtr Pa_GetHostApiInfo(int device); - [DllImport(dllName)] public static extern int Pa_GetHostApiCount(); - - [DllImport(dllName)] - public static extern int Pa_IsFormatSupported( - IntPtr inputParameters, - IntPtr outputParameters, - double sampleRate); - - [DllImport(dllName)] - public static extern int Pa_OpenStream( - IntPtr stream, - IntPtr inputParameters, - IntPtr outputParameters, - double sampleRate, - long framesPerBuffer, - PaStreamFlags streamFlags, - PaStreamCallback streamCallback, - IntPtr userData); - - [DllImport(dllName)] public static extern int Pa_StartStream(IntPtr stream); - [DllImport(dllName)] public static extern int Pa_WriteStream(IntPtr stream, IntPtr buffer, long frames); - [DllImport(dllName)] public static extern int Pa_ReadStream(IntPtr stream, IntPtr buffer, long frames); - [DllImport(dllName)] public static extern int Pa_AbortStream(IntPtr stream); - [DllImport(dllName)] public static extern int Pa_CloseStream(IntPtr stream); - } -} diff --git a/OpenUtau.Core/Audio/Bindings/PaBinding.Enums.cs b/OpenUtau.Core/Audio/Bindings/PaBinding.Enums.cs deleted file mode 100644 index 7dc130f05..000000000 --- a/OpenUtau.Core/Audio/Bindings/PaBinding.Enums.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace OpenUtau.Audio.Bindings { - internal static partial class PaBinding { - public enum PaSampleFormat : long { - paFloat32 = 0x00000001, - paInt32 = 0x00000002, - paInt24 = 0x00000004, - paInt16 = 0x00000008, - paInt8 = 0x00000010, - paUInt8 = 0x00000020, - paCustomFormat = 0x00010000, - paNonInterleaved = 0x80000000, - } - - public enum PaStreamCallbackFlags : long { - paInputUnderflow = 0x00000001, - paInputOverflow = 0x00000002, - paOutputUnderflow = 0x00000004, - paOutputOverflow = 0x00000008, - paPrimingOutput = 0x00000010 - } - - public enum PaStreamCallbackResult { - paContinue = 0, - paComplete = 1, - paAbort = 2 - } - - public enum PaStreamFlags : long { - paNoFlag = 0, - paClipOff = 0x00000001, - paDitherOff = 0x00000002, - paPrimeOutputBuffersUsingStreamCallback = 0x00000008, - paPlatformSpecificFlags = 0xFFFF0000 - } - } -} diff --git a/OpenUtau.Core/Audio/Bindings/PaBinding.Structs.cs b/OpenUtau.Core/Audio/Bindings/PaBinding.Structs.cs deleted file mode 100644 index d5dd522ef..000000000 --- a/OpenUtau.Core/Audio/Bindings/PaBinding.Structs.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace OpenUtau.Audio.Bindings { - internal static partial class PaBinding { - [StructLayout(LayoutKind.Sequential)] - public readonly struct PaVersionInfo { - public readonly int versionMajor; - public readonly int versionMinor; - public readonly int versionSubMinor; - - [MarshalAs(UnmanagedType.LPStr)] - public readonly string versionControlRevision; - - [MarshalAs(UnmanagedType.LPStr)] - public readonly string verionText; - } - - [StructLayout(LayoutKind.Sequential)] - public struct PaStreamParameters { - public int device; - public int channelCount; - public PaSampleFormat sampleFormat; - public double suggestedLatency; - public IntPtr hostApiSpecificStreamInfo; - } - - [StructLayout(LayoutKind.Sequential)] - public readonly struct PaDeviceInfo { - public readonly int structVersion; - - [MarshalAs(UnmanagedType.LPUTF8Str)] - public readonly string name; - - public readonly int hostApi; - public readonly int maxInputChannels; - public readonly int maxOutputChannels; - public readonly double defaultLowInputLatency; - public readonly double defaultLowOutputLatency; - public readonly double defaultHighInputLatency; - public readonly double defaultHighOutputLatency; - public readonly double defaultSampleRate; - } - - [StructLayout(LayoutKind.Sequential)] - public readonly struct PaHostApiInfo { - public readonly int structVersion; - - public readonly int type; - - [MarshalAs(UnmanagedType.LPStr)] - public readonly string name; - - public readonly int deviceCount; - public readonly int defaultInputDevice; - public readonly int defaultOutputDevice; - } - } -} diff --git a/OpenUtau.Core/Audio/Bindings/PaBinding.cs b/OpenUtau.Core/Audio/Bindings/PaBinding.cs deleted file mode 100644 index 502498a70..000000000 --- a/OpenUtau.Core/Audio/Bindings/PaBinding.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace OpenUtau.Audio.Bindings { - internal static partial class PaBinding { - public static string GetErrorText(int code) => Marshal.PtrToStringAnsi(Pa_GetErrorText(code)); - - public static void MaybeThrow(int code) { - if (code >= 0) { - return; - } - throw new Exception(Marshal.PtrToStringAnsi(Pa_GetErrorText(code))); - } - - public static PaDeviceInfo GetDeviceInfo(int device) => Marshal.PtrToStructure(Pa_GetDeviceInfo(device)); - public static PaHostApiInfo GetHostApiInfo(int hostApi) => Marshal.PtrToStructure(Pa_GetHostApiInfo(hostApi)); - } -} diff --git a/OpenUtau.Core/Audio/IAudioOutput.cs b/OpenUtau.Core/Audio/IAudioOutput.cs index e4e08ecd7..1b46aa23d 100644 --- a/OpenUtau.Core/Audio/IAudioOutput.cs +++ b/OpenUtau.Core/Audio/IAudioOutput.cs @@ -8,7 +8,6 @@ public class AudioOutputDevice { public string api; public int deviceNumber; public Guid guid; - public object data; public override string ToString() => $"[{api}] {name}"; } diff --git a/OpenUtau.Core/Audio/MiniAudioOutput.cs b/OpenUtau.Core/Audio/MiniAudioOutput.cs new file mode 100644 index 000000000..feb50d0e2 --- /dev/null +++ b/OpenUtau.Core/Audio/MiniAudioOutput.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using NAudio.Wave; +using NAudio.Wave.SampleProviders; +using OpenUtau.Core.Util; +using Serilog; + +namespace OpenUtau.Audio { + public class MiniAudioOutput : IAudioOutput, IDisposable { + const int channels = 2; + const int sampleRate = 44100; + + public PlaybackState PlaybackState { get; private set; } + public int DeviceNumber { get; private set; } + + + private ISampleProvider? sampleProvider; + private double currentTimeMs; + private bool eof; + + private List devices = new List(); + private IntPtr callbackPtr = IntPtr.Zero; + private IntPtr nativeContext = IntPtr.Zero; + private Guid selectedDevice = Guid.Empty; + + public MiniAudioOutput() { + UpdateDeviceList(); + unsafe { + var f = (ou_audio_data_callback_t)DataCallback; + GCHandle.Alloc(f); + callbackPtr = Marshal.GetFunctionPointerForDelegate(f); + } + if (Guid.TryParse(Preferences.Default.PlaybackDevice, out var guid)) { + SelectDevice(guid, Preferences.Default.PlaybackDeviceNumber); + } else { + bool foundDevice = false; + foreach (AudioOutputDevice dev in devices) { + try { + SelectDevice(dev.guid, dev.deviceNumber); + foundDevice = true; + break; + } catch (Exception e) { + Log.Warning(e, $"Failed to init audio device {dev}"); + } + } + if (!foundDevice) { + throw new Exception("Failed to init any audio device"); + } + } + } + + private void UpdateDeviceList() { + devices.Clear(); + unsafe { + const int kMaxCount = 128; + ou_audio_device_info_t* device_infos = stackalloc ou_audio_device_info_t[kMaxCount]; + int count = ou_get_audio_device_infos(device_infos, kMaxCount); + if (count == 0) { + throw new Exception("Failed to get any audio device info"); + } + if (count > kMaxCount) { + Log.Warning($"More than {kMaxCount} audio devices found, only the first {kMaxCount} will be listed."); + count = kMaxCount; + } + for (int i = 0; i < count; i++) { + var guidData = new byte[16]; + fixed (byte* guidPtr = guidData) { + *(ulong*)guidPtr = device_infos[i].api_id; + *(ulong*)(guidPtr + 8) = device_infos[i].id; + } + string api = Marshal.PtrToStringUTF8(device_infos[i].api); // Should be ascii. + string name = (OS.IsWindows() && api != "WASAPI") + ? Marshal.PtrToStringAnsi(device_infos[i].name) + : Marshal.PtrToStringUTF8(device_infos[i].name); + devices.Add(new AudioOutputDevice { + name = name, + api = api, + deviceNumber = i, + guid = new Guid(guidData), + }); + } + ou_free_audio_device_infos(device_infos, count); + } + } + + public void Init(ISampleProvider sampleProvider) { + PlaybackState = PlaybackState.Stopped; + eof = false; + currentTimeMs = 0; + if (sampleRate != sampleProvider.WaveFormat.SampleRate) { + sampleProvider = new WdlResamplingSampleProvider(sampleProvider, sampleRate); + } + this.sampleProvider = sampleProvider.ToStereo(); + } + + public void Play() { + if (PlaybackState != PlaybackState.Playing) { + CheckError(ou_audio_device_start(nativeContext)); + } + PlaybackState = PlaybackState.Playing; + currentTimeMs = 0; + eof = false; + } + + public void Pause() { + if (PlaybackState == PlaybackState.Playing) { + CheckError(ou_audio_device_stop(nativeContext)); + } + PlaybackState = PlaybackState.Paused; + } + + public void Stop() { + if (PlaybackState == PlaybackState.Playing) { + CheckError(ou_audio_device_stop(nativeContext)); + } + PlaybackState = PlaybackState.Stopped; + } + + float[] temp = new float[0]; + private unsafe void DataCallback(float* buffer, uint channels, uint frame_count) { + int samples = (int)(channels * frame_count); + if (temp.Length < samples) { + temp = new float[samples]; + } + int n = 0; + if (sampleProvider != null) { + n = sampleProvider.Read(temp, 0, samples); + } + if (n < samples) { + Array.Fill(temp, 0, n, samples - n); + } + if (n == 0) { + eof = true; + } + Marshal.Copy(temp, 0, (IntPtr)buffer, samples); + currentTimeMs += n / channels * 1000.0 / sampleRate; + } + + public long GetPosition() { + if (eof && PlaybackState == PlaybackState.Playing) { + Stop(); + } + return (long)(Math.Max(0, currentTimeMs) / 1000 * sampleRate * 2 /* 16 bit */ * channels); + } + + public void SelectDevice(Guid guid, int deviceNumber) { + if (selectedDevice != Guid.Empty && selectedDevice == guid) { + return; + } + if (nativeContext != IntPtr.Zero) { + CheckError(ou_free_audio_device(nativeContext)); + nativeContext = IntPtr.Zero; + selectedDevice = Guid.Empty; + } + for (int i = 0; i < devices.Count; i++) { + if (devices[i].guid == guid) { + deviceNumber = i; + break; + } + if (i == devices.Count - 1) { + guid = devices[0].guid; + deviceNumber = devices[0].deviceNumber; + } + } + uint api_id; + ulong id; + unsafe { + fixed (byte* guidPtr = guid.ToByteArray()) { + api_id = (uint)*(ulong*)guidPtr; + id = *(ulong*)(guidPtr + 8); + } + } + unsafe { + nativeContext = ou_init_audio_device(api_id, id, callbackPtr); + if (nativeContext == IntPtr.Zero) { + throw new Exception("Failed to init audio device"); + } + } + selectedDevice = guid; + DeviceNumber = deviceNumber; + if (Preferences.Default.PlaybackDevice != guid.ToString()) { + Preferences.Default.PlaybackDevice = guid.ToString(); + Preferences.Default.PlaybackDeviceNumber = deviceNumber; + Preferences.Save(); + } + } + + public List GetOutputDevices() { + return devices; + } + + #region binding + + [StructLayout(LayoutKind.Sequential)] + private unsafe struct ou_audio_device_info_t { + public IntPtr name; + public ulong id; + public IntPtr api; + public uint api_id; + } + + [UnmanagedFunctionPointer(callingConvention: CallingConvention.Cdecl)] + private unsafe delegate void ou_audio_data_callback_t(float* buffer, uint channels, uint frame_count); + + [DllImport("worldline")] private static extern unsafe int ou_get_audio_device_infos(ou_audio_device_info_t* device_infos, int max_count); + [DllImport("worldline")] private static extern unsafe void ou_free_audio_device_infos(ou_audio_device_info_t* device_infos, int count); + [DllImport("worldline")] private static extern IntPtr ou_init_audio_device(uint api_id, ulong id, IntPtr callback); + [DllImport("worldline")] private static extern int ou_free_audio_device(IntPtr context); + [DllImport("worldline")] private static extern int ou_audio_device_start(IntPtr context); + [DllImport("worldline")] private static extern int ou_audio_device_stop(IntPtr context); + [DllImport("worldline")] private static extern IntPtr ou_audio_get_error_message(int error_code); + + private static void CheckError(int errorCode) { + if (errorCode == 0) { + return; + } + IntPtr ptr = ou_audio_get_error_message(errorCode); + throw new Exception(Marshal.PtrToStringUTF8(ptr)); + } + + #endregion + + #region disposable + + private bool disposedValue; + + protected virtual void Dispose(bool disposing) { + if (!disposedValue) { + if (disposing) { + // dispose managed state (managed objects) + } + + // free unmanaged resources (unmanaged objects) and override finalizer + if (nativeContext != IntPtr.Zero) { + ou_free_audio_device(nativeContext); + nativeContext = IntPtr.Zero; + } + + // set large fields to null + + disposedValue = true; + } + } + + ~MiniAudioOutput() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: false); + } + + public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/OpenUtau.Core/Audio/PortAudioOutput.cs b/OpenUtau.Core/Audio/PortAudioOutput.cs deleted file mode 100644 index a8bd81ffa..000000000 --- a/OpenUtau.Core/Audio/PortAudioOutput.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading; -using NAudio.Wave; -using NAudio.Wave.SampleProviders; -using OpenUtau.Audio.Bindings; -using OpenUtau.Core; -using OpenUtau.Core.Util; -using Serilog; - -namespace OpenUtau.Audio { - public class PortAudioOutput : IAudioOutput, IDisposable { - const int Channels = 2; - - public PlaybackState PlaybackState { get; private set; } - public int DeviceNumber { get; private set; } - - private readonly object lockObj = new object(); - private ConcurrentQueue queue = new ConcurrentQueue(); - private AudioEngine audioEngine; - private ISampleProvider sampleProvider; - private float[] buffer; - private double bufferedTimeMs; - private double currentTimeMs; - private Thread pushThread; - private Thread pullThread; - private bool eof; - private bool shutdown; - private bool disposed; - - public PortAudioOutput() { - PaBinding.Pa_Initialize(); - - buffer = new float[0]; - try { - if (Preferences.Default.PlaybackDeviceIndex != null) { - try { - SelectDevice(new Guid(), Preferences.Default.PlaybackDeviceIndex.Value); - } catch { - SelectDevice(new Guid(), PaBinding.Pa_GetDefaultOutputDevice()); - throw; - } - } else { - SelectDevice(new Guid(), PaBinding.Pa_GetDefaultOutputDevice()); - } - } catch (Exception e) { - Log.Error(e, "Failed to initialize audio device"); - } - - pullThread = new Thread(Pull) { IsBackground = true, Priority = ThreadPriority.Highest }; - pushThread = new Thread(Push) { IsBackground = true, Priority = ThreadPriority.Highest }; - pullThread.Start(); - pushThread.Start(); - } - - public List GetOutputDevices() { - List devices = new List(); - int count = PaBinding.Pa_GetDeviceCount(); - PaBinding.MaybeThrow(count); - for (int i = 0; i < count; ++i) { - var device = GetEligibleOutputDevice(i); - if (device is AudioDevice dev) { - devices.Add(new AudioOutputDevice() { - api = dev.HostApi, - name = dev.Name, - deviceNumber = dev.DeviceIndex, - guid = new Guid(), - }); - } - } - return devices; - } - - public long GetPosition() { - var latency = audioEngine?.latency ?? 0; - var sampleRate = audioEngine?.sampleRate ?? 44100; - return (long)(Math.Max(0, currentTimeMs - latency) / 1000 * sampleRate * 2 /* currently assumes 16 bit */ * Channels); - } - - public void Init(ISampleProvider sampleProvider) { - PlaybackState = PlaybackState.Stopped; - eof = false; - queue.Clear(); - bufferedTimeMs = 0; - currentTimeMs = 0; - var sampleRate = audioEngine?.sampleRate ?? 44100; - if (sampleRate != sampleProvider.WaveFormat.SampleRate) { - sampleProvider = new WdlResamplingSampleProvider(sampleProvider, sampleRate); - } - this.sampleProvider = sampleProvider.ToStereo(); - } - - public void Pause() { - PlaybackState = PlaybackState.Paused; - } - - public void Play() { - eof = false; - queue.Clear(); - currentTimeMs = 0; - PlaybackState = PlaybackState.Playing; - } - - public void SelectDevice(Guid guid, int deviceNumber) { - lock (lockObj) { - if (audioEngine == null || audioEngine.device.DeviceIndex != deviceNumber) { - var device = GetEligibleOutputDevice(deviceNumber); - if (device is AudioDevice dev) { - audioEngine?.Dispose(); - double latency = dev.DefaultHighOutputLatency; - if (OS.IsWindows() && latency < 0.1) { - latency = 0.1; - } - audioEngine = new AudioEngine( - dev, - Channels, - dev.DefaultSampleRate, - latency); - DeviceNumber = deviceNumber; - buffer = new float[dev.DefaultSampleRate * Channels * 10 / 1000]; // 10ms at 44.1kHz - } - Preferences.Default.PlaybackDeviceIndex = DeviceNumber; - Preferences.Save(); - } - } - } - - private AudioDevice? GetEligibleOutputDevice(int index) { - var device = new AudioDevice(PaBinding.GetDeviceInfo(index), index); - if (device.MaxOutputChannels < Channels) { - return null; - } - var api = device.HostApi.ToLowerInvariant(); - if (api.Contains("wasapi") || api.Contains("wdm-ks")) { - return null; - } - var parameters = new PaBinding.PaStreamParameters { - channelCount = Channels, - device = device.DeviceIndex, - hostApiSpecificStreamInfo = IntPtr.Zero, - sampleFormat = PaBinding.PaSampleFormat.paFloat32, - }; - unsafe { - int code = PaBinding.Pa_IsFormatSupported(IntPtr.Zero, new IntPtr(¶meters), 44100); - if (code < 0) { - return null; - } - } - return device; - } - - public void Stop() { - PlaybackState = PlaybackState.Stopped; - bufferedTimeMs = 0; - currentTimeMs = 0; - sampleProvider = null; - queue.Clear(); - } - - private void Push() { - while (!shutdown) { - if (PlaybackState == PlaybackState.Paused || - PlaybackState == PlaybackState.Stopped) { - Thread.Sleep(10); - continue; - } - - AudioEngine engine = audioEngine; - if (engine == null) { - Thread.Sleep(10); - continue; - } - - if (queue.Count == 0) { - if (eof) { - PlaybackState = PlaybackState.Stopped; - Thread.Sleep(10); - continue; - } - Thread.Sleep(10); - continue; - } - - if (!queue.TryDequeue(out var frame)) { - Thread.Sleep(10); - continue; - } - - if (PlaybackState != PlaybackState.Playing) { - PlaybackState = PlaybackState.Playing; - } - engine.Send(frame.Data); - currentTimeMs = frame.PresentationTime; - } - } - - private void Pull() { - while (!shutdown) { - var sp = sampleProvider; - if (sp == null) { - Thread.Sleep(10); - continue; - } - if (PlaybackState == PlaybackState.Paused || - PlaybackState == PlaybackState.Stopped) { - Thread.Sleep(10); - continue; - } - if (queue.Count >= 10) { - Thread.Sleep(10); - continue; - } - - var n = sp.Read(buffer, 0, buffer.Length); - if (n == 0) { - eof = true; - Thread.Sleep(10); - continue; - } - var data = new float[n]; - Array.Copy(buffer, data, n); - var frame = new AudioFrame(bufferedTimeMs, data); - queue.Enqueue(frame); - var sampleRate = audioEngine?.sampleRate ?? 44100; - bufferedTimeMs += n * 1000.0 / sampleRate / Channels; - } - } - - public void Dispose() { - if (disposed) { - return; - } - - PlaybackState = PlaybackState.Stopped; - shutdown = true; - - if (pushThread != null) { - while (pushThread.IsAlive) { - Thread.Sleep(10); - } - pushThread = null; - } - if (pullThread != null) { - while (pullThread.IsAlive) { - Thread.Sleep(10); - } - pullThread = null; - } - queue.Clear(); - - GC.SuppressFinalize(this); - - disposed = true; - } - } -} diff --git a/OpenUtau.Core/BaseChinesePhonemizer.cs b/OpenUtau.Core/BaseChinesePhonemizer.cs index 4be4b2235..628775036 100644 --- a/OpenUtau.Core/BaseChinesePhonemizer.cs +++ b/OpenUtau.Core/BaseChinesePhonemizer.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; +using IKg2p; using OpenUtau.Api; -using OpenUtau.Core.G2p; using OpenUtau.Core.Ustx; namespace OpenUtau.Core { @@ -24,12 +24,13 @@ public static string[] Romanize(IEnumerable lyrics) { var hanziLyrics = lyricsArray .Where(ZhG2p.MandarinInstance.IsHanzi) .ToList(); - var pinyinResult = ZhG2p.MandarinInstance.Convert(hanziLyrics, false, false).ToLower().Split(); - if(pinyinResult == null) { + List g2pResults = ZhG2p.MandarinInstance.Convert(lyrics.ToList(), false, false); + var pinyinResult = g2pResults.Select(res => res.syllable).ToArray(); + if (pinyinResult == null) { return lyricsArray; } var pinyinIndex = 0; - for(int i=0; i < lyricsArray.Length; i++) { + for (int i = 0; i < lyricsArray.Length; i++) { if (lyricsArray[i].Length == 1 && ZhG2p.MandarinInstance.IsHanzi(lyricsArray[i])) { lyricsArray[i] = pinyinResult[pinyinIndex]; pinyinIndex++; diff --git a/OpenUtau.Core/DiffSinger/Phonemizers/DiffSingerJyutpingPhonemizer.cs b/OpenUtau.Core/DiffSinger/Phonemizers/DiffSingerJyutpingPhonemizer.cs index 0e84b153b..d7e9c9c8c 100644 --- a/OpenUtau.Core/DiffSinger/Phonemizers/DiffSingerJyutpingPhonemizer.cs +++ b/OpenUtau.Core/DiffSinger/Phonemizers/DiffSingerJyutpingPhonemizer.cs @@ -1,14 +1,15 @@ using System.Collections.Generic; -using OpenUtau.Api; -using OpenUtau.Core.G2p; using System.Linq; +using IKg2p; +using OpenUtau.Api; namespace OpenUtau.Core.DiffSinger { [Phonemizer("DiffSinger Jyutping Phonemizer", "DIFFS ZH-YUE", language: "ZH-YUE")] public class DiffSingerJyutpingPhonemizer : DiffSingerBasePhonemizer { protected override string GetDictionaryName() => "dsdict-zh-yue.yaml"; protected override string[] Romanize(IEnumerable lyrics) { - return ZhG2p.CantoneseInstance.Convert(lyrics.ToList(), false, true).Split(" "); + List g2pResults = ZhG2p.CantoneseInstance.Convert(lyrics.ToList(), false, false); + return g2pResults.Select(res => res.syllable).ToArray(); } } } diff --git a/OpenUtau.Core/Editing/ResetBatchEdits.cs b/OpenUtau.Core/Editing/ResetBatchEdits.cs index 7b0fc9dd8..92c04bb70 100644 --- a/OpenUtau.Core/Editing/ResetBatchEdits.cs +++ b/OpenUtau.Core/Editing/ResetBatchEdits.cs @@ -193,4 +193,49 @@ public void Run(UProject project, UVoicePart part, List selectedNotes, Do docManager.EndUndoGroup(); } } + + public class ResetAll : BatchEdit { + public virtual string Name => name; + + private string name; + + public ResetAll() { + name = "pianoroll.menu.notes.reset.all"; + } + + public void Run(UProject project, UVoicePart part, List selectedNotes, DocManager docManager) { + var notes = selectedNotes.Count > 0 ? selectedNotes : part.notes.ToList(); + docManager.StartUndoGroup(true); + foreach (var note in notes) { + // pitch points + docManager.ExecuteCmd(new ResetPitchPointsCommand(part, note)); + // expressions + if (note.phonemeExpressions.Count > 0) { + docManager.ExecuteCmd(new ResetExpressionsCommand(part, note)); + } + // vibrato + if (note.vibrato.length > 0) { + docManager.ExecuteCmd(new VibratoLengthCommand(part, note, 0)); + } + // timings + bool shouldClear = false; + foreach (var o in note.phonemeOverrides) { + if (o.offset != null || o.preutterDelta != null || o.overlapDelta != null) { + shouldClear = true; + break; + } + } + if (shouldClear) { + docManager.ExecuteCmd(new ClearPhonemeTimingCommand(part, note)); + } + // aliases + foreach (var o in note.phonemeOverrides) { + if (o.phoneme != null) { + docManager.ExecuteCmd(new ChangePhonemeAliasCommand(part, note, o.index, null)); + } + } + } + docManager.EndUndoGroup(); + } + } } diff --git a/OpenUtau.Core/G2p/Data/Resources.Designer.cs b/OpenUtau.Core/G2p/Data/Resources.Designer.cs index 97165a05b..2a8992d4b 100644 --- a/OpenUtau.Core/G2p/Data/Resources.Designer.cs +++ b/OpenUtau.Core/G2p/Data/Resources.Designer.cs @@ -1,10 +1,10 @@ //------------------------------------------------------------------------------ // -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 // -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 // //------------------------------------------------------------------------------ @@ -13,12 +13,12 @@ namespace OpenUtau.Core.G2p.Data { /// - /// 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. + // 此类是由 StronglyTypedResourceBuilder + // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 + // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen + // (以 /str 作为命令选项),或重新生成 VS 项目。 [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] @@ -33,7 +33,7 @@ internal Resources() { } /// - /// Returns the cached ResourceManager instance used by this class. + /// 返回此类使用的缓存的 ResourceManager 实例。 /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { @@ -47,8 +47,8 @@ internal Resources() { } /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. + /// 重写当前线程的 CurrentUICulture 属性,对 + /// 使用此强类型资源类的所有资源查找执行重写。 /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { @@ -61,7 +61,7 @@ internal Resources() { } /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_arpabet { get { @@ -71,7 +71,7 @@ internal static byte[] g2p_arpabet { } /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_arpabet_plus { get { @@ -81,7 +81,7 @@ internal static byte[] g2p_arpabet_plus { } /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_de { get { @@ -91,7 +91,7 @@ internal static byte[] g2p_de { } /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_es { get { @@ -101,7 +101,7 @@ internal static byte[] g2p_es { } /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_fr { get { @@ -111,7 +111,7 @@ internal static byte[] g2p_fr { } /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_it { get { @@ -121,7 +121,7 @@ internal static byte[] g2p_it { } /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_ja_mono { get { @@ -131,17 +131,7 @@ internal static byte[] g2p_ja_mono { } /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] g2p_jyutping { - get { - object obj = ResourceManager.GetObject("g2p-jyutping", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_ko { get { @@ -151,17 +141,7 @@ internal static byte[] g2p_ko { } /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] g2p_man { - get { - object obj = ResourceManager.GetObject("g2p-man", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_pt { get { @@ -171,7 +151,7 @@ internal static byte[] g2p_pt { } /// - /// Looks up a localized resource of type System.Byte[]. + /// 查找 System.Byte[] 类型的本地化资源。 /// internal static byte[] g2p_ru { get { diff --git a/OpenUtau.Core/G2p/Data/Resources.resx b/OpenUtau.Core/G2p/Data/Resources.resx index c21c5752d..ab22ee92c 100644 --- a/OpenUtau.Core/G2p/Data/Resources.resx +++ b/OpenUtau.Core/G2p/Data/Resources.resx @@ -136,15 +136,9 @@ g2p-ja-mono.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - g2p-jyutping.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - g2p-ko.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - g2p-man.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - g2p-pt.zip;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/OpenUtau.Core/G2p/Data/g2p-ja-mono.zip b/OpenUtau.Core/G2p/Data/g2p-ja-mono.zip index 09070157b..92ff62fd1 100644 Binary files a/OpenUtau.Core/G2p/Data/g2p-ja-mono.zip and b/OpenUtau.Core/G2p/Data/g2p-ja-mono.zip differ diff --git a/OpenUtau.Core/G2p/Data/g2p-jyutping.zip b/OpenUtau.Core/G2p/Data/g2p-jyutping.zip deleted file mode 100644 index 395d3db88..000000000 Binary files a/OpenUtau.Core/G2p/Data/g2p-jyutping.zip and /dev/null differ diff --git a/OpenUtau.Core/G2p/Data/g2p-man.zip b/OpenUtau.Core/G2p/Data/g2p-man.zip deleted file mode 100644 index 049f83a09..000000000 Binary files a/OpenUtau.Core/G2p/Data/g2p-man.zip and /dev/null differ diff --git a/OpenUtau.Core/G2p/JapaneseMonophoneG2p.cs b/OpenUtau.Core/G2p/JapaneseMonophoneG2p.cs index d77b306c4..222245127 100644 --- a/OpenUtau.Core/G2p/JapaneseMonophoneG2p.cs +++ b/OpenUtau.Core/G2p/JapaneseMonophoneG2p.cs @@ -30,9 +30,9 @@ public class JapaneseMonophoneG2p : G2pPack { private static readonly string[] phonemes = new string[] { "", "", "", "", "A", "AP", "E", "I", "N", "O", "U", - "SP", "a", "b", "ch", "cl", "d", "dy", "e", "f", "g", "gw", + "SP", "a", "b", "by", "ch", "cl", "d", "dy", "e", "f", "g", "gw", "gy", "h", "hy", "i", "j", "k", "kw", "ky", "m", "my", "n", - "ng", "ny", "o", "p", "py", "r", "ry", "s", "sh", "t", "ts", + "ng", "ngy", "ny", "o", "p", "py", "r", "ry", "s", "sh", "t", "ts", "ty", "u", "v", "w", "y", "z" }; diff --git a/OpenUtau.Core/G2p/ZhG2p.cs b/OpenUtau.Core/G2p/ZhG2p.cs deleted file mode 100644 index 4c0304169..000000000 --- a/OpenUtau.Core/G2p/ZhG2p.cs +++ /dev/null @@ -1,226 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using OpenUtau.Core.Util; - -namespace OpenUtau.Core.G2p { - public class ZhG2p { - private static Lazy mandarinInstance = new Lazy(() => new ZhG2p("mandarin")); - private static Lazy cantoneseInstance = new Lazy(() => new ZhG2p("cantonese")); - - public static ZhG2p MandarinInstance => mandarinInstance.Value; - public static ZhG2p CantoneseInstance => cantoneseInstance.Value; - - private Dictionary PhrasesMap = new Dictionary(); - private Dictionary TransDict = new Dictionary(); - private Dictionary WordDict = new Dictionary(); - private Dictionary PhrasesDict = new Dictionary(); - - private ZhG2p(string language) { - byte[] data; - if (language == "mandarin") { - data = Data.Resources.g2p_man; - } else { - data = Data.Resources.g2p_jyutping; - } - - LoadDict(data, "phrases_map.txt", PhrasesMap); - LoadDict(data, "phrases_dict.txt", PhrasesDict); - LoadDict(data, "user_dict.txt", PhrasesDict); - LoadDict(data, "word.txt", WordDict); - LoadDict(data, "trans_word.txt", TransDict); - } - - public static bool LoadDict(byte[] data, string fileName, Dictionary resultMap) { - string[] content = Zip.ExtractText(data, fileName); - - content.Select(line => line.Trim()) - .Select(line => line.Split(":")) - .Where(parts => parts.Length == 2) - .ToList() - .ForEach(parts => resultMap[parts[0]] = parts[1]); - - return true; - } - - private static readonly Dictionary NumMap = new Dictionary - { - {"0", "零"}, - {"1", "一"}, - {"2", "二"}, - {"3", "三"}, - {"4", "四"}, - {"5", "五"}, - {"6", "六"}, - {"7", "七"}, - {"8", "八"}, - {"9", "九"} - }; - - static List SplitString(string input) { - List res = new List(); - - // 正则表达式模式 - string pattern = @"(?![ー゜])([a-zA-Z]+|[+-]|[0-9]|[\u4e00-\u9fa5]|[\u3040-\u309F\u30A0-\u30FF][ャュョゃゅょァィゥェォぁぃぅぇぉ]?)"; - - // 使用正则表达式匹配 - MatchCollection matches = Regex.Matches(input, pattern); - - foreach (Match match in matches) { - res.Add(match.Value); - } - - return res; - } - - private static string ResetZH(List input, List res, List positions) { - var result = input; - for (var i = 0; i < positions.Count; i++) { - result[positions[i]] = res[i]; - } - - return string.Join(" ", result); - } - - private static void AddString(string text, List res) { - var temp = text.Split(' '); - res.AddRange(temp); - } - - private static void RemoveElements(List list, int start, int n) { - if (start >= 0 && start < list.Count && n > 0) { - int countToRemove = Math.Min(n, list.Count - start); - list.RemoveRange(start, countToRemove); - } - } - - private void ZhPosition(List input, List res, List positions, bool convertNum) { - for (int i = 0; i < input.Count; i++) { - if (WordDict.ContainsKey(input[i]) || TransDict.ContainsKey(input[i])) { - res.Add(input[i]); - positions.Add(i); - } else if (convertNum && NumMap.ContainsKey(input[i])) { - res.Add(NumMap[input[i]]); - positions.Add(i); - } - } - } - - public bool IsHanzi(string input){ - /// - /// Whether the input string is a single hanzi. - /// - return WordDict.ContainsKey(input) || TransDict.ContainsKey(input); - } - - public bool IsHanziOrNum(string input){ - return IsHanzi(input) || NumMap.ContainsKey(input); - } - - public bool IsHanzi(string input, bool convertNum){ - return IsHanzi(input) || (convertNum && NumMap.ContainsKey(input)); - } - - public string Convert(string input, bool tone, bool covertNum) { - return Convert(SplitString(input), tone, covertNum); - } - - public string Convert(List input, bool tone, bool convertNum) { - var inputList = new List(); - var inputPos = new List(); - - ZhPosition(input, inputList, inputPos, convertNum); - var result = new List(); - var cursor = 0; - - while (cursor < inputList.Count) { - var rawCurrentChar = inputList[cursor]; - var currentChar = TradToSim(rawCurrentChar); - - if (!WordDict.ContainsKey(currentChar)) { - result.Add(currentChar); - cursor++; - continue; - } - - if (!IsPolyphonic(currentChar)) { - result.Add(GetDefaultPinyin(currentChar)); - cursor++; - } else { - var found = false; - for (var length = 4; length >= 2 && !found; length--) { - if (cursor + length <= inputList.Count) { - // cursor: 地, subPhrase: 地久天长 - var subPhrase = string.Join("", inputList.GetRange(cursor, length)); - if (PhrasesDict.ContainsKey(subPhrase)) { - AddString(PhrasesDict[subPhrase], result); - cursor += length; - found = true; - } - - if (cursor >= 1 && !found) { - // cursor: 重, subPhrase_1: 语重心长 - var subPhrase_1 = string.Join("", inputList.GetRange(cursor - 1, length)); - if (PhrasesDict.ContainsKey(subPhrase_1)) { - result.RemoveAt(result.Count - 1); - AddString(PhrasesDict[subPhrase_1], result); - cursor += length - 1; - found = true; - } - } - } - - if (cursor + 1 - length >= 0 && !found && cursor + 1 <= inputList.Count) { - // cursor: 好, xSubPhrase: 各有所好 - var xSubPhrase = string.Join("", inputList.GetRange(cursor + 1 - length, length)); - if (PhrasesDict.ContainsKey(xSubPhrase)) { - RemoveElements(result, cursor + 1 - length, length - 1); - AddString(PhrasesDict[xSubPhrase], result); - cursor += 1; - found = true; - } - } - - if (cursor + 2 - length >= 0 && cursor + 2 <= inputList.Count && !found) { - // cursor: 好, xSubPhrase: 叶公好龙 - var xSubPhrase_1 = string.Join("", inputList.GetRange(cursor + 2 - length, length)); - if (PhrasesDict.ContainsKey(xSubPhrase_1)) { - RemoveElements(result, cursor + 2 - length, length - 2); - AddString(PhrasesDict[xSubPhrase_1], result); - cursor += 2; - found = true; - } - } - } - - if (!found) { - result.Add(GetDefaultPinyin(currentChar)); - cursor++; - } - } - } - - if (!tone) { - for (var i = 0; i < result.Count; i++) { - result[i] = System.Text.RegularExpressions.Regex.Replace(result[i], "[^a-z]", ""); - } - } - - return ResetZH(input, result, inputPos); - } - - bool IsPolyphonic(string text) { - return PhrasesMap.ContainsKey(text); - } - - string TradToSim(string text) { - return TransDict.ContainsKey(text) ? TransDict[text] : text; - } - - string GetDefaultPinyin(string text) { - return WordDict.ContainsKey(text) ? WordDict[text] : text; - } - - } -} diff --git a/OpenUtau.Core/OpenUtau.Core.csproj b/OpenUtau.Core/OpenUtau.Core.csproj index 49d9f7e81..d02682e84 100644 --- a/OpenUtau.Core/OpenUtau.Core.csproj +++ b/OpenUtau.Core/OpenUtau.Core.csproj @@ -11,6 +11,7 @@ + diff --git a/OpenUtau.Core/Util/LyricsHelper.cs b/OpenUtau.Core/Util/LyricsHelper.cs index 53cbb9e85..f4b9e75b7 100644 --- a/OpenUtau.Core/Util/LyricsHelper.cs +++ b/OpenUtau.Core/Util/LyricsHelper.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using IKg2p; using OpenUtau.Api; using OpenUtau.Core.G2p; @@ -57,14 +58,16 @@ public string Convert(string text) { public class PinyinLyricsHelper : ILyricsHelper { public string Source => "汉->han"; public string Convert(string lyric) { - return ZhG2p.MandarinInstance.Convert(lyric, false, true); + List g2pResults = ZhG2p.MandarinInstance.Convert(lyric, false, true); + return g2pResults.Select(res => res.syllable).ToArray()[0]; } } public class JyutpingLyricsHelper : ILyricsHelper { public string Source => "粤->jyut"; public string Convert(string lyric) { - return ZhG2p.CantoneseInstance.Convert(lyric, false, true); + List g2pResults = ZhG2p.CantoneseInstance.Convert(lyric, false, true); + return g2pResults.Select(res => res.syllable).ToArray()[0]; } } diff --git a/OpenUtau.Core/Util/OS.cs b/OpenUtau.Core/Util/OS.cs index a432ce589..7bd2b47e0 100644 --- a/OpenUtau.Core/Util/OS.cs +++ b/OpenUtau.Core/Util/OS.cs @@ -57,10 +57,19 @@ public static string GetUpdaterRid() { if (RuntimeInformation.ProcessArchitecture == Architecture.X86) { return "win-x86"; } + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { + return "win-arm64"; + } return "win-x64"; } else if (IsMacOS()) { + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { + return "osx-arm64"; + } return "osx-x64"; } else if (IsLinux()) { + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) { + return "linux-arm64"; + } return "linux-x64"; } throw new NotSupportedException(); diff --git a/OpenUtau.Core/Util/Yaml.cs b/OpenUtau.Core/Util/Yaml.cs index 143b210b3..9502b4bfc 100644 --- a/OpenUtau.Core/Util/Yaml.cs +++ b/OpenUtau.Core/Util/Yaml.cs @@ -1,4 +1,4 @@ -using OpenUtau.Classic; +using System.IO; using OpenUtau.Core.Ustx; using YamlDotNet.Core; using YamlDotNet.Core.Events; @@ -7,8 +7,13 @@ using YamlDotNet.Serialization.NamingConventions; namespace OpenUtau.Core { - public static class Yaml { - public static ISerializer DefaultSerializer = new SerializerBuilder() + public class Yaml { + public static Yaml DefaultSerializer => instance; + public static Yaml DefaultDeserializer => instance; + + private static readonly Yaml instance = new Yaml(); + + private readonly ISerializer serializer = new SerializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) .WithEventEmitter(next => new FlowEmitter(next)) @@ -16,10 +21,37 @@ public static class Yaml { .WithQuotingNecessaryStrings() .Build(); - public static IDeserializer DefaultDeserializer = new DeserializerBuilder() + private readonly IDeserializer deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); + + private readonly object serializerLock = new object(); + private readonly object deserializerLock = new object(); + + public string Serialize(object? graph) { + lock (serializerLock) { + return serializer.Serialize(graph); + } + } + + public void Serialize(TextWriter writer, object? graph) { + lock (serializerLock) { + serializer.Serialize(writer, graph); + } + } + + public T Deserialize(string input) { + lock (deserializerLock) { + return deserializer.Deserialize(input); + } + } + + public T Deserialize(TextReader input) { + lock (deserializerLock) { + return deserializer.Deserialize(input); + } + } } public class FlowEmitter : ChainedEventEmitter { diff --git a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs index 4e21f13b2..4ea909f41 100644 --- a/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/ArpasingPlusPhonemizer.cs @@ -25,7 +25,7 @@ public class ArpasingPlusPhonemizer : SyllableBasedPhonemizer { "aan", "an", "axn", "aen", "ahn", "aon", "on", "awn", "aun", "ayn", "ain", "ehn", "en", "eyn", "ein", "ihn", "iyn", "in", "own", "oun", "oyn", "oin", "uhn", "uwn", "un", "aang", "ang", "axng", "aeng", "ahng", "aong", "ong", "awng", "aung", "ayng", "aing", "ehng", "eng", "eyng", "eing", "ihng", "iyng", "ing", "owng", "oung", "oyng", "oing", "uhng", "uwng", "ung", "aam", "am", "axm", "aem", "ahm", "aom", "om", "awm", "aum", "aym", "aim", "ehm", "em", "eym", "eim", "ihm", "iym", "im", "owm", "oum", "oym", "oim", "uhm", "uwm", "um", "oh", - "eu", "oe", "yw", "yx", "wx", "ox", "ex", "ea", "ia", "oa", "ua" + "eu", "oe", "yw", "yx", "wx", "ox", "ex", "ea", "ia", "oa", "ua", "ean", "eam", "eang" }; private readonly string[] consonants = "b,ch,d,dh,dr,dx,f,g,hh,jh,k,l,m,n,nx,ng,p,q,r,s,sh,t,th,tr,v,w,y,z,zh".Split(','); private readonly string[] affricates = "ch,jh,j".Split(','); @@ -110,7 +110,20 @@ public class ArpasingPlusPhonemizer : SyllableBasedPhonemizer { {"uwr","r"}, {"awn","n"}, {"awng","ng"}, + {"ean","n"}, + {"eam","m"}, + {"eang","ng"}, + // r-colored vowel and l + {"ar","r"}, + {"or","r"}, + {"air","r"}, + {"ir","r"}, + {"ur","r"}, + {"al","l"}, + {"ol","l"}, + {"il","l"}, {"el","l"}, + {"ul","l"}, }; private readonly Dictionary vvDiphthongExceptions = new Dictionary() { @@ -122,10 +135,9 @@ public class ArpasingPlusPhonemizer : SyllableBasedPhonemizer { {"oy","ao"}, }; - private readonly string[] ccvException = { "ch", "dh", "dx", "fh", "gh", "hh", "jh", "kh", "ph", "ng", "sh", "th", "vh", "wh", "zh" }; private readonly string[] RomajiException = { "a", "e", "i", "o", "u" }; - private string[] tails = "-,R,RB".Split(','); + private string[] tails = "-,R".Split(','); protected override string[] GetSymbols(Note note) { string[] original = base.GetSymbols(note); @@ -386,8 +398,9 @@ protected override List ProcessSyllable(Syllable syllable) { basePhoneme = v; } else if ((HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone) && (!HasOto(v, syllable.vowelTone) && !HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(ccv, syllable.vowelTone) && !HasOto(ValidateAlias(ccv), syllable.vowelTone)) && (!HasOto(crv, syllable.vowelTone) && !HasOto(ValidateAlias(crv), syllable.vowelTone)))) { basePhoneme = $"{prevV} {v}"; + } else { - basePhoneme = $"{cc.Last()} {v}"; + basePhoneme = $"{cc.Last()}{v}"; } // TRY RCC [- CC] for (var i = cc.Length; i > 1; i--) { @@ -409,13 +422,15 @@ protected override List ProcessSyllable(Syllable syllable) { /// CV if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { basePhoneme = crv; - } else if ((HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) && HasOto(ValidateAlias(crv), syllable.vowelTone))) { + } else if ((HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) && (!HasOto(crv, syllable.vowelTone) && !HasOto(ValidateAlias(crv), syllable.vowelTone))) { basePhoneme = cv; /// C+V - } else if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) && (!HasOto(crv, syllable.vowelTone) && !HasOto(ValidateAlias(crv), syllable.vowelTone))) { + /* + } else if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone))) { basePhoneme = v; - } else if ((HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone) && (!HasOto(v, syllable.vowelTone) && !HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) && (!HasOto(crv, syllable.vowelTone) && !HasOto(ValidateAlias(crv), syllable.vowelTone)))) { + } else if ((HasOto($"{prevV} {v}", syllable.vowelTone) || HasOto(ValidateAlias($"{prevV} {v}"), syllable.vowelTone) && (!HasOto(v, syllable.vowelTone) && !HasOto(ValidateAlias(v), syllable.vowelTone)))) { basePhoneme = $"{prevV} {v}"; + */ } else { basePhoneme = $"{cc.Last()} {v}"; } @@ -435,7 +450,13 @@ protected override List ProcessSyllable(Syllable syllable) { break; /// C-Last V } else if (syllable.CurrentWordCc.Length == 1 && syllable.PreviousWordCc.Length == 1) { - basePhoneme = crv; + if ((HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { + basePhoneme = crv; + } else if ((HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) && !(HasOto(crv, syllable.vowelTone)) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { + basePhoneme = cv; + } else { + basePhoneme = crv; + } } } // try [V C], [V CC], [VC C], [V -][- C] @@ -479,8 +500,10 @@ protected override List ProcessSyllable(Syllable syllable) { for (var i = firstC; i < lastC; i++) { var ccv = $"{string.Join("", cc.Skip(i))} {v}"; + var ccv1 = $"{string.Join("", cc.Skip(i))}{v}"; var cc1 = $"{string.Join(" ", cc.Skip(i))}"; var lcv = $"{cc.Last()} {v}"; + var cv = $"{cc.Last()}{v}"; if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); } @@ -510,8 +533,14 @@ protected override List ProcessSyllable(Syllable syllable) { basePhoneme = ccv; lastC = i; break; + } else if (HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = ccv1; + lastC = i; + break; } else if ((HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone)) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { basePhoneme = lcv; + } else if ((HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) && HasOto(cc1, syllable.vowelTone)) { + basePhoneme = cv; } // [C1 C2C3] if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { @@ -519,17 +548,22 @@ protected override List ProcessSyllable(Syllable syllable) { } } else if (syllable.CurrentWordCc.Length == 1 && syllable.PreviousWordCc.Length == 1) { basePhoneme = lcv; + if ((HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone))) { + basePhoneme = cv; + } // [C1 C2] if (!HasOto(cc1, syllable.tone)) { - cc1 = $"{cc[i]} {cc[i + 1]}"; + cc1 = $"{cc[i]} {cc[i + 1]}"; } } // C+V - if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone))) { + + if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { cPV_FallBack = true; basePhoneme = v; cc1 = ValidateAlias(cc1); } + if (i + 1 < lastC) { if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); @@ -556,29 +590,37 @@ protected override List ProcessSyllable(Syllable syllable) { basePhoneme = ccv; lastC = i; break; + } else if (HasOto(ccv1, syllable.vowelTone) || HasOto(ValidateAlias(ccv1), syllable.vowelTone) && !ccvException.Contains(cc[0])) { + basePhoneme = ccv1; + lastC = i; + break; } else if ((HasOto(lcv, syllable.vowelTone) || HasOto(ValidateAlias(lcv), syllable.vowelTone)) && HasOto(cc1, syllable.vowelTone) && !HasOto(ccv, syllable.vowelTone)) { basePhoneme = lcv; + } else if ((HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone)) && HasOto(cc1, syllable.vowelTone)) { + basePhoneme = cv; } // [C1 C2C3] if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i + 1))}", syllable.tone)) { cc1 = $"{cc[i]} {string.Join("", cc.Skip(i + 1))}"; } - // [C1 C2C3...] - if (HasOto($"{cc[i]} {string.Join("", cc.Skip(i))}", syllable.tone)) { - cc1 = $"{cc[i]} {string.Join("", cc.Skip(i))}"; - } } else if (syllable.CurrentWordCc.Length == 1 && syllable.PreviousWordCc.Length == 1) { basePhoneme = lcv; + if ((HasOto(cv, syllable.vowelTone) || HasOto(ValidateAlias(cv), syllable.vowelTone))) { + basePhoneme = cv; + } // [C1 C2] if (!HasOto(cc1, syllable.tone)) { cc1 = $"{cc[i]} {cc[i + 1]}"; } } // C+V - if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone))) { + + if ((HasOto(v, syllable.vowelTone) || HasOto(ValidateAlias(v), syllable.vowelTone)) && (!HasOto(lcv, syllable.vowelTone) && !HasOto(ValidateAlias(lcv), syllable.vowelTone) && (!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)))) { cPV_FallBack = true; basePhoneme = v; + cc1 = ValidateAlias(cc1); } + if (HasOto(cc1, syllable.tone) && HasOto(cc1, syllable.tone) && !cc1.Contains($"{string.Join("", cc.Skip(i))}")) { // like [V C1] [C1 C2] [C2 C3] [C3 ..] phonemes.Add(cc1); @@ -772,7 +814,7 @@ protected override List ProcessEnding(Ending ending) { } protected override string ValidateAlias(string alias) { - //FALLBACKS + //CV FALLBACKS if (alias == "ng ao") { return alias.Replace("ao", "ow"); } else if (alias == "ch ao") { @@ -885,14 +927,14 @@ protected override string ValidateAlias(string alias) { } var CVMappings = new Dictionary { - { "ao", new[] { "ow" } }, - { "oy", new[] { "ow" } }, - { "aw", new[] { "ah" } }, - { "ay", new[] { "ah" } }, - { "eh", new[] { "ae" } }, - { "ey", new[] { "eh" } }, - { "ow", new[] { "ao" } }, - { "uh", new[] { "uw" } }, + { "ao", new[] { "ow" } }, + { "oy", new[] { "ow" } }, + { "aw", new[] { "ah" } }, + { "ay", new[] { "ah" } }, + { "eh", new[] { "ae" } }, + { "ey", new[] { "eh" } }, + { "ow", new[] { "ao" } }, + { "uh", new[] { "uw" } }, }; foreach (var kvp in CVMappings) { var v1 = kvp.Key; @@ -904,23 +946,21 @@ protected override string ValidateAlias(string alias) { } } - Dictionary> vvReplacements = new Dictionary> - { //VV (diphthongs) some - { "ay ay", new List { "y ah" } }, - { "ey ey", new List { "iy ey" } }, - { "oy oy", new List { "y ow" } }, - { "er er", new List { "er" } }, - { "aw aw", new List { "w ae" } }, - { "ow ow", new List { "w ao" } }, - { "uw uw", new List { "w uw" } }, + var vvReplacements = new Dictionary> { + { "ay ay", new List { "y ah" } }, + { "ey ey", new List { "iy ey" } }, + { "oy oy", new List { "y ow" } }, + { "er er", new List { "er" } }, + { "aw aw", new List { "w ae" } }, + { "ow ow", new List { "w ao" } }, + { "uw uw", new List { "w uw" } } }; - foreach (var kvp in vvReplacements) { - var originalValue = kvp.Key; - var replacementOptions = kvp.Value; - foreach (var replacement in replacementOptions) { - alias = alias.Replace(originalValue, replacement); + // Apply VV replacements + foreach (var (originalValue, replacementOptions) in vvReplacements) { + foreach (var replacementOption in replacementOptions) { + alias = alias.Replace(originalValue, replacementOption); } } //VC (diphthongs) @@ -1229,18 +1269,12 @@ protected override string ValidateAlias(string alias) { if (ccSpecific) { //CC (b) //CC (b specific) - if (alias == "b ch") { - return alias.Replace("b ch", "t ch"); - } if (alias == "b dh") { return alias.Replace("b ch", "p dh"); } if (alias == "b ng") { return alias.Replace("b ng", "ng"); } - if (alias == "b th") { - return alias.Replace("b th", "t th"); - } if (alias == "b zh") { return alias.Replace("zh", "z"); } @@ -1263,23 +1297,14 @@ protected override string ValidateAlias(string alias) { } //CC (d specific) - if (alias == "d ch") { - return alias.Replace("d", "t"); - } if (alias == "d ng") { return alias.Replace("ng", "n"); } - if (alias == "d th") { - return alias.Replace("d th", "t th"); - } if (alias == "d zh") { return alias.Replace("zh", "z"); } //CC (dh specific) - if (alias == "dh ch") { - return alias.Replace("dh ch", "t ch"); - } if (alias == "dh dh") { return alias.Replace("dh dh", "dh d"); } @@ -1306,9 +1331,6 @@ protected override string ValidateAlias(string alias) { } //CC (g specific) - if (alias == "g ch") { - return alias.Replace("g ch", "t ch"); - } if (alias == "g dh") { return alias.Replace("g", "d"); } @@ -1323,9 +1345,7 @@ protected override string ValidateAlias(string alias) { if (alias == "hh y") { return alias.Replace("hh", "f"); } - if (alias == "hh -") { - return alias.Replace("hh -", "- hh"); - } + //CC (jh specific) if (alias == "jh r") { @@ -1353,9 +1373,6 @@ protected override string ValidateAlias(string alias) { if (alias == "l b") { return alias.Replace("b", "d"); } - if (alias == "l hh") { - return alias.Replace("l", "r"); - } if (alias == "l jh") { return alias.Replace("jh", "d"); } @@ -1435,17 +1452,11 @@ protected override string ValidateAlias(string alias) { if (alias == "ng ng") { return alias.Replace("ng", "n"); } - if (alias == "ng v") { - return alias.Replace("ng v", "ng s"); - } if (alias == "ng zh") { return alias.Replace("zh", "z"); } //CC (p specific) - if (alias == "p dx") { - return alias.Replace("p dx", "t d"); - } if (alias == "p z") { return alias.Replace("z", "s"); } @@ -1474,9 +1485,6 @@ protected override string ValidateAlias(string alias) { if (alias == "s ch") { return alias.Replace("ch", "t"); } - if (alias == "s dx") { - return alias.Replace("dx", "d"); - } if (alias == "s ng") { return alias.Replace("ng", "n"); } @@ -1514,9 +1522,6 @@ protected override string ValidateAlias(string alias) { } //CC (t specific) - if (alias == "t z") { - return alias.Replace("t", "g"); - } if (alias == "t zh") { return alias.Replace("t zh", "g z"); } @@ -1536,12 +1541,6 @@ protected override string ValidateAlias(string alias) { if (alias == "v th") { return alias.Replace("v th", "th"); } - if (alias == "v s") { - return alias.Replace("v", "s"); - } - if (alias == "v z") { - return alias.Replace("v z", "s s"); - } // CC (w C) foreach (var c2 in consonants) { if (!(alias.Contains($"aw {c2}") || alias.Contains($"ew {c2}") || alias.Contains($"iw {c2}") || alias.Contains($"ow {c2}") || alias.Contains($"uw {c2}"))) { diff --git a/OpenUtau.Plugin.Builtin/CantoneseCVVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/CantoneseCVVCPhonemizer.cs index 4e4338f6a..04af4ae04 100644 --- a/OpenUtau.Plugin.Builtin/CantoneseCVVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/CantoneseCVVCPhonemizer.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; +using System.Collections.Generic; using System.Linq; +using IKg2p; using OpenUtau.Api; -using OpenUtau.Classic; -using OpenUtau.Core.G2p; -using OpenUtau.Core.Ustx; -using Serilog; namespace OpenUtau.Plugin.Builtin { /// @@ -17,7 +12,8 @@ namespace OpenUtau.Plugin.Builtin { [Phonemizer("Cantonese CVVC Phonemizer", "ZH-YUE CVVC", "Lotte V", language: "ZH-YUE")] public class CantoneseCVVCPhonemizer : ChineseCVVCPhonemizer { protected override string[] Romanize(IEnumerable lyrics) { - return ZhG2p.CantoneseInstance.Convert(lyrics.ToList(), false, true).Split(" "); + List g2pResults = ZhG2p.CantoneseInstance.Convert(lyrics.ToList(), false, false); + return g2pResults.Select(res => res.syllable).ToArray(); } } } diff --git a/OpenUtau.Plugin.Builtin/CantoneseSyoPhonemizer.cs b/OpenUtau.Plugin.Builtin/CantoneseSyoPhonemizer.cs index 44fd41331..fce5052ed 100644 --- a/OpenUtau.Plugin.Builtin/CantoneseSyoPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/CantoneseSyoPhonemizer.cs @@ -1,8 +1,8 @@ -using OpenUtau.Api; -using OpenUtau.Core.G2p; -using OpenUtau.Core.Ustx; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using IKg2p; +using OpenUtau.Api; +using OpenUtau.Core.Ustx; namespace OpenUtau.Plugin.Builtin { /// @@ -288,7 +288,8 @@ public static string[] Romanize(IEnumerable lyrics) { var hanziLyrics = lyricsArray .Where(ZhG2p.CantoneseInstance.IsHanzi) .ToList(); - var jyutpingResult = ZhG2p.CantoneseInstance.Convert(hanziLyrics, false, false).ToLower().Split(); + List g2pResults = ZhG2p.CantoneseInstance.Convert(lyrics.ToList(), false, false); + var jyutpingResult = g2pResults.Select(res => res.syllable).ToArray(); if (jyutpingResult == null) { return lyricsArray; } diff --git a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs index 6dacbf8a6..a572df413 100644 --- a/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/EnXSampaPhonemizer.cs @@ -22,7 +22,7 @@ namespace OpenUtau.Plugin.Builtin { /// [Phonemizer("English X-SAMPA phonemizer", "EN X-SAMPA", "Lotte V", language: "EN")] public class EnXSampaPhonemizer : SyllableBasedPhonemizer { - private readonly string[] vowels = "a,A,@,{,V,O,aU,aI,E,3,eI,I,i,oU,OI,U,u,Q,Ol,Ql,aUn,e@,eN,IN,e,o,Ar,Qr,Er,Ir,Or,Ur,ir,ur,aIr,aUr,A@,Q@,E@,I@,O@,U@,i@,u@,aI@,aU@,@r,@l,@m,@n,@N,1,e@m,e@n,y,I\\,M,U\\,Y,@\\,@`,3`,A`,Q`,E`,I`,O`,U`,i`,u`,aI`,aU`,},2,3\\,6,7,8,9,&,{~,I~,aU~,VI,VU,@U,ai,ei,Oi,au,ou,Ou,@u,i:,u:,O:,e@0,E~,e~,3r,ar,or,{l,Al,al,El,Il,il,ol,ul,Ul,oUl,mm,nn,ll,NN".Split(','); + private readonly string[] vowels = "a,A,@,{,V,O,aU,aI,E,3,eI,I,i,oU,OI,U,u,Q,Ol,Ql,aUn,e@,eN,IN,e,o,Ar,Qr,Er,Ir,Or,Ur,ir,ur,aIr,aUr,A@,Q@,E@,I@,O@,U@,i@,u@,aI@,aU@,@r,@l,@m,@n,@N,1,e@m,e@n,y,I\\,M,U\\,Y,@\\,@`,3`,A`,Q`,E`,I`,O`,U`,i`,u`,aI`,aU`,},2,3\\,6,7,8,9,&,{~,I~,aU~,VI,VU,@U,ai,ei,Oi,au,ou,Ou,@u,i:,u:,O:,e@0,E~,e~,3r,ar,or,{l,Al,al,El,Il,il,ol,ul,Ul,oUl,@5,u5,O5,A5,E5,I5,i5,mm,nn,ll,NN".Split(','); private readonly string[] consonants = "b,tS,d,D,4,f,g,h,dZ,k,l,m,n,N,p,r,s,S,t,T,v,w,W,j,z,Z,t_},・,_".Split(','); private readonly string[] affricates = "tS,dZ".Split(','); private readonly string[] shortConsonants = "4".Split(","); @@ -117,6 +117,15 @@ public class EnXSampaPhonemizer : SyllableBasedPhonemizer { private bool isTetoException = false; + // For dark L vowels + private readonly Dictionary darkLVowel = "@l=@5,ul=u5,Ol=O5,Al=A5,El=E5,Il=I5,il=i5,".Split(';') + .Select(entry => entry.Split('=')) + .Where(parts => parts.Length == 2) + .Where(parts => parts[0] != parts[1]) + .ToDictionary(parts => parts[0], parts => parts[1]); + + private bool isDarkLVowel = false; + private readonly Dictionary vvExceptions = new Dictionary() { {"aI","j"}, @@ -264,6 +273,10 @@ protected override List ProcessSyllable(Syllable syllable) { isVelarNasalFallback = true; } + if (HasOto("@5", syllable.vowelTone) || HasOto("u5", syllable.vowelTone) || HasOto("O5", syllable.vowelTone) || HasOto("A5", syllable.vowelTone) || HasOto("E5", syllable.vowelTone) || HasOto("I5", syllable.vowelTone) || HasOto("i5", syllable.vowelTone) || HasOto("- @5", syllable.vowelTone) || HasOto("- u5", syllable.vowelTone) || HasOto("- O5", syllable.vowelTone) || HasOto("- A5", syllable.vowelTone) || HasOto("- E5", syllable.vowelTone) || HasOto("- I5", syllable.vowelTone) || HasOto("- i5", syllable.vowelTone)) { + isDarkLVowel = true; + } + if (syllable.IsStartingV) { if (HasOto(rv, syllable.vowelTone) || HasOto(ValidateAlias(rv), syllable.vowelTone)) { basePhoneme = rv; @@ -383,24 +396,27 @@ protected override List ProcessSyllable(Syllable syllable) { lastC = 0; } else { var cv = cc.Last() + v; - if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { + basePhoneme = cv; + if ((!HasOto(cv, syllable.vowelTone) && !HasOto(ValidateAlias(cv), syllable.vowelTone)) && (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone))) { basePhoneme = crv; - } else { - basePhoneme = cv; } // try CCV - if (cc.Length - firstC > 1) { + if ((cc.Length - firstC > 1) && CurrentWordCc.Length >= 2) { for (var i = firstC; i < cc.Length; i++) { - var ccv = $"{string.Join("", cc.Skip(i))}{v}"; - var rccv = $"- {string.Join("", cc.Skip(i))}{v}"; - if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2) { - lastC = i; + var ccv = $"{string.Join("", cc.Skip(0))}{v}"; + var rccv = $"- {string.Join("", cc.Skip(0))}{v}"; + var ucv = $"_{cc.Last()}{v}"; + if (HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) { + lastC = 0; basePhoneme = ccv; break; - } else if ((HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) && (!HasOto(ccv, syllable.vowelTone) && !HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2) { - lastC = i; + } else if (!HasOto(ccv, syllable.vowelTone) && !HasOto(ValidateAlias(ccv), syllable.vowelTone) && (HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone))) { + lastC = 0; basePhoneme = rccv; break; + } else if ((!HasOto(rccv, syllable.vowelTone) || !HasOto(ValidateAlias(rccv), syllable.vowelTone)) && (HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone))) { + basePhoneme = ucv; + break; } } } @@ -435,76 +451,81 @@ protected override List ProcessSyllable(Syllable syllable) { for (var i = firstC; i < lastC; i++) { // we could use some CCV, so lastC is used // we could use -CC so firstC is used - var cc1 = $"{string.Join("", cc.Skip(i))}"; - var ccv = string.Join("", cc.Skip(i)) + v; + var cc1 = $"{cc[i]} {cc[i + 1]}"; + var ccv = string.Join("", cc.Skip(i + 1)) + v; + var rccv = $"- {string.Join("", cc.Skip(i + 1)) + v}"; var ucv = $"_{cc.Last()}{v}"; var crv = $"{cc.Last()} {v}"; var cv = $"{cc.Last()}{v}"; // Use [C1C2...] when current word starts with 2 consonants or more - if (CurrentWordCc.Length >= 2) { + if (!HasOto(cc1, syllable.tone)) { + cc1 = ValidateAlias(cc1); + } + if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc1)) { cc1 = $"{string.Join("", cc.Skip(i))}"; } if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); } // Use [C1C2] when current word has 2 consonants or more and [C1C2C3...] does not exist - if (!HasOto(cc1, syllable.tone) && CurrentWordCc.Length >= 2) { + if (!HasOto(cc1, syllable.tone) && CurrentWordCc.Length >= 2 && CurrentWordCc.Contains(cc1)) { cc1 = $"{cc[i]}{cc[i + 1]}"; } if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); } // Use [C1 C2] when either [C1C2] does not exist, or current word has 1 consonant or less and previous word has 1 consonant or more - if (!HasOto(cc1, syllable.tone) || (prevWordConsonantsCount >= 1 && CurrentWordCc.Length <= 1)) { + if ((!HasOto(cc1, syllable.tone)) || PreviousWordCc.Contains(cc1)) { cc1 = $"{cc[i]} {cc[i + 1]}"; } if (!HasOto(cc1, syllable.tone)) { cc1 = ValidateAlias(cc1); } - // Use CCV if it exists - if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2) { - basePhoneme = ccv; - lastC = i; - // Use _CV if it exists - } else if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && HasOto(cc1, syllable.vowelTone) && !cc1.Contains($"{cc[i]} {cc[i + 1]}")) { + // Use UCV if it exists + if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && !cc1.Contains($"{cc[i]} {cc[i + 1]}")) { basePhoneme = ucv; - } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { - basePhoneme = crv; - } else { - basePhoneme = cv; } if (i + 1 < lastC) { - var cc2 = $"{string.Join("", cc.Skip(i))}"; + var cc2 = $"{cc[i + 1]} {cc[i + 2]}"; + if (!HasOto(cc2, syllable.tone)) { + cc2 = ValidateAlias(cc2); + } // Use [C2C3...] when current word starts with 2 consonants or more - if (CurrentWordCc.Length >= 2) { + if (CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(cc2)) { cc2 = $"{string.Join("", cc.Skip(i))}"; } if (!HasOto(cc2, syllable.tone)) { cc2 = ValidateAlias(cc2); } // Use [C2C3] when current word has 2 consonants or more and [C2C3C4...] does not exist - if (!HasOto(cc2, syllable.tone) && CurrentWordCc.Length >= 2) { + if (!HasOto(cc2, syllable.tone) && CurrentWordCc.Length >= 2 && CurrentWordCc.Contains(cc2)) { cc2 = $"{cc[i + 1]}{cc[i + 2]}"; } if (!HasOto(cc2, syllable.tone)) { cc2 = ValidateAlias(cc2); } // Use [C2 C3] when either [C2C3] does not exist, or current word has 1 consonant or less and previous word has 2 consonants or more - if (!HasOto(cc2, syllable.tone) || (prevWordConsonantsCount >= 2 && CurrentWordCc.Length <= 1 && !CurrentWordCc.Contains(cc2))) { + if ((!HasOto(cc2, syllable.tone)) || PreviousWordCc.Contains(cc2)) { cc2 = $"{cc[i + 1]} {cc[i + 2]}"; } if (!HasOto(cc2, syllable.tone)) { cc2 = ValidateAlias(cc2); } - // Use CCV if it exists - if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2) { + //Use CCV if it exists + if ((HasOto(ccv, syllable.vowelTone) || HasOto(ValidateAlias(ccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(string.Join("", cc.Skip(i + 1)))) { + lastC = i; basePhoneme = ccv; + // Use RCCV if it exists + } else if ((HasOto(rccv, syllable.vowelTone) || HasOto(ValidateAlias(rccv), syllable.vowelTone)) && CurrentWordCc.Length >= 2 && !PreviousWordCc.Contains(string.Join("", cc.Skip(i + 1)))) { lastC = i; + basePhoneme = rccv; // Use _CV if it exists - } else if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && (HasOto(cc2, syllable.vowelTone) || HasOto(ValidateAlias(cc2), syllable.vowelTone)) && !cc2.Contains($"{cc[i + 1]} {cc[i + 2]}") && !PreviousWordCc.Contains(ucv)) { + } else if ((HasOto(ucv, syllable.vowelTone) || HasOto(ValidateAlias(ucv), syllable.vowelTone)) && HasOto(cc2, syllable.vowelTone) && !cc2.Contains($"{cc[i + 1]} {cc[i + 2]}") && CurrentWordCc.Length >= 2) { basePhoneme = ucv; + // Use spaced CV if it exists } else if (HasOto(crv, syllable.vowelTone) || HasOto(ValidateAlias(crv), syllable.vowelTone)) { basePhoneme = crv; + // Use normal CV } else { basePhoneme = cv; } @@ -517,7 +538,7 @@ protected override List ProcessSyllable(Syllable syllable) { i++; } } else { - /// Add single consonant if no CC cluster + // Add single consonant if no CC cluster // like [V C1] [C1] [C2 ..] TryAddPhoneme(phonemes, syllable.tone, cc[i], ValidateAlias(cc[i])); } @@ -743,6 +764,12 @@ protected override string ValidateAlias(string alias) { } } + if (isDarkLVowel) { + foreach (var syllable in darkLVowel) { + alias = alias.Replace(syllable.Key, syllable.Value); + } + } + // Split diphthongs adjuster if (alias.Contains("U^")) { alias = alias.Replace("U^", "U"); diff --git a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs index 3bd1b143c..6c748d888 100644 --- a/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/JapanesePresampPhonemizer.cs @@ -60,15 +60,29 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN var result = new List(); bool preCFlag = false; + // If the [PRIORITY] section in the presamp.ini contains a blank newline, don't treat any consonant as priority. + // If there is no [PRIORITY] section in the presamp.ini, it will return the default values. + if (presamp.Priorities == null) { + presamp.Priorities?.Clear(); + } + + // If the [REPLACE] section in the presamp.ini contains a blank newline, don't treat any consonant as priority. + // If there is no [REPLACE] section in the presamp.ini, it will return the default values. + if (presamp.Replace == null) { + presamp.Replace?.Clear(); + } + var note = notes[0]; var currentLyric = note.lyric.Normalize(); // Normalize(): measures for Unicode if (!string.IsNullOrEmpty(note.phoneticHint)) { currentLyric = note.phoneticHint.Normalize(); } else { // replace (exact match) - foreach (var pair in presamp.Replace) { - if (pair.Key == currentLyric) { - currentLyric = pair.Value; + if (presamp.Replace != null) { + foreach (var pair in presamp.Replace) { + if (pair.Key == currentLyric) { + currentLyric = pair.Value; + } } } } @@ -105,9 +119,11 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN if (!string.IsNullOrEmpty(prevNeighbour.Value.phoneticHint)) { // current phoneme is converted even if prev has hint prevLyric = prevNeighbour.Value.phoneticHint.Normalize(); } else { - foreach (var pair in presamp.Replace) { - if (pair.Key == prevLyric) { - prevLyric = pair.Value; + if (presamp.Replace != null) { + foreach (var pair in presamp.Replace) { + if (pair.Key == prevLyric) { + prevLyric = pair.Value; + } } } } @@ -168,9 +184,11 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN if (!string.IsNullOrEmpty(nextNeighbour.Value.phoneticHint)) { nextLyric = nextNeighbour.Value.phoneticHint.Normalize(); } else { - foreach (var pair in presamp.Replace) { - if (pair.Key == nextLyric) { - nextLyric = pair.Value; + if (presamp.Replace != null) { + foreach (var pair in presamp.Replace) { + if (pair.Key == nextLyric) { + nextLyric = pair.Value; + } } } } @@ -217,12 +235,12 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN && !currentLyric.Contains(vcvpad) && presamp.PhonemeList.TryGetValue(currentAlias, out PresampPhoneme phoneme) && phoneme.HasConsonant - && !presamp.Priorities.Contains(phoneme.Consonant)) { + && (presamp.Priorities == null || !presamp.Priorities.Contains(phoneme.Consonant))) { if (checkOtoUntilHit(new List { $"-{vcvpad}{phoneme.Consonant}" }, note, 2, out var cOto, out var color) && checkOtoUntilHit(new List { currentLyric }, note, out var oto)) { int endTick = notes[^1].position + notes[^1].duration; var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; - var cLength = Math.Max(30, -MsToTickAt(-oto.Preutter, endTick) * (attr.consonantStretchRatio ?? 1)); + var cLength = Math.Max(30, -timeAxis.MsToTickAt(-oto.Preutter, endTick) * (attr.consonantStretchRatio ?? 1)); if (prevNeighbour != null) { cLength = Math.Min(prevNeighbour.Value.duration / 2, cLength); @@ -250,9 +268,11 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN } var nextLyric = nextNeighbour.Value.lyric.Normalize(); - foreach (var pair in presamp.Replace) { - if (pair.Key == nextLyric) { - nextLyric = pair.Value; + if (presamp.Replace != null) { + foreach (var pair in presamp.Replace) { + if (pair.Key == nextLyric) { + nextLyric = pair.Value; + } } } string nextAlias = presamp.ParseAlias(nextLyric)[1]; // exclude useless characters @@ -333,9 +353,9 @@ public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevN if (singer.TryGetMappedOto(nextLyric, nextNeighbour.Value.tone + nextAttr.toneShift, nextAttr.voiceColor, out var nextOto)) { // If overlap is a negative value, vcLength is longer than Preutter if (nextOto.Overlap < 0) { - vcLength = -MsToTickAt(-(nextOto.Preutter - nextOto.Overlap), endTick); + vcLength = -timeAxis.MsToTickAt(-(nextOto.Preutter - nextOto.Overlap), endTick); } else { - vcLength = -MsToTickAt(-nextOto.Preutter, endTick); + vcLength = -timeAxis.MsToTickAt(-nextOto.Preutter, endTick); } } // Minimam is 30 tick, maximum is half of note @@ -414,17 +434,5 @@ private bool checkOtoUntilHit(List input, Note note, int index, out UOto } return false; } - - /// - /// Convert ms to tick at a given reference tick position - /// - /// Duration in ms - /// Reference tick position - /// Duration in ticks - public int MsToTickAt(double offsetMs, int refTick) { - return timeAxis.TicksBetweenMsPos( - timeAxis.TickPosToMsPos(refTick), - timeAxis.TickPosToMsPos(refTick) + offsetMs); - } } } diff --git a/OpenUtau.Plugin.Builtin/KOtoJAPhonemizer.cs b/OpenUtau.Plugin.Builtin/KOtoJAPhonemizer.cs index ef7a80b6c..aac6b7b90 100644 --- a/OpenUtau.Plugin.Builtin/KOtoJAPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/KOtoJAPhonemizer.cs @@ -4,12 +4,13 @@ using System.Text; using System.Text.RegularExpressions; using OpenUtau.Api; +using OpenUtau.Core; using OpenUtau.Core.Ustx; using WanaKanaNet; namespace OpenUtau.Plugin.Builtin { [Phonemizer("KO to JA Phonemizer", "KO to JA", "Lotte V", language: "KO")] - public class KOtoJAPhonemizer : Phonemizer { + public class KOtoJAPhonemizer : BaseKoreanPhonemizer { /// /// Phonemizer for making Japanese banks sing in Korean. /// Supports Hangul and phonetic hint (based on Japanese romaji). @@ -308,6 +309,14 @@ static KOtoJAPhonemizer() { {"m", "mu" }, }; + /// + /// Apply Korean sandhi rules to Hangeul lyrics. + /// + public override void SetUp(Note[][] groups, UProject project, UTrack track) { + // variate lyrics + KoreanPhonemizerUtil.RomanizeNotes(groups, false); + } + /// /// Gets the romanized initial, medial, and final components of the passed Hangul syllable. /// diff --git a/OpenUtau.Plugin.Builtin/KoreanCVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/KoreanCVCCVPhonemizer.cs index 48eb5d3d1..35263e0a6 100644 --- a/OpenUtau.Plugin.Builtin/KoreanCVCCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/KoreanCVCCVPhonemizer.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.Linq; using OpenUtau.Api; +using OpenUtau.Core; using OpenUtau.Core.Ustx; using static OpenUtau.Api.Phonemizer; namespace OpenUtau.Plugin.Builtin { [Phonemizer("Korean CVCCV Phonemizer", "KO CVCCV", "RYUUSEI", language:"KO")] - public class KoreanCVCCVPhonemizer : Phonemizer { + public class KoreanCVCCVPhonemizer : BaseKoreanPhonemizer { static readonly string initialConsonantsTable = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"; static readonly string vowelsTable = "ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ"; static readonly string YVowelsTable = "ㅣㅑㅖㅛㅠㅕ"; @@ -207,6 +208,14 @@ private char[] SeparateHangul(char letter) { "l=ㄹ,ㄺ,ㄻ,ㄼ,ㄽ,ㄾ,ㄿ,ㅀ", }; + /// + /// Apply Korean sandhi rules to Hangeul lyrics. + /// + public override void SetUp(Note[][] groups, UProject project, UTrack track) { + // variate lyrics + KoreanPhonemizerUtil.RomanizeNotes(groups, false); + } + static readonly Dictionary initialConsonantLookup; static readonly Dictionary ccvContinuousinitialConsonantsLookup; static readonly Dictionary vrcInitialConsonantLookup; diff --git a/OpenUtau.Plugin.Builtin/KoreanCVCPhonemizer.cs b/OpenUtau.Plugin.Builtin/KoreanCVCPhonemizer.cs index 5fa8b3016..2ac3ba5e9 100644 --- a/OpenUtau.Plugin.Builtin/KoreanCVCPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/KoreanCVCPhonemizer.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.Linq; using OpenUtau.Api; +using OpenUtau.Core; using OpenUtau.Core.Ustx; namespace OpenUtau.Plugin.Builtin { [Phonemizer("KoreanCVCPhonemizer", "KO CVC", "NANA", language:"KO")] - public class KoreanCVCPhonemizer : Phonemizer { + public class KoreanCVCPhonemizer : BaseKoreanPhonemizer { static readonly string[] naPlainVowels = new string[] { "a", "e", "a", "e", "eo", "e", "eo", "e", "o", "a", "e", "e", "o", "u", "eo", "e", "i", "u", "eu", "i", "i" }; @@ -91,6 +92,14 @@ string getConsonant(string str) { return str; } + /// + /// Apply Korean sandhi rules to Hangeul lyrics. + /// + public override void SetUp(Note[][] groups, UProject project, UTrack track) { + // variate lyrics + KoreanPhonemizerUtil.RomanizeNotes(groups, false); + } + bool isAlphaCon(string str) { if (str == "gg") { return true; } else if (str == "dd") { return true; } diff --git a/OpenUtau.Plugin.Builtin/KoreanVCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/KoreanVCVPhonemizer.cs index 60cd6380a..ef45888ad 100644 --- a/OpenUtau.Plugin.Builtin/KoreanVCVPhonemizer.cs +++ b/OpenUtau.Plugin.Builtin/KoreanVCVPhonemizer.cs @@ -3,13 +3,14 @@ using System.Text; using System.Text.RegularExpressions; using OpenUtau.Api; +using OpenUtau.Core; using OpenUtau.Core.Ustx; namespace OpenUtau.Plugin.Builtin { [Phonemizer("Korean VCV Phonemizer", "KO VCV", "ldc", language: "KO")] - public class KoreanVCVPhonemizer : Phonemizer + public class KoreanVCVPhonemizer : BaseKoreanPhonemizer { /// /// Initial jamo as ordered in Unicode @@ -39,6 +40,14 @@ public class KoreanVCVPhonemizer : Phonemizer /// static readonly string[] extras = { "f", "v", "th", "dh", "z", "rr", "kk", "pp", "tt" }; + /// + /// Apply Korean sandhi rules to Hangeul lyrics. + /// + public override void SetUp(Note[][] groups, UProject project, UTrack track) { + // variate lyrics + KoreanPhonemizerUtil.RomanizeNotes(groups, false); + } + /// /// Gets the romanized initial, medial, and final components of the passed Hangul syllable. /// diff --git a/OpenUtau.Plugin.Builtin/ThaiVCCVPhonemizer.cs b/OpenUtau.Plugin.Builtin/ThaiVCCVPhonemizer.cs new file mode 100644 index 000000000..4d9319e30 --- /dev/null +++ b/OpenUtau.Plugin.Builtin/ThaiVCCVPhonemizer.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text.RegularExpressions; +using Melanchall.DryWetMidi.Interaction; +using OpenUtau.Api; +using OpenUtau.Classic; +using OpenUtau.Core.Ustx; +using Serilog; + +namespace OpenUtau.Plugin.Builtin { + [Phonemizer("Thai VCCV Phonemizer", "TH VCCV", "PRINTmov", language: "TH")] + public class ThaiVCCVPhonemizer : Phonemizer { + + readonly string[] vowels = new string[] { + "a", "i", "u", "e", "o", "@", "Q", "3", "6", "1", "ia", "ua", "I", "8" + }; + + readonly string[] diphthongs = new string[] { + "r", "l", "w" + }; + + readonly string[] consonants = new string[] { + "b", "ch", "d", "f", "g", "h", "j", "k", "kh", "l", "m", "n", "p", "ph", "r", "s", "t", "th", "w", "y" + }; + + readonly string[] endingConsonants = new string[] { + "b", "ch", "d", "f", "g", "h", "j", "k", "kh", "l", "m", "n", "p", "ph", "r", "s", "t", "th", "w", "y" + }; + + private readonly Dictionary VowelMapping = new Dictionary { + {"เcือะ", "6"}, {"เcือx", "6"}, {"แcะ", "@"}, {"แcx", "@"}, {"เcอะ", "3"}, {"เcอ", "3"}, {"ไc", "I"}, {"ใc", "I"}, {"เcาะ", "Q"}, {"cอx", "Q"}, + {"cืx", "1"}, {"cึx", "1"}, {"cือ", "1"}, {"cะ", "a"}, {"cัx", "a"}, {"cาx", "a"}, {"เcา", "8"}, {"เcะ", "e"}, {"เcx", "e"}, {"cิx", "i"}, {"cีx", "i"}, + {"เcียะ", "ia"}, {"เcียx", "ia"}, {"โcะ", "o"}, {"โcx", "o"}, {"cุx", "u"}, {"cูx", "u"}, {"cัวะ", "ua"}, {"cัว", "ua"}, {"cำ", "am"}, {"เcิx", "3"}, {"เcิ", "3"} + }; + + private readonly Dictionary CMapping = new Dictionary { + {'ก', "k"}, {'ข', "kh"}, {'ค', "kh"}, {'ฆ', "kh"}, {'ฅ', "kh"}, {'ฃ', "kh"}, + {'จ', "j"}, {'ฉ', "ch"}, {'ช', "ch"}, {'ฌ', "ch"}, + {'ฎ', "d"}, {'ด', "d"}, + {'ต', "t"}, {'ฏ', "t"}, + {'ถ', "th"}, {'ฐ', "th"}, {'ฑ', "th"}, {'ธ', "th"}, {'ท', "th"}, + {'บ', "b"}, {'ป', "p"}, {'พ', "ph"}, {'ผ', "ph"}, {'ภ', "ph"}, {'ฟ', "f"}, {'ฝ', "f"}, + {'ห', "h"}, {'ฮ', "h"}, + {'ม', "m"}, {'น', "n"}, {'ณ', "n"}, {'ร', "r"}, {'ล', "l"}, {'ฤ', "r"}, + {'ส', "s"}, {'ศ', "s"}, {'ษ', "s"}, {'ซ', "s"}, + {'ง', "g"}, {'ย', "y"}, {'ญ', "y"}, {'ว', "w"}, {'ฬ', "r"} + }; + + private readonly Dictionary XMapping = new Dictionary { + {'บ', "b"}, {'ป', "b"}, {'พ', "b"}, {'ฟ', "b"}, {'ภ', "b"}, + {'ด', "d"}, {'จ', "d"}, {'ช', "d"}, {'ซ', "d"}, {'ฎ', "d"}, {'ฏ', "d"}, {'ฐ', "d"}, + {'ฑ', "d"}, {'ฒ', "d"}, {'ต', "d"}, {'ถ', "d"}, {'ท', "d"}, {'ธ', "d"}, {'ศ', "d"}, {'ษ', "d"}, {'ส', "d"}, + {'ก', "k"}, {'ข', "k"}, {'ค', "k"}, {'ฆ', "k"}, + {'ว', "w"}, + {'ย', "y"}, + {'น', "n"}, {'ญ', "n"}, {'ณ', "n"}, {'ร', "n"}, {'ล', "n"}, {'ฬ', "n"}, + {'ง', "g"}, + {'ม', "m"} + }; + + private USinger singer; + public override void SetSinger(USinger singer) => this.singer = singer; + + private bool checkOtoUntilHit(string[] input, Note note, out UOto oto) { + oto = default; + var attr = note.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + + foreach (string test in input) { + if (singer.TryGetMappedOto(test, note.tone + attr.toneShift, attr.voiceColor, out var otoCandidacy)) { + oto = otoCandidacy; + return true; + } + } + return false; + } + + public override Result Process(Note[] notes, Note? prev, Note? next, Note? prevNeighbour, Note? nextNeighbour, Note[] prevNeighbours) { + var note = notes[0]; + var currentLyric = note.lyric.Normalize(); + if (!string.IsNullOrEmpty(note.phoneticHint)) { + currentLyric = note.phoneticHint.Normalize(); + } + + var phonemes = new List(); + + List tests = new List(); + + string prevTemp = ""; + if (prevNeighbour != null) { + prevTemp = prevNeighbour.Value.lyric; + } + var prevTh = ParseInput(prevTemp); + + var noteTh = ParseInput(currentLyric); + + if (noteTh.Consonant != null && noteTh.Dipthong == null && noteTh.Vowel != null) { + if (checkOtoUntilHit(new string[] { noteTh.Consonant + noteTh.Vowel }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } + } else if (noteTh.Consonant != null && noteTh.Dipthong != null && noteTh.Vowel != null) { + if (checkOtoUntilHit(new string[] { noteTh.Consonant + noteTh.Dipthong + noteTh.Vowel }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } else { + if (checkOtoUntilHit(new string[] { noteTh.Consonant + noteTh.Dipthong }, note, out tempOto)) { + tests.Add(tempOto.Alias); + } + if (checkOtoUntilHit(new string[] { noteTh.Dipthong + noteTh.Vowel }, note, out tempOto)) { + tests.Add(tempOto.Alias); + } + } + } + + if (noteTh.Consonant == null && noteTh.Vowel != null) { + if (prevTh.EndingConsonant != null && checkOtoUntilHit(new string[] { prevTh.EndingConsonant + noteTh.Vowel }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } else if (prevTh.Vowel != null && checkOtoUntilHit(new string[] { prevTh.Vowel + noteTh.Vowel }, note, out tempOto)) { + tests.Add(tempOto.Alias); + } else if (checkOtoUntilHit(new string[] { noteTh.Vowel }, note, out tempOto)) { + tests.Add(tempOto.Alias); + } + } + + if (noteTh.EndingConsonant != null && noteTh.Vowel != null) { + if (checkOtoUntilHit(new string[] { noteTh.Vowel + noteTh.EndingConsonant }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } + } else if (nextNeighbour != null && noteTh.Vowel != null) { + var nextTh = ParseInput(nextNeighbour.Value.lyric); + if (checkOtoUntilHit(new string[] { noteTh.Vowel + " " + nextTh.Consonant }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } + } + + if (prevNeighbour == null && tests.Count >= 1) { + if (checkOtoUntilHit(new string[] { "-" + tests[0] }, note, out var tempOto)) { + tests[0] = (tempOto.Alias); + } + } + + if (nextNeighbour == null && tests.Count >= 1) { + if (noteTh.EndingConsonant == null) { + if (checkOtoUntilHit(new string[] { noteTh.Vowel + "-" }, note, out var tempOto)) { + tests.Add(tempOto.Alias); + } + } else { + if (checkOtoUntilHit(new string[] { tests[tests.Count - 1] + "-" }, note, out var tempOto)) { + tests[tests.Count - 1] = (tempOto.Alias); + } + } + } + + if (tests.Count <= 0) { + if (checkOtoUntilHit(new string[] { currentLyric }, note, out var tempOto)) { + tests.Add(currentLyric); + } + } + + if (checkOtoUntilHit(tests.ToArray(), note, out var oto)) { + + var noteDuration = notes.Sum(n => n.duration); + + for (int i = 0; i < tests.ToArray().Length; i++) { + + int position = 0; + int vcPosition = noteDuration - 120; + + if (nextNeighbour != null && tests[i].Contains(" ")) { + var nextLyric = nextNeighbour.Value.lyric.Normalize(); + if (!string.IsNullOrEmpty(nextNeighbour.Value.phoneticHint)) { + nextLyric = nextNeighbour.Value.phoneticHint.Normalize(); + } + var nextTh = ParseInput(nextLyric); + var nextCheck = nextTh.Vowel; + if (nextTh.Consonant != null) { + nextCheck = nextTh.Consonant + nextTh.Vowel; + } + if (nextTh.Dipthong != null) { + nextCheck = nextTh.Consonant + nextTh.Dipthong + nextTh.Vowel; + } + var nextAttr = nextNeighbour.Value.phonemeAttributes?.FirstOrDefault(attr => attr.index == 0) ?? default; + if (singer.TryGetMappedOto(nextCheck, nextNeighbour.Value.tone + nextAttr.toneShift, nextAttr.voiceColor, out var nextOto)) { + if (oto.Overlap > 0) { + vcPosition = noteDuration - MsToTick(nextOto.Overlap) - MsToTick(nextOto.Preutter); + } + } + } + + + if (noteTh.Dipthong == null || tests.Count <= 2) { + if (i == 1) { + position = Math.Max((int)(noteDuration * 0.75), vcPosition); + } + } else { + if (i == 1) { + position = Math.Min((int)(noteDuration * 0.1), 60); + } else if (i == 2) { + position = Math.Max((int)(noteDuration * 0.75), vcPosition); + } + } + + phonemes.Add(new Phoneme { phoneme = tests[i], position = position }); + } + + } + + return new Result { + phonemes = phonemes.ToArray() + }; + } + + (string Consonant, string Dipthong, string Vowel, string EndingConsonant) ParseInput(string input) { + + input = WordToPhonemes(input); + + string consonant = null; + string dipthong = null; + string vowel = null; + string endingConsonant = null; + + if (input == null) { + return (null, null, null, null); + } + + if (input.Length >= 3) { + foreach (var dip in diphthongs) { + if (input[1].ToString().Equals(dip) || input[2].ToString().Equals(dip)) { + dipthong = dip; + } + } + } + + foreach (var con in consonants) { + if (input.StartsWith(con)) { + if (consonant == null || consonant.Length < con.Length) { + consonant = con; + } + } + if (input.EndsWith(con)) { + if (endingConsonant == null || endingConsonant.Length < con.Length) { + endingConsonant = con; + } + } + } + + foreach (var vow in vowels) { + if (input.Contains(vow)) { + if (vowel == null || vowel.Length < vow.Length) { + vowel = vow; + } + } + } + + return (consonant, dipthong, vowel, endingConsonant); + } + + public string WordToPhonemes(string input) { + input.Replace(" ", ""); + input = RemoveInvalidLetters(input); + if (!Regex.IsMatch(input, "[ก-ฮ]")) { + return input; + } + foreach (var mapping in VowelMapping) { + string pattern = "^" + mapping.Key + .Replace("c", "([ก-ฮ][ลรว]?|อ[ย]?|ห[ก-ฮ]?)") + .Replace("x", "([ก-ฮ]?)") + "$"; + + var match = Regex.Match(input, pattern); + if (match.Success) { + string c = match.Groups[1].Value; + string x = match.Groups.Count > 2 ? match.Groups[2].Value : string.Empty; + if (c.Length >= 2 && (c.StartsWith("ห") || c.StartsWith("อ"))) { + c = c.Substring(1); + } + string cConverted = ConvertC(c); + string xConverted = ConvertX(x); + if (mapping.Value == "a" && input.Contains("ั") && x == "ว") { + return cConverted + "ua"; + } + if (mapping.Value == "e" && x == "ย") { + return cConverted + "3" + xConverted; + } + return cConverted + mapping.Value + xConverted; + } + } + if (input.Length == 1) { + return ConvertC(input) + "Q"; + } else if (input.Length == 2) { + return ConvertC(input[0].ToString()) + "o" + ConvertX(input[1].ToString()); + } else if (input.Length == 3) { + if (input[1] == 'ว') { + return ConvertC(input[0].ToString()) + "ua" + ConvertX(input[2].ToString()); + } else { + return ConvertC(input.Substring(0, 2).ToString()) + "o" + ConvertX(input[1].ToString()); + } + } else if (input.Length == 4) { + if (input[21] == 'ว') { + return ConvertC(input.Substring(0, 2).ToString()) + "ua" + ConvertX(input[3].ToString()); + } + } + return input; + } + + private string ConvertC(string input) { + if (string.IsNullOrEmpty(input)) return input; + char firstChar = input[0]; + char? secondChar = input.Length > 1 ? input[1] : (char?)null; + if (CMapping.ContainsKey(firstChar)) { + string firstCharConverted = CMapping[firstChar]; + if (secondChar != null && CMapping.ContainsKey((char)secondChar)) { + return firstCharConverted + CMapping[(char)secondChar]; + } + return firstCharConverted; + } + return input; + } + + private string ConvertX(string input) { + if (string.IsNullOrEmpty(input)) return input; + char firstChar = input[0]; + if (XMapping.ContainsKey(firstChar)) { + return XMapping[firstChar]; + } + return input; + } + + private string RemoveInvalidLetters(string input) { + input = Regex.Replace(input, ".์", ""); + input = Regex.Replace(input, "[่้๊๋็]", ""); + return input; + } + + } +} diff --git a/OpenUtau.Test/App/AppTest.cs b/OpenUtau.Test/App/AppTest.cs index 1ec506da0..bc9c02b07 100644 --- a/OpenUtau.Test/App/AppTest.cs +++ b/OpenUtau.Test/App/AppTest.cs @@ -1,11 +1,39 @@ using Xunit; +using Avalonia; +using Avalonia.Headless; +using Avalonia.Styling; +using OpenUtau.App; + +[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))] + +public class TestAppBuilder { + public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .UseHeadless(new AvaloniaHeadlessPlatformOptions()); +} namespace OpenUtau.App { public class AppTest { [Fact] public void BuildTest() { - Assert.False(typeof(OpenUtau.App.App).IsAbstract); - Assert.False(typeof(OpenUtau.App.Program).IsAbstract); + Assert.False(typeof(App).IsAbstract); + Assert.False(typeof(Program).IsAbstract); + } + + [Fact] + public void StringsTest() { + var appBuilder = TestAppBuilder.BuildAvaloniaApp() + .SetupWithoutStarting(); + var app = appBuilder.Instance as App; + Assert.NotNull(app); + + var languages = App.GetLanguages(); + Assert.True(languages.Count > 1); + Assert.Contains("en-US", languages.Keys); + Assert.Contains("zh-CN", languages.Keys); + Assert.Contains("ja-JP", languages.Keys); + foreach (var pair in languages) { + Assert.NotNull(pair.Value); + } } } } diff --git a/OpenUtau.Test/OpenUtau.Test.csproj b/OpenUtau.Test/OpenUtau.Test.csproj index a04da8565..fc0d970a6 100644 --- a/OpenUtau.Test/OpenUtau.Test.csproj +++ b/OpenUtau.Test/OpenUtau.Test.csproj @@ -11,6 +11,7 @@ + diff --git a/OpenUtau.nsi b/OpenUtau.nsi index 7914954a6..6806ae38d 100644 --- a/OpenUtau.nsi +++ b/OpenUtau.nsi @@ -45,7 +45,7 @@ ; MUI end ------ Name "${PRODUCT_NAME} ${PRODUCT_VERSION}" -OutFile "OpenUtau-win-x64.exe" +OutFile "OpenUtau-win-${ARCH}.exe" InstallDir "$PROGRAMFILES64\OpenUtau" ShowInstDetails show ShowUnInstDetails show @@ -57,7 +57,7 @@ FunctionEnd Section "MainSection" SEC01 SetOutPath "$INSTDIR" SetOverwrite ifnewer - File "bin\win-x64\*" + File "bin\win-${ARCH}\*" SectionEnd Section -AdditionalIcons @@ -86,6 +86,12 @@ Section -Post WriteRegStr HKCR "OpenUtauFile\shell\open\command" "" `"$INSTDIR\OpenUtau.exe" "%1"` SectionEnd +Section "VC Redist" + SetOutPath "$INSTDIR" + File "vc_redist.${ARCH}.exe" + ExecWait "$INSTDIR\vc_redist.${ARCH}.exe" + Delete "$INSTDIR\vc_redist.${ARCH}.exe" +SectionEnd Function un.onUninstSuccess HideWindow diff --git a/OpenUtau/App.axaml.cs b/OpenUtau/App.axaml.cs index 7a72b7474..2d0c89141 100644 --- a/OpenUtau/App.axaml.cs +++ b/OpenUtau/App.axaml.cs @@ -126,9 +126,9 @@ public static void InitAudio() { Log.Information("Initializing audio."); if (!OS.IsWindows() || Core.Util.Preferences.Default.PreferPortAudio) { try { - PlaybackManager.Inst.AudioOutput = new Audio.PortAudioOutput(); + PlaybackManager.Inst.AudioOutput = new Audio.MiniAudioOutput(); } catch (Exception e1) { - Log.Error(e1, "Failed to init PortAudio"); + Log.Error(e1, "Failed to init MiniAudio"); } } else { try { diff --git a/OpenUtau/OpenUtau.csproj b/OpenUtau/OpenUtau.csproj index e83d1b36b..d62d5a316 100644 --- a/OpenUtau/OpenUtau.csproj +++ b/OpenUtau/OpenUtau.csproj @@ -77,18 +77,25 @@ - - + + + + + + + + + NotePropertyExpression.axaml diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index a14acd3a9..afa89e2ee 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -122,6 +122,7 @@ Vietnamese Chinese Cantonese + Thai Apply Cancel @@ -298,6 +299,7 @@ Warning: this option removes custom presets. Remove tail "-" Remove tail "R" Reset phoneme aliases + Reset notes to default Reset all parameters Reset all expressions Reset phoneme timings @@ -398,7 +400,7 @@ Warning: this option removes custom presets. Stationary Cursor Audio Backend Automatic - PortAudio + MiniAudio Auto-Scroll Margin Playback Device On Pausing diff --git a/OpenUtau/Strings/Strings.de-DE.axaml b/OpenUtau/Strings/Strings.de-DE.axaml index 46d668c0a..66ec7f352 100644 --- a/OpenUtau/Strings/Strings.de-DE.axaml +++ b/OpenUtau/Strings/Strings.de-DE.axaml @@ -115,6 +115,7 @@ Vietnamesisch Chinesisch Kantonesisch + Thailändisch Anwenden Abbrechen @@ -381,7 +382,6 @@ Warnung: Diese Option entfernt alle benutzerdefinierten Einstellungen.Stationärer Cursor Automatisch - Auto-Scroll Abstand Wiedergabegerät Beim Pausieren diff --git a/OpenUtau/Strings/Strings.es-ES.axaml b/OpenUtau/Strings/Strings.es-ES.axaml index 5a042fefe..c9b0971dc 100644 --- a/OpenUtau/Strings/Strings.es-ES.axaml +++ b/OpenUtau/Strings/Strings.es-ES.axaml @@ -115,6 +115,7 @@ vietnamita chino cantonés + tailandés @@ -381,7 +382,6 @@ Warning: this option removes custom presets.--> - Posición del cursor Dispositivo de reproducción diff --git a/OpenUtau/Strings/Strings.es-MX.axaml b/OpenUtau/Strings/Strings.es-MX.axaml index efc9274db..172a550a6 100644 --- a/OpenUtau/Strings/Strings.es-MX.axaml +++ b/OpenUtau/Strings/Strings.es-MX.axaml @@ -115,6 +115,7 @@ vietnamita chino cantonés + tailandés Aplicar Cancelar @@ -377,7 +378,6 @@ Advertencia: Esta opción eliminará las bases personalizadas. Cursor fijo Backend de audio Escoger automáticamente - Posición del cursor Dispositivo de reproducción Cuando se pausa... diff --git a/OpenUtau/Strings/Strings.fi-FI.axaml b/OpenUtau/Strings/Strings.fi-FI.axaml index 9d64ea706..931e8e8f0 100644 --- a/OpenUtau/Strings/Strings.fi-FI.axaml +++ b/OpenUtau/Strings/Strings.fi-FI.axaml @@ -115,6 +115,7 @@ vietnam kiina kantoninkiina + thaikieli @@ -381,7 +382,6 @@ Warning: this option removes custom presets.--> - Äänentoistolaite diff --git a/OpenUtau/Strings/Strings.fr-FR.axaml b/OpenUtau/Strings/Strings.fr-FR.axaml index a4b6aca9f..7bedc4ffa 100644 --- a/OpenUtau/Strings/Strings.fr-FR.axaml +++ b/OpenUtau/Strings/Strings.fr-FR.axaml @@ -115,6 +115,7 @@ vietnamien chinois cantonais + thaïlandais Appliquer Annuler @@ -377,7 +378,6 @@ Attention: cela va effacer le préréglage. Curseur fixe Automatique - Position du curseur Audio de sortie En pause diff --git a/OpenUtau/Strings/Strings.id-ID.axaml b/OpenUtau/Strings/Strings.id-ID.axaml index 500f8ece1..8f4c7d90a 100644 --- a/OpenUtau/Strings/Strings.id-ID.axaml +++ b/OpenUtau/Strings/Strings.id-ID.axaml @@ -115,6 +115,7 @@ Vietnam Tionghoa Kanton + Thai Terapkan Batalkan diff --git a/OpenUtau/Strings/Strings.it-IT.axaml b/OpenUtau/Strings/Strings.it-IT.axaml index b1e4e0c0c..8c5e8c903 100644 --- a/OpenUtau/Strings/Strings.it-IT.axaml +++ b/OpenUtau/Strings/Strings.it-IT.axaml @@ -115,6 +115,7 @@ vietnamita cinese cantonese + thailandese Applica Cancella @@ -381,7 +382,6 @@ Tieni premuto Ctrl per selezionare Cursore Immobile Backend Audio Automatico - Posizione del cursore Dispositivo audio Pausa alla posizione corrente diff --git a/OpenUtau/Strings/Strings.ja-JP.axaml b/OpenUtau/Strings/Strings.ja-JP.axaml index 8d9b32fb8..dcfec1fc3 100644 --- a/OpenUtau/Strings/Strings.ja-JP.axaml +++ b/OpenUtau/Strings/Strings.ja-JP.axaml @@ -120,6 +120,7 @@ ベトナム語 中国語 広東語 + タイ語 適用 キャンセル @@ -395,7 +396,6 @@ カーソル追従 オーディオバックエンド 自動 - カーソルの場所 再生デバイス 一時停止した時のカーソル diff --git a/OpenUtau/Strings/Strings.ko-KR.axaml b/OpenUtau/Strings/Strings.ko-KR.axaml index ae5c0ed87..6ba7cfb64 100644 --- a/OpenUtau/Strings/Strings.ko-KR.axaml +++ b/OpenUtau/Strings/Strings.ko-KR.axaml @@ -115,6 +115,7 @@ 베트남어 중국어 광둥어 + 태국어 적용 취소 @@ -382,7 +383,6 @@ 커서 스크롤 오디오 백엔드 자동 - 자동 스크롤 여백 오디오 출력 장치 일시정지할 때 diff --git a/OpenUtau/Strings/Strings.nl-NL.axaml b/OpenUtau/Strings/Strings.nl-NL.axaml index 964f1c320..433f94d91 100644 --- a/OpenUtau/Strings/Strings.nl-NL.axaml +++ b/OpenUtau/Strings/Strings.nl-NL.axaml @@ -115,6 +115,7 @@ Vietnamees Chinees Kantonees + Thais Toepassen Annuleer @@ -377,7 +378,6 @@ Ctrl ingedrukt houden om te selecteren Vaste cursor Automatisch - Cursorpositie Afspeelapparaat Op pauze diff --git a/OpenUtau/Strings/Strings.pl-PL.axaml b/OpenUtau/Strings/Strings.pl-PL.axaml index c4aa4fc9d..881e33279 100644 --- a/OpenUtau/Strings/Strings.pl-PL.axaml +++ b/OpenUtau/Strings/Strings.pl-PL.axaml @@ -122,6 +122,7 @@ wietnamski chiński kantoński + tajski Zastosuj Anuluj @@ -398,7 +399,6 @@ Uwaga: ta opcja usuwa presety własne. Nieruchomy kursor Audio Backend Automatyczny - PortAudio Margines automatycznego przewijania Urządzenie odtwarzania Podczas pauzy diff --git a/OpenUtau/Strings/Strings.pt-BR.axaml b/OpenUtau/Strings/Strings.pt-BR.axaml index 91e09c323..8ba2d0538 100644 --- a/OpenUtau/Strings/Strings.pt-BR.axaml +++ b/OpenUtau/Strings/Strings.pt-BR.axaml @@ -115,6 +115,7 @@ vietnamita chinês cantonês + tailandês Aplicar Cancelar @@ -377,7 +378,6 @@ Segure Ctrl para selecionar Cursor Estacionário Back-end de áudio Automático - Posição do Cursor Dispositivo de Reprodução Quando Parar diff --git a/OpenUtau/Strings/Strings.ru-RU.axaml b/OpenUtau/Strings/Strings.ru-RU.axaml index 0f7f2d025..eb57472c6 100644 --- a/OpenUtau/Strings/Strings.ru-RU.axaml +++ b/OpenUtau/Strings/Strings.ru-RU.axaml @@ -115,6 +115,7 @@ вьетнамский китайский кантонский + тайский Применить Отмена @@ -377,7 +378,6 @@ Неподвижный курсор Звуковой бэкенд Автоматический - Поле автопрокрутки Устройство воспроизведения На паузе diff --git a/OpenUtau/Strings/Strings.th-TH.axaml b/OpenUtau/Strings/Strings.th-TH.axaml index b5823ac0d..e46175350 100644 --- a/OpenUtau/Strings/Strings.th-TH.axaml +++ b/OpenUtau/Strings/Strings.th-TH.axaml @@ -115,6 +115,7 @@ เวียดนาม จีน กวางตุ้ง + ไทย ใช้งาน ยกเลิก @@ -377,7 +378,6 @@ ระบบเสียงหลังบ้าน อัตโนมัติ - ระยะขอบเลื่อนอัตโนมัติ เครื่องเล่นซ้ำ หยุดชั่วคราว diff --git a/OpenUtau/Strings/Strings.vi-VN.axaml b/OpenUtau/Strings/Strings.vi-VN.axaml index dfe6b4808..e903299f6 100644 --- a/OpenUtau/Strings/Strings.vi-VN.axaml +++ b/OpenUtau/Strings/Strings.vi-VN.axaml @@ -115,6 +115,7 @@ Tiếng Việt Tiếng Trung Tiếng Quảng Đông + Tiếng Thái Áp dụng Huỷ bỏ @@ -377,7 +378,6 @@ Nhấn giữ Ctrl để chọn nhiều nốt Con trỏ đứng ở một chỗ Backend âm thanh Tự động - Vị trí đường phát nhạc Thiết bị phát nhạc Khi dừng phát nhạc diff --git a/OpenUtau/Strings/Strings.zh-CN.axaml b/OpenUtau/Strings/Strings.zh-CN.axaml index 298d33c4c..c90c3511e 100644 --- a/OpenUtau/Strings/Strings.zh-CN.axaml +++ b/OpenUtau/Strings/Strings.zh-CN.axaml @@ -115,6 +115,7 @@ 越南语 中文 中文(粤语) + 泰语 应用 取消 @@ -378,7 +379,6 @@ 固定播放标记 音频后端 自动 - PortAudio 自动滚动边界 回放设备 暂停时 diff --git a/OpenUtau/Strings/Strings.zh-TW.axaml b/OpenUtau/Strings/Strings.zh-TW.axaml index 580e84fbc..a1e89f00f 100644 --- a/OpenUtau/Strings/Strings.zh-TW.axaml +++ b/OpenUtau/Strings/Strings.zh-TW.axaml @@ -115,6 +115,7 @@ 越南文 中文 粵語 + 泰語 套用 取消 @@ -377,7 +378,6 @@ 固定游標位置 音訊後端 自動 - 開始捲動位置 播放裝置 暫停時 diff --git a/OpenUtau/Views/MainWindow.axaml.cs b/OpenUtau/Views/MainWindow.axaml.cs index fe5835244..7180f6f07 100644 --- a/OpenUtau/Views/MainWindow.axaml.cs +++ b/OpenUtau/Views/MainWindow.axaml.cs @@ -314,7 +314,8 @@ async void OnMenuImportTracks(object sender, RoutedEventArgs args) { FilePicker.USTX, FilePicker.VSQX, FilePicker.UST, - FilePicker.MIDI); + FilePicker.MIDI, + FilePicker.UFDATA); if (files == null || files.Length == 0) { return; } diff --git a/OpenUtau/Views/PianoRollWindow.axaml.cs b/OpenUtau/Views/PianoRollWindow.axaml.cs index 2c1b9ec0f..3e760ff2c 100644 --- a/OpenUtau/Views/PianoRollWindow.axaml.cs +++ b/OpenUtau/Views/PianoRollWindow.axaml.cs @@ -130,6 +130,7 @@ await MessageBox.ShowProcessing(this, $"{name} - ? / ?", new ResetVibratos(), new ClearTimings(), new ResetAliases(), + new ResetAll(), }.Select(edit => new MenuItemViewModel() { Header = ThemeManager.GetString(edit.Name), Command = noteBatchEditCommand, diff --git a/OpenUtau/Views/PreferencesDialog.axaml b/OpenUtau/Views/PreferencesDialog.axaml index 69040dd1b..35d1524a3 100644 --- a/OpenUtau/Views/PreferencesDialog.axaml +++ b/OpenUtau/Views/PreferencesDialog.axaml @@ -45,7 +45,7 @@ - + diff --git a/appcast.py b/appcast.py new file mode 100644 index 000000000..22f1d6fbd --- /dev/null +++ b/appcast.py @@ -0,0 +1,41 @@ +import argparse +from datetime import datetime + +def main(): + parser = argparse.ArgumentParser('Writes Appcast XML file') + parser.add_argument('-v', '--version', help='Version number', required=True) + parser.add_argument('-o', '--os', help='OS name', required=True) + parser.add_argument('-r', '--rid', help='RID', required=True) + parser.add_argument('-f', '--file', help='File name', required=True) + args = parser.parse_args() + + appcast_ver = args.version + appcast_os = args.os + appcast_rid = args.rid + appcast_file = args.file + + xml = ''' + + + OpenUtau + en + + OpenUtau %s + %s + + + +''' % (appcast_ver, datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z"), + appcast_ver, appcast_file, appcast_ver, appcast_ver, appcast_os) + + with open("appcast.%s.xml" % (appcast_rid), 'w') as f: + f.write(xml) + + +if __name__ == '__main__': + main() diff --git a/appveyor.yml b/appveyor.yml index d70160f63..f2ceff8c1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,7 +8,6 @@ image: - Ubuntu branches: only: - - master - stable skip_commits: files: diff --git a/cpp/.bazelrc b/cpp/.bazelrc index 1c14c83e2..8c924f44a 100644 --- a/cpp/.bazelrc +++ b/cpp/.bazelrc @@ -1,5 +1,17 @@ -build --enable_platform_specific_config +common --enable_platform_specific_config +common --experimental_cc_shared_library +common --enable_bzlmod + +run --define absl=1 +test --define absl=1 build:linux --cxxopt='-std=c++17' build:macos --cxxopt='-std=c++17' +build:macos --macos_minimum_os=10.12 build:windows --cxxopt='/std:c++17' + +build:ubuntu-aarch64 --cxxopt='-std=c++17' +build:ubuntu-aarch64 --linkopt='-lm' +build:ubuntu-aarch64 --crosstool_top=//toolchain:arm_suite +build:ubuntu-aarch64 --cpu=aarch64 +build:ubuntu-aarch64 --compiler=gcc diff --git a/cpp/.bazelversion b/cpp/.bazelversion index 2f963cd6d..4be2c727a 100644 --- a/cpp/.bazelversion +++ b/cpp/.bazelversion @@ -1 +1 @@ -7.0.2 \ No newline at end of file +6.5.0 \ No newline at end of file diff --git a/cpp/MODULE.bazel b/cpp/MODULE.bazel index 3eb4975ab..b0a16f001 100644 --- a/cpp/MODULE.bazel +++ b/cpp/MODULE.bazel @@ -1,5 +1,7 @@ -"""Worldline""" -module(name = "worldline") +"""OpenUtau C++ module.""" +module(name = "openutau-cpp") bazel_dep(name = "abseil-cpp", version = "20240116.1", repo_name = "absl") bazel_dep(name = "googletest", version = "1.14.0", repo_name = "gtest") + +bazel_dep(name = "xxhash", version = "0.8.2") \ No newline at end of file diff --git a/cpp/WORKSPACE.bazel b/cpp/WORKSPACE.bazel index 55068b3e0..ca6f0e4ae 100644 --- a/cpp/WORKSPACE.bazel +++ b/cpp/WORKSPACE.bazel @@ -45,3 +45,11 @@ http_archive( strip_prefix = "World-f8dd5fb289db6a7f7f704497752bf32b258f9151", urls = ["https://github.com/mmorise/World/archive/f8dd5fb289db6a7f7f704497752bf32b258f9151.zip"], ) + +http_archive( + name = "miniaudio", + build_file = "@//third_party:miniaudio.BUILD", + sha256 = "cbde908871e2619115fd216c74235265348060fe7d340f980cd14342e88d7f72", + strip_prefix = "miniaudio-0.11.21", + urls = ["https://github.com/mackron/miniaudio/archive/refs/tags/0.11.21.zip"], +) diff --git a/cpp/build_linux.sh b/cpp/build_linux.sh new file mode 100644 index 000000000..6c9f308b3 --- /dev/null +++ b/cpp/build_linux.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +setup() +{ + sudo apt-get install gcc-arm-linux-gnueabi g++-arm-linux-gnueabi binutils-arm-linux-gnueabi + sudo apt-get install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binutils-aarch64-linux-gnu +} + +build() +{ + mkdir -p ../runtimes/linux-$1/native + bazel build //worldline:worldline -c opt $2 + chmod +w bazel-bin/worldline/libworldline.so + cp bazel-bin/worldline/libworldline.so ../runtimes/linux-$1/native +} + +build x64 "--cpu=k8" +build arm64 "--config=ubuntu-aarch64" diff --git a/cpp/build_mac.sh b/cpp/build_mac.sh new file mode 100755 index 000000000..d91af8c5f --- /dev/null +++ b/cpp/build_mac.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +build() +{ + bazel build //worldline:worldline -c opt $2 + chmod +w bazel-bin/worldline/libworldline.dylib + cp bazel-bin/worldline/libworldline.dylib ../runtimes/osx/native/libworldline-$1.dylib +} + +mkdir -p ../runtimes/osx/native + +build x64 "--cpu=darwin_x86_64" +build arm64 "--cpu=darwin_arm64" + +lipo -create ../runtimes/osx/native/libworldline-x64.dylib ../runtimes/osx/native/libworldline-arm64.dylib -output ../runtimes/osx/native/libworldline.dylib +rm ../runtimes/osx/native/libworldline-x64.dylib ../runtimes/osx/native/libworldline-arm64.dylib diff --git a/cpp/build_win.bat b/cpp/build_win.bat new file mode 100644 index 000000000..7ad9301e6 --- /dev/null +++ b/cpp/build_win.bat @@ -0,0 +1,16 @@ +@goto :MAIN + +:BUILD +@echo Building %~1 + +if not exist ..\runtimes\%~1\native mkdir ..\runtimes\%~1\native +bazel build //worldline:worldline -c opt --cpu=%~2 +attrib -r bazel-bin\worldline\worldline.dll +copy bazel-bin\worldline\worldline.dll ..\runtimes\%~1\native + +@EXIT /B + +:MAIN +@call :BUILD win-x64 x64_windows +@call :BUILD win-x86 x64_x86_windows +@call :BUILD win-arm64 arm64_windows diff --git a/cpp/third_party/miniaudio.BUILD b/cpp/third_party/miniaudio.BUILD new file mode 100644 index 000000000..1817e77ae --- /dev/null +++ b/cpp/third_party/miniaudio.BUILD @@ -0,0 +1,7 @@ +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "miniaudio", + hdrs = ["miniaudio.h"], + includes = ["."], +) diff --git a/cpp/toolchain/BUILD.bazel b/cpp/toolchain/BUILD.bazel new file mode 100644 index 000000000..40af70299 --- /dev/null +++ b/cpp/toolchain/BUILD.bazel @@ -0,0 +1,35 @@ +load(":cc_toolchain_config.bzl", "cc_toolchain_config") + +package(default_visibility = ["//visibility:public"]) + +cc_toolchain_suite( + name = "arm_suite", + toolchains = { + "aarch64|gcc": ":aarch64_toolchain", + }, +) + +filegroup(name = "empty") + +cc_toolchain( + name = "aarch64_toolchain", + all_files = ":empty", + compiler_files = ":empty", + dwp_files = ":empty", + linker_files = ":empty", + objcopy_files = ":empty", + strip_files = ":empty", + supports_param_files = 0, + toolchain_config = ":aarch64_toolchain_config", + toolchain_identifier = "aarch64-toolchain", +) + +cc_toolchain_config( + name = "aarch64_toolchain_config", + cxx_builtin_include_directories = [ + "/usr/aarch64-linux-gnu/include", + "/usr/lib/gcc-cross/aarch64-linux-gnu/11/include", + ], + target_cpu = "aarch64", + tool_path_prefx = "/usr/bin/aarch64-linux-gnu-", +) diff --git a/cpp/toolchain/cc_toolchain_config.bzl b/cpp/toolchain/cc_toolchain_config.bzl new file mode 100644 index 000000000..d6978b527 --- /dev/null +++ b/cpp/toolchain/cc_toolchain_config.bzl @@ -0,0 +1,124 @@ +"toolchain rule" + +load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES") +load( + "@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl", + "feature", + "flag_group", + "flag_set", + "tool_path", +) + +all_link_actions = [ + ACTION_NAMES.cpp_link_executable, + ACTION_NAMES.cpp_link_dynamic_library, + ACTION_NAMES.cpp_link_nodeps_dynamic_library, +] + +def _impl(ctx): + tool_paths = [ + tool_path( + name = "gcc", + path = ctx.attr.tool_path_prefx + "gcc", + ), + tool_path( + name = "ld", + path = ctx.attr.tool_path_prefx + "ld", + ), + tool_path( + name = "ar", + path = ctx.attr.tool_path_prefx + "ar", + ), + tool_path( + name = "cpp", + path = ctx.attr.tool_path_prefx + "cpp", + ), + tool_path( + name = "gcov", + path = ctx.attr.tool_path_prefx + "gcov", + ), + tool_path( + name = "nm", + path = ctx.attr.tool_path_prefx + "nm", + ), + tool_path( + name = "objdump", + path = ctx.attr.tool_path_prefx + "objdump", + ), + tool_path( + name = "strip", + path = ctx.attr.tool_path_prefx + "strip", + ), + ] + + features = [ + feature(name = "supports_pic", enabled = True), + feature( + name = "default_linker_flags", + enabled = True, + flag_sets = [ + flag_set( + actions = all_link_actions, + flag_groups = ([ + flag_group( + flags = [ + "-lstdc++", + ], + ), + ]), + ), + ], + ), + feature( + name = "opt", + flag_sets = [ + flag_set( + actions = [ACTION_NAMES.c_compile, ACTION_NAMES.cpp_compile], + flag_groups = [ + flag_group( + flags = [ + "-g0", + "-O2", + "-DNDEBUG", + "-ffunction-sections", + "-fdata-sections", + ], + ), + ], + ), + flag_set( + actions = [ + ACTION_NAMES.cpp_link_dynamic_library, + ACTION_NAMES.cpp_link_nodeps_dynamic_library, + ACTION_NAMES.cpp_link_executable, + ], + flag_groups = [flag_group(flags = ["-Wl,--gc-sections"])], + ), + ], + ), + ] + + return cc_common.create_cc_toolchain_config_info( + ctx = ctx, + features = features, + cxx_builtin_include_directories = ctx.attr.cxx_builtin_include_directories, + toolchain_identifier = "local", + host_system_name = "local", + target_system_name = "local", + target_cpu = ctx.attr.target_cpu, + target_libc = "unknown", + compiler = "gcc", + abi_version = "unknown", + abi_libc_version = "unknown", + tool_paths = tool_paths, + ) + +cc_toolchain_config = rule( + implementation = _impl, + attrs = { + "cxx_builtin_include_directories": attr.string_list(), + "target_cpu": attr.string(), + "tool_path_prefx": attr.string(), + }, + provides = [CcToolchainConfigInfo], +) diff --git a/cpp/worldline/BUILD.bazel b/cpp/worldline/BUILD.bazel index 8640694eb..5a0866953 100644 --- a/cpp/worldline/BUILD.bazel +++ b/cpp/worldline/BUILD.bazel @@ -2,6 +2,48 @@ package( default_visibility = ["//visibility:public"], ) +cc_binary( + name = "audio_debug", + srcs = ["audio_debug.cc"], + deps = [ + ":audio_output_lib", + "@absl//absl/debugging:failure_signal_handler", + "@absl//absl/debugging:symbolize", + "@absl//absl/flags:flag", + "@absl//absl/flags:parse", + "@absl//absl/log", + "@absl//absl/log:check", + "@absl//absl/log:initialize", + "@absl//absl/strings", + "@absl//absl/strings:string_view", + "@miniaudio", + "@xxhash", + ], +) + +cc_library( + name = "audio_output_lib", + srcs = ["audio_output.cc"], + hdrs = ["audio_output.h"], + deps = [ + "@miniaudio", + "@xxhash", + ], +) + +cc_test( + name = "audio_output_test", + srcs = ["audio_output_test.cc"], + deps = [ + ":audio_output_lib", + "@absl//absl/log", + "@absl//absl/strings", + "@absl//absl/time", + "@gtest//:gtest_main", + "@xxhash", + ], +) + cc_library( name = "synth_request", hdrs = ["synth_request.h"], @@ -35,7 +77,10 @@ cc_library( cc_shared_library( name = "worldline", - deps = [":worldline_lib"], + deps = [ + ":audio_output_lib", + ":worldline_lib", + ], ) cc_test( diff --git a/cpp/worldline/audio_debug.cc b/cpp/worldline/audio_debug.cc new file mode 100644 index 000000000..a8aab5abf --- /dev/null +++ b/cpp/worldline/audio_debug.cc @@ -0,0 +1,74 @@ +#include + +#include "absl/debugging/failure_signal_handler.h" +#include "absl/debugging/symbolize.h" +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "absl/log/globals.h" +#include "absl/log/initialize.h" +#include "absl/log/log.h" +#include "absl/strings/escaping.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "miniaudio.h" +#include "worldline/audio_output.h" +#include "xxhash.h" + +int main(int argc, char** argv) { + absl::InitializeSymbolizer(argv[0]); + absl::FailureSignalHandlerOptions options; + absl::InstallFailureSignalHandler(options); + + absl::InitializeLog(); + absl::SetStderrThreshold(absl::LogSeverity::kInfo); + + absl::ParseCommandLine(argc, argv); + + for (int i = 0; i < ma_backend_coreaudio; i++) { + LOG(INFO) << "============"; + LOG(INFO) << "Trying backend: " << ma_get_backend_name((ma_backend)i); + + ma_context context; + ma_backend backends[1] = {(ma_backend)i}; + ma_result result = ma_context_init(backends, 1, NULL, &context); + + if (result != MA_SUCCESS) { + LOG(ERROR) << "Failed to initialize context"; + LOG(ERROR) << "Error: " << ma_result_description(result); + continue; + } + + ma_device_info* playback_device_infos; + ma_uint32 playback_device_count; + ma_device_info* capture_device_infos; + ma_uint32 capture_device_count; + + result = ma_context_get_devices( + &context, &playback_device_infos, &playback_device_count, + &capture_device_infos, &capture_device_count); + + if (result != MA_SUCCESS) { + LOG(ERROR) << "Failed to get devices"; + LOG(ERROR) << "Error: " << ma_result_description(result); + ma_context_uninit(&context); + continue; + } + LOG(INFO) << "Playback device count: " << playback_device_count; + + for (int j = 0; j < playback_device_count; j++) { + LOG(INFO) << "------------"; + LOG(INFO) << "Device: #" << j; + ma_device_info* info = &playback_device_infos[j]; + LOG(INFO) << "Device name: " << info->name; + LOG(INFO) << "Device name bytes: " + << absl::BytesToHexString(std::string_view(info->name)); + uint64_t id = XXH64(&(info->id), sizeof(ma_device_id), 0); + LOG(INFO) << "Device ID: " << absl::Hex(id); + } + } + + LOG(INFO) << "============"; + LOG(INFO) << "Done"; + + return 0; +} diff --git a/cpp/worldline/audio_output.cc b/cpp/worldline/audio_output.cc new file mode 100644 index 000000000..7579905d2 --- /dev/null +++ b/cpp/worldline/audio_output.cc @@ -0,0 +1,191 @@ +#include "worldline/audio_output.h" + +#include + +#define MINIAUDIO_IMPLEMENTATION +#include "miniaudio.h" +#include "xxhash.h" + +DLL_API int32_t ou_get_audio_device_infos(ou_audio_device_info_t* device_infos, + int32_t max_count) { + ma_context context; + ma_backend backends[1]; + + int32_t device_count = 0; + + for (int i = 0; i < ma_backend_null; i++) { + backends[0] = (ma_backend)i; + ma_result result = ma_context_init(backends, 1, NULL, &context); + if (result != MA_SUCCESS) { + continue; + } + + ma_device_info* playback_device_infos; + ma_uint32 playback_device_count; + ma_device_info* capture_device_infos; + ma_uint32 capture_device_count; + result = ma_context_get_devices( + &context, &playback_device_infos, &playback_device_count, + &capture_device_infos, &capture_device_count); + if (result != MA_SUCCESS) { + ma_context_uninit(&context); + continue; + } + + for (int j = 0; j < playback_device_count; j++) { + if (device_count >= max_count) { + device_count++; + break; + } + + ma_device_info* info = &playback_device_infos[j]; + device_infos[device_count].name = strdup(info->name); + device_infos[device_count].id = + XXH64(&(info->id), sizeof(ma_device_id), 0); + device_infos[device_count].api = + strdup(ma_get_backend_name(context.backend)); + device_infos[device_count].api_id = context.backend; + device_count++; + } + + ma_context_uninit(&context); + } + return device_count; +} + +DLL_API void ou_free_audio_device_infos(ou_audio_device_info_t* device_infos, + int32_t count) { + for (int32_t i = 0; i < count; i++) { + free(device_infos[i].name); + free(device_infos[i].api); + } +} + +static ou_audio_data_callback_t g_data_callback = NULL; + +static void silence(float* buffer, uint32_t channels, uint32_t frame_count) { + memset(buffer, 0, channels * frame_count * sizeof(float)); +} + +static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, + ma_uint32 frameCount) { + if (g_data_callback == NULL) { + silence((float*)pOutput, pDevice->playback.channels, frameCount); + } else { + g_data_callback((float*)pOutput, pDevice->playback.channels, frameCount); + } +} + +DLL_API ou_audio_context_t* ou_init_audio_device( + uint32_t api_id, uint64_t id, ou_audio_data_callback_t callback) { + ou_audio_context_t* result = new ou_audio_context_t(); + if (result == NULL) { + return NULL; + } + + ma_backend backends[1] = {(ma_backend)api_id}; + if (ma_context_init(backends, 1, NULL, &result->context) != MA_SUCCESS) { + delete result; + return NULL; + } + + ma_device_info* playback_device_infos; + ma_uint32 playback_device_count; + ma_device_info* capture_device_infos; + ma_uint32 capture_device_count; + if (ma_context_get_devices(&result->context, &playback_device_infos, + &playback_device_count, &capture_device_infos, + &capture_device_count) != MA_SUCCESS) { + ma_context_uninit(&result->context); + delete result; + return NULL; + } + + ma_device_config config = ma_device_config_init(ma_device_type_playback); + config.playback.format = ma_format_f32; + config.playback.channels = 2; + config.sampleRate = 44100; + g_data_callback = callback; + config.dataCallback = data_callback; + config.pUserData = result; + + for (ma_uint32 i = 0; i < playback_device_count; i++) { + ma_device_info* info = &playback_device_infos[i]; + if (XXH64(&(info->id), sizeof(ma_device_id), 0) == id) { + config.playback.pDeviceID = &info->id; + break; + } + } + + if (config.playback.pDeviceID == NULL) { + ma_context_uninit(&result->context); + delete result; + return NULL; + } + + if (ma_device_init(&result->context, &config, &result->device) != + MA_SUCCESS) { + ma_context_uninit(&result->context); + delete result; + return NULL; + } + + return result; +} + +DLL_API ou_audio_context_t* ou_init_audio_device_auto( + ou_audio_data_callback_t callback) { + ou_audio_context_t* result = new ou_audio_context_t(); + if (result == NULL) { + return NULL; + } + + ma_device_config config = ma_device_config_init(ma_device_type_playback); + config.playback.format = ma_format_f32; + config.playback.channels = 2; + config.sampleRate = 44100; + g_data_callback = callback; + config.dataCallback = data_callback; + config.pUserData = result; + + if (ma_device_init(NULL, &config, &result->device) != MA_SUCCESS) { + delete result; + return NULL; + } + + return result; +} + +DLL_API const char* ou_get_audio_device_api(ou_audio_context_t* context) { + ma_backend backend = context->device.pContext->backend; + return ma_get_backend_name(backend); +} + +DLL_API const char* ou_get_audio_device_name(ou_audio_context_t* context) { + return context->device.playback.name; +} + +DLL_API int ou_free_audio_device(ou_audio_context_t* context) { + bool release_context = !context->device.isOwnerOfContext; + ma_device_uninit(&context->device); + if (release_context) { + ma_result result = ma_context_uninit(&context->context); + if (result != MA_SUCCESS) { + return result; + } + } + delete context; + return 0; +} + +DLL_API int ou_audio_device_start(ou_audio_context_t* context) { + return ma_device_start(&context->device); +} + +DLL_API int ou_audio_device_stop(ou_audio_context_t* context) { + return ma_device_stop(&context->device); +} + +DLL_API const char* ou_audio_get_error_message(int error_code) { + return ma_result_description((ma_result)error_code); +} diff --git a/cpp/worldline/audio_output.h b/cpp/worldline/audio_output.h new file mode 100644 index 000000000..a34ad9454 --- /dev/null +++ b/cpp/worldline/audio_output.h @@ -0,0 +1,63 @@ +#ifndef WORLDLINE_AUDIO_OUTPUT_H +#define WORLDLINE_AUDIO_OUTPUT_H + +#include + +#include "miniaudio.h" + +#if defined(_MSC_VER) +#define DLL_API __declspec(dllexport) +#elif defined(__GNUC__) +#define DLL_API __attribute__((visibility("default"))) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +struct ou_audio_device_info_t { + char* name; + uint64_t id; + char* api; + uint32_t api_id; +}; + +struct ou_audio_context_t { + ma_context context; + ma_device device; +}; + +typedef void (*ou_audio_data_callback_t)(float* buffer, uint32_t channels, + uint32_t frame_count); + +DLL_API int32_t ou_get_audio_device_infos(ou_audio_device_info_t* device_infos, + int32_t max_count); + +DLL_API void ou_free_audio_device_infos(ou_audio_device_info_t* device_infos, + int32_t count); + +DLL_API ou_audio_context_t* ou_init_audio_device( + uint32_t api_id, uint64_t id, ou_audio_data_callback_t callback); + +DLL_API ou_audio_context_t* ou_init_audio_device_auto( + ou_audio_data_callback_t callback); + +DLL_API const char* ou_get_audio_device_api(ou_audio_context_t* context); + +// On windows returns string of local code page, except for WASAPI which returns UTF-8. +// On other platforms returns UTF-8. +DLL_API const char* ou_get_audio_device_name(ou_audio_context_t* context); + +DLL_API int ou_free_audio_device(ou_audio_context_t* context); + +DLL_API int ou_audio_device_start(ou_audio_context_t* context); + +DLL_API int ou_audio_device_stop(ou_audio_context_t* context); + +DLL_API const char* ou_audio_get_error_message(int error_code); + +#if defined(__cplusplus) +} +#endif + +#endif // WORLDLINE_AUDIO_OUTPUT_H diff --git a/cpp/worldline/audio_output_test.cc b/cpp/worldline/audio_output_test.cc new file mode 100644 index 000000000..e260fa3e6 --- /dev/null +++ b/cpp/worldline/audio_output_test.cc @@ -0,0 +1,60 @@ +#include "worldline/audio_output.h" + +#include "absl/log/log.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "xxhash.h" + +static void white_noise(float* buffer, uint32_t channels, + uint32_t frame_count) { + for (uint32_t i = 0; i < frame_count; i++) { + for (uint32_t j = 0; j < channels; j++) { + if (j == 0) { + buffer[i * channels + j] = (float)rand() / RAND_MAX * 2.0f - 1.0f; + buffer[i * channels + j] *= 0.1f; + } else { + buffer[i * channels + j] = buffer[i * channels + j - 1]; + } + } + } +} + +TEST(MiniAudioTest, Playback) { + uint32_t api_id = 0; + uint64_t id = 0; + + ou_audio_device_info_t device_infos[10]; + int32_t count = ou_get_audio_device_infos(device_infos, 10); + LOG(INFO) << "Device count: " << count; + for (int32_t i = 0; i < count; i++) { + LOG(INFO) << "Device " << i << ": " << device_infos[i].name + << " API: " << device_infos[i].api + << " API Id: " << device_infos[i].api_id << " Index: " << i + << " ID: " << absl::Hex(device_infos[i].id); + if (i == 0) { + api_id = device_infos[i].api_id; + id = device_infos[i].id; + } + } + ou_free_audio_device_infos(device_infos, count); + + ou_audio_context_t* context = ou_init_audio_device(api_id, id, &white_noise); + LOG(INFO) << "Device API: " << ou_get_audio_device_api(context); + LOG(INFO) << "Device Name: " << ou_get_audio_device_name(context); + ou_audio_device_start(context); + absl::SleepFor(absl::Seconds(1)); + ou_audio_device_stop(context); + ou_free_audio_device(context); + + absl::SleepFor(absl::Seconds(1)); + + context = ou_init_audio_device_auto(&white_noise); + LOG(INFO) << "Auto Device API: " << ou_get_audio_device_api(context); + LOG(INFO) << "Auto Device Name: " << ou_get_audio_device_name(context); + ou_audio_device_start(context); + absl::SleepFor(absl::Seconds(1)); + ou_audio_device_stop(context); + ou_free_audio_device(context); +} diff --git a/runtimes/linux-arm64/native/libworldline.so b/runtimes/linux-arm64/native/libworldline.so index 99c68b2f1..c3daafe41 100644 Binary files a/runtimes/linux-arm64/native/libworldline.so and b/runtimes/linux-arm64/native/libworldline.so differ diff --git a/runtimes/linux-x64/native/libportaudio.so b/runtimes/linux-x64/native/libportaudio.so deleted file mode 100644 index 1c447fc3c..000000000 Binary files a/runtimes/linux-x64/native/libportaudio.so and /dev/null differ diff --git a/runtimes/linux-x64/native/libworldline.so b/runtimes/linux-x64/native/libworldline.so index f2cc1eef3..0ff2cb90d 100644 Binary files a/runtimes/linux-x64/native/libworldline.so and b/runtimes/linux-x64/native/libworldline.so differ diff --git a/runtimes/osx/native/libportaudio.dylib b/runtimes/osx/native/libportaudio.dylib deleted file mode 100755 index 526ba19ab..000000000 Binary files a/runtimes/osx/native/libportaudio.dylib and /dev/null differ diff --git a/runtimes/osx/native/libworldline.dylib b/runtimes/osx/native/libworldline.dylib index a8dd71b00..63b57da05 100755 Binary files a/runtimes/osx/native/libworldline.dylib and b/runtimes/osx/native/libworldline.dylib differ diff --git a/runtimes/win-arm64/native/worldline.dll b/runtimes/win-arm64/native/worldline.dll new file mode 100644 index 000000000..853c8f9ca Binary files /dev/null and b/runtimes/win-arm64/native/worldline.dll differ diff --git a/runtimes/win-x64/native/portaudio.dll b/runtimes/win-x64/native/portaudio.dll deleted file mode 100644 index c77cabbc5..000000000 Binary files a/runtimes/win-x64/native/portaudio.dll and /dev/null differ diff --git a/runtimes/win-x64/native/worldline.dll b/runtimes/win-x64/native/worldline.dll index ecd707b79..f962e49a0 100644 Binary files a/runtimes/win-x64/native/worldline.dll and b/runtimes/win-x64/native/worldline.dll differ diff --git a/runtimes/win-x86/native/portaudio.dll b/runtimes/win-x86/native/portaudio.dll deleted file mode 100644 index 0683865f8..000000000 Binary files a/runtimes/win-x86/native/portaudio.dll and /dev/null differ diff --git a/runtimes/win-x86/native/worldline.dll b/runtimes/win-x86/native/worldline.dll index 3a0829f15..82fd27bdd 100644 Binary files a/runtimes/win-x86/native/worldline.dll and b/runtimes/win-x86/native/worldline.dll differ