diff --git a/OpenUtau.Core/Audio/NAudioOutput.cs b/OpenUtau.Core/Audio/NAudioOutput.cs index 7231101c3..4ffaabb81 100644 --- a/OpenUtau.Core/Audio/NAudioOutput.cs +++ b/OpenUtau.Core/Audio/NAudioOutput.cs @@ -48,6 +48,7 @@ public void Init(ISampleProvider sampleProvider) { } waveOutEvent = new WaveOutEvent() { DeviceNumber = deviceNumber, + DesiredLatency = 100 }; waveOutEvent.Init(sampleProvider); } diff --git a/OpenUtau.Core/PlaybackManager.cs b/OpenUtau.Core/PlaybackManager.cs index a0b369094..46420a9b1 100644 --- a/OpenUtau.Core/PlaybackManager.cs +++ b/OpenUtau.Core/PlaybackManager.cs @@ -13,32 +13,155 @@ using Serilog; namespace OpenUtau.Core { - public class SineGen : ISampleProvider { + public class SineGenerator : ISampleProvider { public WaveFormat WaveFormat => waveFormat; - public double Freq { get; set; } - public bool Stop { get; set; } private WaveFormat waveFormat; - private double phase; - private double gain; - public SineGen() { - waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(44100, 1); - Freq = 440; - gain = 1; + + private readonly double attackSampleCount; + private readonly double releaseSampleCount; + + public double freq { get; set; } + + private int position; + private int releasePosition = 0; + private float gain = 1; + + public bool isActive { get; private set; } = true; + public bool isPlaying { get; private set; } = true; + + public SineGenerator(double freq, float gain, int attackMs = 25, int releaseMs = 25) { + waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(44100, 2); + this.freq = freq; + this.gain = gain; + position = 0; + + // Number of samples the attack & release fades take + attackSampleCount = (attackMs / 1000.0f) * waveFormat.SampleRate; + releaseSampleCount = (releaseMs / 1000.0f) * waveFormat.SampleRate; } + public int Read(float[] buffer, int offset, int count) { - double delta = 2 * Math.PI * Freq / waveFormat.SampleRate; - for (int i = 0; i < count; i++) { - if (Stop) { - gain = Math.Max(0, gain - 0.01); + // Duplicate sample across two channels + for (int i = 0; i < count / 2; i++) { + float sample = GetNextSample(); + buffer[offset + (i * 2)] += (float)sample * gain; + buffer[offset + (i * 2) + 1] += (float)sample * gain; + } + return count; + } + + private float GetNextSample() { + double delta = 2 * Math.PI * freq / waveFormat.SampleRate; + double sample = Math.Sin(position * delta); + + // Calculate attack envelope + sample *= Math.Clamp(position / attackSampleCount, 0, 1); + + // Calculate release envelope + double releaseEnvelope = 1; + if (!isActive) { + releaseEnvelope = Math.Clamp(1.0f - ((position - releasePosition) / releaseSampleCount), 0, 1); + } + sample *= releaseEnvelope; + + if (releaseEnvelope < double.Epsilon) { + // Stop sampling this generator if release is completed + // Instance will be cleaned up later + isPlaying = false; + } + + position++; + return (float)sample * gain; + } + + public void Stop() { + if (!isActive) return; + + isActive = false; + releasePosition = position; + } + } + + public class ToneGenerator : ISignalSource { + private Dictionary activeFrequencies = new Dictionary(); + private List inactiveFrequencies = new List(); + private readonly float gain = 0.4f; + + private readonly object _lockObj = new object(); + + public ToneGenerator() {} + + public ToneGenerator(float gain) { + this.gain = gain; + } + + public bool IsReady(int position, int count) { + return true; + } + + public int Mix(int position, float[] buffer, int offset, int count) { + lock (_lockObj) { + foreach (var freqEntry in activeFrequencies) { + if (freqEntry.Value.isPlaying) { + freqEntry.Value.Read(buffer, offset, count); + } } - if (gain == 0) { - return i; + foreach (var generator in inactiveFrequencies) { + if (generator.isPlaying) { + generator.Read(buffer, offset, count); + } } - phase += delta; - double sampleValue = Math.Sin(phase) * 0.2 * gain; - buffer[offset++] = (float)sampleValue; } - return count; + + return position + count; + } + public void StartTone(double freq) { + if (activeFrequencies.ContainsKey(freq)) { + if (activeFrequencies[freq].isActive) { + // Don't cut off tone to replace with the same frequency + // Should never happen + return; + } + } + + lock (_lockObj) { + activeFrequencies[freq] = new SineGenerator(freq, gain); + } + } + + public void EndTone(double freq) { + if (activeFrequencies.ContainsKey(freq)) { + activeFrequencies[freq].Stop(); + + lock (_lockObj) { + // Move to inactive frequencies list + inactiveFrequencies.Add(activeFrequencies[freq]); + activeFrequencies.Remove(freq); + } + } + + CleanupTones(); + } + + public void EndAllTones() { + foreach (var tone in activeFrequencies) { + tone.Value.Stop(); + + lock (_lockObj) { + // Move to inactive frequencies list + inactiveFrequencies.Add(tone.Value); + activeFrequencies.Remove(tone.Key); + } + } + + + CleanupTones(); + } + + private void CleanupTones() { + lock (_lockObj) { + inactiveFrequencies.RemoveAll(gen => !gen.isPlaying); + } } } @@ -51,17 +174,24 @@ private PlaybackManager() { } catch (Exception e) { Log.Error(e, "Failed to release source temp."); } + + toneGenerator = new ToneGenerator(); + editingMix = new MasterAdapter(toneGenerator); } + public readonly ToneGenerator toneGenerator; List faders; MasterAdapter masterMix; + MasterAdapter editingMix; + double startMs; public int StartTick => DocManager.Inst.Project.timeAxis.MsPosToTickPos(startMs); CancellationTokenSource renderCancellation; public Audio.IAudioOutput AudioOutput { get; set; } = new Audio.DummyAudioOutput(); - public bool Playing => AudioOutput.PlaybackState == PlaybackState.Playing; + public bool OutputActive => AudioOutput.PlaybackState == PlaybackState.Playing; public bool StartingToPlay { get; private set; } + public bool PlayingMaster { get; private set; } public void PlayTestSound() { masterMix = null; @@ -70,19 +200,27 @@ public void PlayTestSound() { AudioOutput.Play(); } - public SineGen PlayTone(double freq) { - masterMix = null; - AudioOutput.Stop(); - var sineGen = new SineGen() { - Freq = freq, - }; - AudioOutput.Init(sineGen); - AudioOutput.Play(); - return sineGen; + public void PlayTone(double freq) { + toneGenerator.StartTone(freq); + + // If nothing is playing, start editing mix + if (!OutputActive) { + AudioOutput.Stop(); + AudioOutput.Init(editingMix); + AudioOutput.Play(); + } + } + + public void EndTone(double freq) { + toneGenerator.EndTone(freq); + } + + public void EndAllTones() { + toneGenerator.EndAllTones(); } public void PlayOrPause(int tick = -1, int endTick = -1, int trackNo = -1) { - if (Playing) { + if (PlayingMaster) { PausePlayback(); } else { Play( @@ -95,23 +233,29 @@ public void PlayOrPause(int tick = -1, int endTick = -1, int trackNo = -1) { public void Play(UProject project, int tick, int endTick = -1, int trackNo = -1) { if (AudioOutput.PlaybackState == PlaybackState.Paused) { + PlayingMaster = true; AudioOutput.Play(); return; } AudioOutput.Stop(); Render(project, tick, endTick, trackNo); StartingToPlay = true; + PlayingMaster = true; } public void StopPlayback() { AudioOutput.Stop(); + PlayingMaster = false; } public void PausePlayback() { AudioOutput.Pause(); + PlayingMaster = false; } private void StartPlayback(double startMs, MasterAdapter masterAdapter) { + toneGenerator.EndAllTones(); + this.startMs = startMs; var start = TimeSpan.FromMilliseconds(startMs); Log.Information($"StartPlayback at {start}"); @@ -139,7 +283,7 @@ private void Render(UProject project, int tick, int endTick, int trackNo) { } public void UpdatePlayPos() { - if (AudioOutput != null && AudioOutput.PlaybackState == PlaybackState.Playing && masterMix != null) { + if (AudioOutput != null && AudioOutput.PlaybackState == PlaybackState.Playing && PlayingMaster) { double ms = (AudioOutput.GetPosition() / sizeof(float) - masterMix.Waited / 2) * 1000.0 / 44100; int tick = DocManager.Inst.Project.timeAxis.MsPosToTickPos(startMs + ms); DocManager.Inst.ExecuteCmd(new SetPlayPosTickNotification(tick, masterMix.IsWaiting)); diff --git a/OpenUtau/ViewModels/PlaybackViewModel.cs b/OpenUtau/ViewModels/PlaybackViewModel.cs index 04e5c6d2e..ae35820f1 100644 --- a/OpenUtau/ViewModels/PlaybackViewModel.cs +++ b/OpenUtau/ViewModels/PlaybackViewModel.cs @@ -32,7 +32,7 @@ public void SeekEnd() { public void PlayOrPause(int tick = -1, int endTick = -1, int trackNo = -1) { PlaybackManager.Inst.PlayOrPause(tick: tick, endTick: endTick, trackNo: trackNo); var lockStartTime = Convert.ToBoolean(Preferences.Default.LockStartTime); - if (!PlaybackManager.Inst.Playing && !PlaybackManager.Inst.StartingToPlay && lockStartTime) { + if (!PlaybackManager.Inst.OutputActive && !PlaybackManager.Inst.StartingToPlay && lockStartTime) { DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(PlaybackManager.Inst.StartTick, true)); } } diff --git a/OpenUtau/Views/NoteEditStates.cs b/OpenUtau/Views/NoteEditStates.cs index 4b61172b7..95720a8f7 100644 --- a/OpenUtau/Views/NoteEditStates.cs +++ b/OpenUtau/Views/NoteEditStates.cs @@ -15,7 +15,8 @@ namespace OpenUtau.App.Views { class KeyboardPlayState { private readonly TrackBackground element; private readonly PianoRollViewModel vm; - private SineGen? sineGen; + private int activeTone; + public KeyboardPlayState(TrackBackground element, PianoRollViewModel vm) { this.element = element; this.vm = vm; @@ -23,19 +24,20 @@ public KeyboardPlayState(TrackBackground element, PianoRollViewModel vm) { public void Begin(IPointer pointer, Point point) { pointer.Capture(element); var tone = vm.NotesViewModel.PointToTone(point); - sineGen = PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(tone)); + PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(tone)); + activeTone = tone; } public void Update(IPointer pointer, Point point) { var tone = vm.NotesViewModel.PointToTone(point); - if (sineGen != null) { - sineGen.Freq = MusicMath.ToneToFreq(tone); + if (activeTone != tone) { + PlaybackManager.Inst.EndTone(MusicMath.ToneToFreq(activeTone)); + PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(tone)); + activeTone = tone; } } public void End(IPointer pointer, Point point) { pointer.Capture(null); - if (sineGen != null) { - sineGen.Stop = true; - } + PlaybackManager.Inst.EndTone(MusicMath.ToneToFreq(activeTone)); } } @@ -208,8 +210,9 @@ public override void Update(IPointer pointer, Point point) { class NoteDrawEditState : NoteEditState { private UNote? note; - private SineGen? sineGen; private bool playTone; + private int activeTone; + public NoteDrawEditState( Control control, PianoRollViewModel vm, @@ -221,7 +224,12 @@ public override void Begin(IPointer pointer, Point point) { base.Begin(pointer, point); note = vm.NotesViewModel.MaybeAddNote(point, false); if (note != null && playTone) { - sineGen = PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(note.tone)); + if (PlaybackManager.Inst.PlayingMaster) { + // Stop playback if playing project + PlaybackManager.Inst.StopPlayback(); + } + activeTone = note.tone; + PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(note.tone)); } } public override void Update(IPointer pointer, Point point) { @@ -235,8 +243,11 @@ public override void Update(IPointer pointer, Point point) { return; } int tone = notesVm.PointToTone(point); - if (sineGen != null) { - sineGen.Freq = MusicMath.ToneToFreq(tone); + if (activeTone != tone) { + // Tone has changed + PlaybackManager.Inst.EndTone(MusicMath.ToneToFreq(activeTone)); + PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(tone)); + activeTone = tone; } int deltaTone = tone - note.tone; int snapUnit = project.resolution * 4 / notesVm.SnapDiv; @@ -271,9 +282,7 @@ public override void Update(IPointer pointer, Point point) { } public override void End(IPointer pointer, Point point) { base.End(pointer, point); - if (sineGen != null) { - sineGen.Stop = true; - } + PlaybackManager.Inst.EndTone(MusicMath.ToneToFreq(activeTone)); } }