Skip to content
Open
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
144 changes: 138 additions & 6 deletions OpenUtau.Core/PlaybackManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public class ToneGenerator : ISignalSource {

private readonly object _lockObj = new object();

public ToneGenerator() {}
public ToneGenerator() { }

public ToneGenerator(float gain) {
this.gain = gain;
Expand Down Expand Up @@ -166,6 +166,134 @@ private void CleanupTones() {
}
}

public class MetronomeGenerator : ISignalSource {
private const int SampleRate = 44100;

private TimeAxis? timeAxis;
private int startTick;

public bool Enabled { get; set; }

public MetronomeGenerator() {
}

public void SetProject(UProject project, int startTick) {
timeAxis = project.timeAxis;
this.startTick = startTick;
}

public bool IsReady(int position, int count) => true;

public int Mix(int position, float[] buffer, int offset, int count) {
if (!Enabled || timeAxis == null) {
return position + count;
}

double startFrame = position / 2.0;
double endFrame = (position + count) / 2.0;

double startProjectMs =
timeAxis.TickPosToMsPos(startTick)
+ startFrame * 1000.0 / SampleRate;

double endProjectMs =
timeAxis.TickPosToMsPos(startTick)
+ endFrame * 1000.0 / SampleRate;

const double clickLengthMs = 50.0;
double searchStartMs = Math.Max(0, startProjectMs - clickLengthMs);
double searchStartTickExact = timeAxis.MsPosToNonExactTickPos(searchStartMs);
double endTickExact = timeAxis.MsPosToNonExactTickPos(endProjectMs);

int currentTick = (int)Math.Floor(searchStartTickExact);

timeAxis.TickPosToBarBeat(
currentTick,
out int bar,
out int beat,
out _);

int clickTick = timeAxis.BarBeatToTickPos(bar, beat);

if (clickTick < currentTick) {
timeAxis.NextBarBeat(
bar,
beat,
out bar,
out beat);

clickTick = timeAxis.BarBeatToTickPos(bar, beat);
}

int clickLengthSamples = (int)(SampleRate * clickLengthMs / 1000.0);

while (clickTick < endTickExact) {
double clickMs = timeAxis.TickPosToMsPos(clickTick);
double startMs = timeAxis.TickPosToMsPos(startTick);

double clickFrame = (clickMs - startMs) * SampleRate / 1000.0;
double relativeFrame = clickFrame - startFrame;
int frame = (int)Math.Round(relativeFrame);

if (frame + clickLengthSamples > 0 && frame < count / 2) {
AddClick(
buffer,
offset,
frame,
beat == 0,
count);
}

timeAxis.NextBarBeat(
bar,
beat,
out bar,
out beat);

clickTick = timeAxis.BarBeatToTickPos(bar, beat);
}

return position + count;
}

private static void AddClick(
float[] buffer,
int offset,
int frame,
bool accent,
int count) {

const double clickLengthMs = 50.0;
const float volume = 0.5f;

int clickLength = (int)(SampleRate * clickLengthMs / 1000.0);
double frequency = accent ? 1600.0 : 1000.0;
int bufferFrameCount = count / 2;

int startI = frame < 0 ? -frame : 0;

for (int i = startI; i < clickLength; ++i) {
int targetFrame = frame + i;

if (targetFrame >= bufferFrameCount) {
break;
}

double t = (double)i / SampleRate;

double envelope = Math.Exp(-t * 80.0);

float sample = (float)(
Math.Sin(2.0 * Math.PI * frequency * t)
* envelope
* volume);

buffer[offset + targetFrame * 2] += sample;
buffer[offset + targetFrame * 2 + 1] += sample;
}
}
}

public class PlaybackManager : SingletonBase<PlaybackManager>, ICmdSubscriber {
private PlaybackManager() {
DocManager.Inst.AddSubscriber(this);
Expand All @@ -177,14 +305,16 @@ private PlaybackManager() {
}

toneGenerator = new ToneGenerator();
metronomeGenerator = new MetronomeGenerator();
editingMix = new MasterAdapter(toneGenerator);
}

public readonly ToneGenerator toneGenerator;
public readonly MetronomeGenerator metronomeGenerator;
List<Fader> faders;
MasterAdapter masterMix;
MasterAdapter editingMix;

double startMs;
public int StartTick => DocManager.Inst.Project.timeAxis.MsPosToTickPos(startMs);
CancellationTokenSource renderCancellation;
Expand Down Expand Up @@ -230,15 +360,15 @@ public void PlayFile(string file) {
if (AudioOutput.PlaybackState == PlaybackState.Playing) {
AudioOutput.Stop();
}
try{
try {
var playSound = Wave.OpenFile(file);
AudioOutput.Init(playSound.ToSampleProvider());
} catch (Exception ex) {
Log.Error(ex, $"Failed to load sample {file}.");
return;
}
AudioOutput.Play();
}
}

public void PlayOrPause(int tick = -1, int endTick = -1, int trackNo = -1) {
if (PlayingMaster) {
Expand Down Expand Up @@ -294,8 +424,10 @@ private void StartPlayback(double startMs, MasterAdapter masterAdapter) {
toneGenerator.EndAllTones();

this.startMs = startMs;
var start = TimeSpan.FromMilliseconds(startMs);
Log.Information($"StartPlayback at {start}");
int startTick = DocManager.Inst.Project.timeAxis.MsPosToTickPos(startMs);
Log.Information($"StartPlayback at {startMs}");

metronomeGenerator.SetProject(DocManager.Inst.Project, startTick);
masterMix = masterAdapter;
AudioOutput.Stop();
AudioOutput.Init(masterMix);
Expand Down
2 changes: 1 addition & 1 deletion OpenUtau.Core/Render/RenderEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public Tuple<WaveMix, List<Fader>> RenderMixdown(TaskScheduler uiScheduler, ref
public Tuple<MasterAdapter, List<Fader>> RenderProject(TaskScheduler uiScheduler, ref CancellationTokenSource cancellation) {
double startMs = project.timeAxis.TickPosToMsPos(startTick);
var renderMixdownResult = RenderMixdown(uiScheduler, ref cancellation, wait: false);
var master = new MasterAdapter(renderMixdownResult.Item1);
var master = new MasterAdapter(renderMixdownResult.Item1, PlaybackManager.Inst.metronomeGenerator);
master.SetPosition((int)(startMs * 44100 / 1000) * 2);
return Tuple.Create(master, renderMixdownResult.Item2);
}
Expand Down
5 changes: 4 additions & 1 deletion OpenUtau.Core/SignalChain/MasterAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ namespace OpenUtau.Core.SignalChain {
class MasterAdapter : ISampleProvider {
private readonly WaveFormat waveFormat;
private readonly ISignalSource source;
private readonly ISignalSource? metronome;
private int position;

public WaveFormat WaveFormat => waveFormat;
public int Waited { get; private set; }
public bool IsWaiting { get; private set; }
public MasterAdapter(ISignalSource source) {
public MasterAdapter(ISignalSource source, ISignalSource? metronome = null) {
waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(44100, 2);
this.source = source;
this.metronome = metronome;
}

public int Read(float[] buffer, int offset, int count) {
Expand All @@ -25,6 +27,7 @@ public int Read(float[] buffer, int offset, int count) {
return count;
} else {
int pos = source.Mix(position, buffer, offset, count);
metronome?.Mix(position, buffer, offset, count);
int n = Math.Max(0, pos - position);
position = pos;
IsWaiting = false;
Expand Down
2 changes: 2 additions & 0 deletions OpenUtau/Strings/Strings.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,8 @@ Warning: this option removes custom presets.</system:String>
<system:String x:Key="pianoroll.tooltip.preutter">Preutter</system:String>
<system:String x:Key="pianoroll.tooltip.release">Release time delta</system:String>

<system:String x:Key="playback.toggle.metronom">Toggle Metronom</system:String>

<system:String x:Key="prefs.advanced">Advanced</system:String>
<system:String x:Key="prefs.advanced.beta">Beta</system:String>
<system:String x:Key="prefs.advanced.lyricshelper">Lyrics Helper</system:String>
Expand Down
2 changes: 2 additions & 0 deletions OpenUtau/Strings/Strings.ja-JP.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@
Ctrl長押しで選択</system:String>
<system:String x:Key="pianoroll.tool.selectionv2">選択ツール (1)</system:String>

<system:String x:Key="playback.toggle.metronom">メトロノーム</system:String>

<system:String x:Key="prefs.advanced">高度な設定</system:String>
<system:String x:Key="prefs.advanced.beta">ベータ版</system:String>
<system:String x:Key="prefs.advanced.lyricshelper">歌詞入力ヘルパー</system:String>
Expand Down
31 changes: 18 additions & 13 deletions OpenUtau/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public class MainWindowViewModel : ViewModelBase, ICmdSubscriber {
[Reactive] public string UndoText { get; set; } = ThemeManager.GetString("menu.edit.undo");
[Reactive] public string RedoText { get; set; } = ThemeManager.GetString("menu.edit.redo");

[Reactive] public bool IsMetronomeEnabled { get; set; } = PlaybackManager.Inst.metronomeGenerator.Enabled;

private ObservableCollectionExtended<MenuItemViewModel> openRecentMenuItems
= new ObservableCollectionExtended<MenuItemViewModel>();
private ObservableCollectionExtended<MenuItemViewModel> openTemplatesMenuItems
Expand Down Expand Up @@ -136,6 +138,9 @@ public MainWindowViewModel() {
PianoRollMaxHeight = x ? double.PositiveInfinity : 0;
PianoRollMinHeight = x ? ViewConstants.PianoRollMinHeight : 0;
});
this.WhenAnyValue(x => x.IsMetronomeEnabled)
.Subscribe(enabled =>
PlaybackManager.Inst.metronomeGenerator.Enabled = enabled);
}

public void Undo() {
Expand Down Expand Up @@ -187,7 +192,7 @@ public void InitProject(MainWindow window) {
HasRecovery = true;
return;
}

var args = Environment.GetCommandLineArgs();
if (args.Length == 2 && File.Exists(args[1])) {
try {
Expand Down Expand Up @@ -269,7 +274,7 @@ public void SaveProject(string file = "") {
this.RaisePropertyChanged(nameof(Title));
}

public void ImportTracks(UProject[] loadedProjects, bool importTempo){
public void ImportTracks(UProject[] loadedProjects, bool importTempo) {
if (loadedProjects == null || loadedProjects.Length < 1) {
return;
}
Expand Down Expand Up @@ -314,7 +319,7 @@ public void ImportMidi(string file) {
var track = new UTrack(project);
track.TrackNo = project.tracks.Count;
part.trackNo = track.TrackNo;
if(part.name != "New Part"){
if (part.name != "New Part") {
track.TrackName = part.name;
}
part.AfterLoad(project, track);
Expand Down Expand Up @@ -400,7 +405,7 @@ public void RefreshTimelineContextMenu(int tick) {
/// Remap a tick position from the old time axis to the new time axis without changing its absolute position (in ms).
/// Note that this can only be used on positions, not durations.
/// </summary>
private int RemapTickPos(int tickPos, TimeAxis oldTimeAxis, TimeAxis newTimeAxis){
private int RemapTickPos(int tickPos, TimeAxis oldTimeAxis, TimeAxis newTimeAxis) {
double msPos = oldTimeAxis.TickPosToMsPos(tickPos);
return newTimeAxis.MsPosToTickPos(msPos);
}
Expand All @@ -409,41 +414,41 @@ private int RemapTickPos(int tickPos, TimeAxis oldTimeAxis, TimeAxis newTimeAxis
/// Remap the starting and ending positions of all the notes and parts in the whole project
/// from the old time axis to the new time axis, without changing their absolute positions in ms.
/// </summary>
public void RemapTimeAxis(TimeAxis oldTimeAxis, TimeAxis newTimeAxis){
public void RemapTimeAxis(TimeAxis oldTimeAxis, TimeAxis newTimeAxis) {
var project = DocManager.Inst.Project;
foreach(var part in project.parts){
foreach (var part in project.parts) {
var partOldStartTick = part.position;
var partNewStartTick = RemapTickPos(part.position, oldTimeAxis, newTimeAxis);
if(partNewStartTick != partOldStartTick){
if (partNewStartTick != partOldStartTick) {
DocManager.Inst.ExecuteCmd(new MovePartCommand(
project, part, partNewStartTick, part.trackNo));
}
if(part is UVoicePart voicePart){
if (part is UVoicePart voicePart) {
var partOldDuration = voicePart.Duration;
var partNewDuration = RemapTickPos(partOldStartTick + voicePart.duration, oldTimeAxis, newTimeAxis) - partNewStartTick;
if(partNewDuration != partOldDuration) {
if (partNewDuration != partOldDuration) {
DocManager.Inst.ExecuteCmd(new ResizeVoicePartCommand(
project, voicePart, partNewDuration - partOldDuration, false));
}
var noteCommands = new List<UCommand>();
foreach(var note in voicePart.notes){
foreach (var note in voicePart.notes) {
var noteOldStartTick = note.position + partOldStartTick;
var noteOldEndTick = note.End + partOldStartTick;
var noteOldDuration = note.duration;
var noteNewStartTick = RemapTickPos(noteOldStartTick, oldTimeAxis, newTimeAxis);
var noteNewEndTick = RemapTickPos(noteOldEndTick, oldTimeAxis, newTimeAxis);
var deltaPosTickInPart = (noteNewStartTick - partNewStartTick) - (noteOldStartTick - partOldStartTick);
if(deltaPosTickInPart != 0){
if (deltaPosTickInPart != 0) {
noteCommands.Add(new MoveNoteCommand(voicePart, note, deltaPosTickInPart, 0));
}
var noteNewDuration = noteNewEndTick - noteNewStartTick;
var deltaDur = noteNewDuration - noteOldDuration;
if(deltaDur != 0){
if (deltaDur != 0) {
noteCommands.Add(new ResizeNoteCommand(voicePart, note, deltaDur));
}
//TODO: expression curve remapping, phoneme timing remapping
}
foreach(var command in noteCommands){
foreach (var command in noteCommands) {
DocManager.Inst.ExecuteCmd(command);
}
}
Expand Down
17 changes: 15 additions & 2 deletions OpenUtau/Views/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@
<RowDefinition Height="4"/>
</Grid.RowDefinitions>
<Panel Grid.Row="1" Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="24">
<Border Classes="playback" Width="72" HorizontalAlignment="Left" Margin="4,0">
<Border Classes="playback" Width="92" HorizontalAlignment="Left" Margin="4,0">
<Grid>
<TextBlock Width="28" HorizontalAlignment="Left"
Background="Transparent" PointerPressed="OnEditTimeSignature">
Expand All @@ -231,14 +231,27 @@
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Width="42" HorizontalAlignment="Right"
<TextBlock Width="42" HorizontalAlignment="Center"
Background="Transparent" PointerPressed="OnEditBpm">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0:#0.00}">
<Binding Path="PlaybackViewModel.Bpm"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<ToggleButton Classes="toolbar" Margin="0" Padding="1" Height="18" Width="18" HorizontalAlignment="Right"
IsChecked="{Binding IsMetronomeEnabled}" ToolTip.Tip="{DynamicResource playback.toggle.metronom}">
<Path Classes="stroked"
Data="M1.5 15.5 L3.8 3.5 Q4 2.5 5 2.5 H8 Q9 2.5 9.2 3.5 L11.5 15.5 Z
M5.8 2.8 L5.8 12.8
M4.9 5 H6.7 V6.5 H4.9 Z
M3.6 5.5 H4.5
M3.3 7.2 H4.5
M3 8.9 H4.5
M2.7 10.6 H4.5
M2.4 12.3 H4.5
M1.2 15.5 H11.8"/>
</ToggleButton>
</Grid>
</Border>
<Border Classes="playback" Width="88" HorizontalAlignment="Center">
Expand Down