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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions OpenUtau.Core/Audio/NAudioOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public void Init(ISampleProvider sampleProvider) {
}
waveOutEvent = new WaveOutEvent() {
DeviceNumber = deviceNumber,
DesiredLatency = 100
};
waveOutEvent.Init(sampleProvider);
}
Expand Down
206 changes: 175 additions & 31 deletions OpenUtau.Core/PlaybackManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double, SineGenerator> activeFrequencies = new Dictionary<double, SineGenerator>();
private List<SineGenerator> inactiveFrequencies = new List<SineGenerator>();
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);
}
}
}

Expand All @@ -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<Fader> 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;
Expand All @@ -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(
Expand All @@ -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}");
Expand Down Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion OpenUtau/ViewModels/PlaybackViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Expand Down
37 changes: 23 additions & 14 deletions OpenUtau/Views/NoteEditStates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,29 @@ 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;
}
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));
}
}

Expand Down Expand Up @@ -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,
Expand All @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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));
}
}

Expand Down
Loading