Skip to content

Commit 40bd0af

Browse files
committed
Fix tone generator crackling (stakira#1641) fix
1 parent c82279d commit 40bd0af

File tree

3 files changed

+199
-47
lines changed

3 files changed

+199
-47
lines changed

OpenUtau.Core/PlaybackManager.cs

Lines changed: 174 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,155 @@
1414
using Serilog;
1515

1616
namespace OpenUtau.Core {
17-
public class SineGen : ISampleProvider {
17+
public class SineGenerator : ISampleProvider {
1818
public WaveFormat WaveFormat => waveFormat;
19-
public double Freq { get; set; }
20-
public bool Stop { get; set; }
2119
private WaveFormat waveFormat;
22-
private double phase;
23-
private double gain;
24-
public SineGen() {
25-
waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(44100, 1);
26-
Freq = 440;
27-
gain = 1;
20+
21+
private readonly double attackSampleCount;
22+
private readonly double releaseSampleCount;
23+
24+
public double freq { get; set; }
25+
26+
private int position;
27+
private int releasePosition = 0;
28+
private float gain = 1;
29+
30+
public bool isActive { get; private set; } = true;
31+
public bool isPlaying { get; private set; } = true;
32+
33+
public SineGenerator(double freq, float gain, int attackMs = 25, int releaseMs = 25) {
34+
waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(44100, 2);
35+
this.freq = freq;
36+
this.gain = gain;
37+
position = 0;
38+
39+
// Number of samples the attack & release fades take
40+
attackSampleCount = (attackMs / 1000.0f) * waveFormat.SampleRate;
41+
releaseSampleCount = (releaseMs / 1000.0f) * waveFormat.SampleRate;
2842
}
43+
2944
public int Read(float[] buffer, int offset, int count) {
30-
double delta = 2 * Math.PI * Freq / waveFormat.SampleRate;
31-
for (int i = 0; i < count; i++) {
32-
if (Stop) {
33-
gain = Math.Max(0, gain - 0.01);
45+
// Duplicate sample across two channels
46+
for (int i = 0; i < count / 2; i++) {
47+
float sample = GetNextSample();
48+
buffer[offset + (i * 2)] += (float)sample * gain;
49+
buffer[offset + (i * 2) + 1] += (float)sample * gain;
50+
}
51+
return count;
52+
}
53+
54+
private float GetNextSample() {
55+
double delta = 2 * Math.PI * freq / waveFormat.SampleRate;
56+
double sample = Math.Sin(position * delta);
57+
58+
// Calculate attack envelope
59+
sample *= Math.Clamp(position / attackSampleCount, 0, 1);
60+
61+
// Calculate release envelope
62+
double releaseEnvelope = 1;
63+
if (!isActive) {
64+
releaseEnvelope = Math.Clamp(1.0f - ((position - releasePosition) / releaseSampleCount), 0, 1);
65+
}
66+
sample *= releaseEnvelope;
67+
68+
if (releaseEnvelope < double.Epsilon) {
69+
// Stop sampling this generator if release is completed
70+
// Instance will be cleaned up later
71+
isPlaying = false;
72+
}
73+
74+
position++;
75+
return (float)sample * gain;
76+
}
77+
78+
public void Stop() {
79+
if (!isActive) return;
80+
81+
isActive = false;
82+
releasePosition = position;
83+
}
84+
}
85+
86+
public class ToneGenerator : ISignalSource {
87+
private Dictionary<double, SineGenerator> activeFrequencies = new Dictionary<double, SineGenerator>();
88+
private List<SineGenerator> inactiveFrequencies = new List<SineGenerator>();
89+
private readonly float gain = 0.4f;
90+
91+
private readonly object _lockObj = new object();
92+
93+
public ToneGenerator() { }
94+
95+
public ToneGenerator(float gain) {
96+
this.gain = gain;
97+
}
98+
99+
public bool IsReady(int position, int count) {
100+
return true;
101+
}
102+
103+
public int Mix(int position, float[] buffer, int offset, int count) {
104+
lock (_lockObj) {
105+
foreach (var freqEntry in activeFrequencies) {
106+
if (freqEntry.Value.isPlaying) {
107+
freqEntry.Value.Read(buffer, offset, count);
108+
}
34109
}
35-
if (gain == 0) {
36-
return i;
110+
foreach (var generator in inactiveFrequencies) {
111+
if (generator.isPlaying) {
112+
generator.Read(buffer, offset, count);
113+
}
37114
}
38-
phase += delta;
39-
double sampleValue = Math.Sin(phase) * 0.2 * gain;
40-
buffer[offset++] = (float)sampleValue;
41115
}
42-
return count;
116+
117+
return position + count;
118+
}
119+
public void StartTone(double freq) {
120+
if (activeFrequencies.ContainsKey(freq)) {
121+
if (activeFrequencies[freq].isActive) {
122+
// Don't cut off tone to replace with the same frequency
123+
// Should never happen
124+
return;
125+
}
126+
}
127+
128+
lock (_lockObj) {
129+
activeFrequencies[freq] = new SineGenerator(freq, gain);
130+
}
131+
}
132+
133+
public void EndTone(double freq) {
134+
if (activeFrequencies.ContainsKey(freq)) {
135+
activeFrequencies[freq].Stop();
136+
137+
lock (_lockObj) {
138+
// Move to inactive frequencies list
139+
inactiveFrequencies.Add(activeFrequencies[freq]);
140+
activeFrequencies.Remove(freq);
141+
}
142+
}
143+
144+
CleanupTones();
145+
}
146+
147+
public void EndAllTones() {
148+
foreach (var tone in activeFrequencies) {
149+
tone.Value.Stop();
150+
151+
lock (_lockObj) {
152+
// Move to inactive frequencies list
153+
inactiveFrequencies.Add(tone.Value);
154+
activeFrequencies.Remove(tone.Key);
155+
}
156+
}
157+
158+
159+
CleanupTones();
160+
}
161+
162+
private void CleanupTones() {
163+
lock (_lockObj) {
164+
inactiveFrequencies.RemoveAll(gen => !gen.isPlaying);
165+
}
43166
}
44167
}
45168

@@ -52,17 +175,24 @@ private PlaybackManager() {
52175
} catch (Exception e) {
53176
Log.Error(e, "Failed to release source temp.");
54177
}
178+
179+
toneGenerator = new ToneGenerator();
180+
editingMix = new MasterAdapter(toneGenerator);
55181
}
56182

183+
public readonly ToneGenerator toneGenerator;
57184
List<Fader> faders;
58185
MasterAdapter masterMix;
186+
MasterAdapter editingMix;
187+
59188
double startMs;
60189
public int StartTick => DocManager.Inst.Project.timeAxis.MsPosToTickPos(startMs);
61190
CancellationTokenSource renderCancellation;
62191

63192
public Audio.IAudioOutput AudioOutput { get; set; } = new Audio.DummyAudioOutput();
64-
public bool Playing => AudioOutput.PlaybackState == PlaybackState.Playing;
193+
public bool OutputActive => AudioOutput.PlaybackState == PlaybackState.Playing;
65194
public bool StartingToPlay { get; private set; }
195+
public bool PlayingMaster { get; private set; }
66196

67197
public void PlayTestSound() {
68198
masterMix = null;
@@ -71,15 +201,23 @@ public void PlayTestSound() {
71201
AudioOutput.Play();
72202
}
73203

74-
public SineGen PlayTone(double freq) {
75-
masterMix = null;
76-
AudioOutput.Stop();
77-
var sineGen = new SineGen() {
78-
Freq = freq,
79-
};
80-
AudioOutput.Init(sineGen);
81-
AudioOutput.Play();
82-
return sineGen;
204+
public void PlayTone(double freq) {
205+
toneGenerator.StartTone(freq);
206+
207+
// If nothing is playing, start editing mix
208+
if (!OutputActive) {
209+
AudioOutput.Stop();
210+
AudioOutput.Init(editingMix);
211+
AudioOutput.Play();
212+
}
213+
}
214+
215+
public void EndTone(double freq) {
216+
toneGenerator.EndTone(freq);
217+
}
218+
219+
public void EndAllTones() {
220+
toneGenerator.EndAllTones();
83221
}
84222

85223
public void PlayFile(string file) {
@@ -98,7 +236,7 @@ public void PlayFile(string file) {
98236
}
99237

100238
public void PlayOrPause(int tick = -1, int endTick = -1, int trackNo = -1) {
101-
if (Playing) {
239+
if (PlayingMaster) {
102240
PausePlayback();
103241
} else {
104242
Play(
@@ -111,6 +249,7 @@ public void PlayOrPause(int tick = -1, int endTick = -1, int trackNo = -1) {
111249

112250
public void Play(UProject project, int tick, int endTick = -1, int trackNo = -1) {
113251
if (AudioOutput.PlaybackState == PlaybackState.Paused) {
252+
PlayingMaster = true;
114253
AudioOutput.Play();
115254
return;
116255
}
@@ -121,13 +260,17 @@ public void Play(UProject project, int tick, int endTick = -1, int trackNo = -1)
121260

122261
public void StopPlayback() {
123262
AudioOutput.Stop();
263+
PlayingMaster = false;
124264
}
125265

126266
public void PausePlayback() {
127267
AudioOutput.Pause();
268+
PlayingMaster = false;
128269
}
129270

130271
private void StartPlayback(double startMs, MasterAdapter masterAdapter) {
272+
toneGenerator.EndAllTones();
273+
131274
this.startMs = startMs;
132275
var start = TimeSpan.FromMilliseconds(startMs);
133276
Log.Information($"StartPlayback at {start}");
@@ -158,7 +301,7 @@ private void Render(UProject project, int tick, int endTick, int trackNo) {
158301
}
159302

160303
public void UpdatePlayPos() {
161-
if (AudioOutput != null && AudioOutput.PlaybackState == PlaybackState.Playing && masterMix != null) {
304+
if (AudioOutput != null && AudioOutput.PlaybackState == PlaybackState.Playing && PlayingMaster) {
162305
double ms = (AudioOutput.GetPosition() / sizeof(float) - masterMix.Waited / 2) * 1000.0 / 44100;
163306
int tick = DocManager.Inst.Project.timeAxis.MsPosToTickPos(startMs + ms);
164307
DocManager.Inst.ExecuteCmd(new SetPlayPosTickNotification(tick, masterMix.IsWaiting));

OpenUtau/ViewModels/PlaybackViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public void SeekEnd() {
3232
public void PlayOrPause(int tick = -1, int endTick = -1, int trackNo = -1) {
3333
PlaybackManager.Inst.PlayOrPause(tick: tick, endTick: endTick, trackNo: trackNo);
3434
var lockStartTime = Convert.ToBoolean(Preferences.Default.LockStartTime);
35-
if (!PlaybackManager.Inst.Playing && !PlaybackManager.Inst.StartingToPlay && lockStartTime) {
35+
if (!PlaybackManager.Inst.OutputActive && !PlaybackManager.Inst.StartingToPlay && lockStartTime) {
3636
DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(PlaybackManager.Inst.StartTick, true));
3737
}
3838
}

OpenUtau/Views/NoteEditStates.cs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,29 @@ namespace OpenUtau.App.Views {
1515
class KeyboardPlayState {
1616
private readonly TrackBackground element;
1717
private readonly PianoRollViewModel vm;
18-
private SineGen? sineGen;
18+
private int activeTone;
19+
1920
public KeyboardPlayState(TrackBackground element, PianoRollViewModel vm) {
2021
this.element = element;
2122
this.vm = vm;
2223
}
2324
public void Begin(IPointer pointer, Point point) {
2425
pointer.Capture(element);
2526
var tone = vm.NotesViewModel.PointToTone(point);
26-
sineGen = PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(tone));
27+
PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(tone));
28+
activeTone = tone;
2729
}
2830
public void Update(IPointer pointer, Point point) {
2931
var tone = vm.NotesViewModel.PointToTone(point);
30-
if (sineGen != null) {
31-
sineGen.Freq = MusicMath.ToneToFreq(tone);
32+
if (activeTone != tone) {
33+
PlaybackManager.Inst.EndTone(MusicMath.ToneToFreq(activeTone));
34+
PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(tone));
35+
activeTone = tone;
3236
}
3337
}
3438
public void End(IPointer pointer, Point point) {
3539
pointer.Capture(null);
36-
if (sineGen != null) {
37-
sineGen.Stop = true;
38-
}
40+
PlaybackManager.Inst.EndTone(MusicMath.ToneToFreq(activeTone));
3941
}
4042
}
4143

@@ -208,8 +210,9 @@ public override void Update(IPointer pointer, Point point) {
208210

209211
class NoteDrawEditState : NoteEditState {
210212
private UNote? note;
211-
private SineGen? sineGen;
212213
private bool playTone;
214+
private int activeTone;
215+
213216
public NoteDrawEditState(
214217
Control control,
215218
PianoRollViewModel vm,
@@ -221,7 +224,12 @@ public override void Begin(IPointer pointer, Point point) {
221224
base.Begin(pointer, point);
222225
note = vm.NotesViewModel.MaybeAddNote(point, false);
223226
if (note != null && playTone) {
224-
sineGen = PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(note.tone));
227+
if (PlaybackManager.Inst.PlayingMaster) {
228+
// Stop playback if playing project
229+
PlaybackManager.Inst.StopPlayback();
230+
}
231+
activeTone = note.tone;
232+
PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(note.tone));
225233
}
226234
}
227235
public override void Update(IPointer pointer, Point point) {
@@ -235,8 +243,11 @@ public override void Update(IPointer pointer, Point point) {
235243
return;
236244
}
237245
int tone = notesVm.PointToTone(point);
238-
if (sineGen != null) {
239-
sineGen.Freq = MusicMath.ToneToFreq(tone);
246+
if (activeTone != tone) {
247+
// Tone has changed
248+
PlaybackManager.Inst.EndTone(MusicMath.ToneToFreq(activeTone));
249+
PlaybackManager.Inst.PlayTone(MusicMath.ToneToFreq(tone));
250+
activeTone = tone;
240251
}
241252
int deltaTone = tone - note.tone;
242253
int snapUnit = project.resolution * 4 / notesVm.SnapDiv;
@@ -271,9 +282,7 @@ public override void Update(IPointer pointer, Point point) {
271282
}
272283
public override void End(IPointer pointer, Point point) {
273284
base.End(pointer, point);
274-
if (sineGen != null) {
275-
sineGen.Stop = true;
276-
}
285+
PlaybackManager.Inst.EndTone(MusicMath.ToneToFreq(activeTone));
277286
}
278287
}
279288

@@ -1380,4 +1389,4 @@ public override void Update(IPointer pointer, Point point) {
13801389
lastPoint = point;
13811390
}
13821391
}
1383-
}
1392+
}

0 commit comments

Comments
 (0)