Skip to content

Add a metronome - #2341

Open
rokujyushi wants to merge 10 commits into
openutau:masterfrom
rokujyushi:Metronome
Open

Add a metronome#2341
rokujyushi wants to merge 10 commits into
openutau:masterfrom
rokujyushi:Metronome

Conversation

@rokujyushi

@rokujyushi rokujyushi commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a metronome to playback. While the transport is running, a click is generated on every beat, with an accented click on the first beat of each bar. The click follows tempo and time signature changes, including changes made while playing.

This is a modified version of the pull request at #2067

Features

  • Toolbar toggle next to the loop button turns the metronome on and off. The state is persisted in prefs.json (MetronomeEnabled) and restored on startup.
  • Preferences → Playback gains a Metronome section:
    • MetronomeVolume (0–100, default 60)
    • MetronomeHighFrequency (20–5000 Hz, default 2200) — bar accent
    • MetronomeLowFrequency (20–3000 Hz, default 1320) — beat
    • A Test button previews the two clicks without starting playback.
    • Right-clicking any of the three sliders resets it to its default.
  • Toggling during playback takes effect immediately and stays aligned to the grid — the schedule is rebuilt from the current play position rather than restarted from zero.

Implementation

  • MetronomeEngine (OpenUtau.Core/SignalChain/) is an ISignalSource that owns a private ToneGenerator. It is mixed into the playback chain by the new PlaybackMix, which sums the rendered master mix with the metronome overlay.
  • MetronomeScheduler walks bar/beat positions through TimeAxis, so tempo maps and time signature changes are handled without any assumption of a constant BPM. BpmCommand, TimeSignatureCommand and the tempo/time-signature add/remove commands rebuild the schedule mid-playback.
  • Clicks are placed at a sample offset inside the buffer rather than at buffer boundaries, so timing does not quantize to the audio buffer size. Each click is a short two-tone burst (base + accent partial) with a 5 ms attack and a 10 ms release.
  • The engine is thread-safe: the scheduler and the playback TimeAxis are guarded by a lock, since the schedule is written from the UI thread (toggle, tempo edits, start/stop) and read from the audio thread. Gain is only pushed to the tone generator when the preference actually changes, keeping the audio callback allocation- and lock-free in the common case.
  • PlaybackManager.MetronomeEnabled is the single source of truth. It reads MetronomeEngine.Enabled, writes the preference on change, and is initialized from Preferences in the PlaybackManager constructor, so the metronome no longer depends on view model construction order to be in the right state. PlaybackViewModel.MetronomeEnabled is a pass-through property, matching the existing LoopPlayback pattern.

Playback end detection

The overlay required a change to how the end of playback is detected.

Previously, playback ended implicitly: once the rendered audio ran out, WaveMix.Mix() stopped advancing, MasterAdapter.Read() returned 0, and NAudio reported end-of-stream. The metronome can produce sound at any position, so with the overlay in the mix that signal never arrives and playback would run past the end of the project forever.

Playback now stops on the play position instead:

  • Play() records playbackEndTick (the explicit end tick, or UProject.EndTick).
  • PlaybackMix exposes MasterExhausted, set when the master source stops advancing. This is what preserves release tails — playback continues while the rendered audio is still producing sound past the last part.
  • UpdatePlayPos() ends playback when the play position reaches playbackEndTick and the master source is exhausted.
  • When the project is empty (EndTick == 0) no bound applies, so the metronome can be used on its own.

As a side effect, whole-project loop playback (loopProjectOnPlaybackEnd) now works; it depended on the end-of-stream signal that was never reached.

Fixes

  • Race condition at playback start. StartingToPlay was cleared on the render thread before AudioOutput.Play() was called. In that window the 15 ms UI timer could observe Stopped && PlayingMaster && !StartingToPlay, treat it as the end of playback, and call StopPlayback(). Playback then started anyway but with PlayingMaster == false, so audio played while the position marker never moved. StartingToPlay is now cleared after playback has actually started, and UpdatePlayPos() null-checks masterMix.

Files changed

File Change
OpenUtau.Core/SignalChain/MetronomeEngine.cs New — click generation and scheduling
OpenUtau.Core/SignalChain/MetronomeScheduler.cs New — bar/beat walking over TimeAxis
OpenUtau.Core/PlaybackManager.cs PlaybackMix, MetronomeEnabled, click preview, end-of-playback detection, start race fix
OpenUtau.Core/Util/Preferences.cs 4 new preferences
OpenUtau/ViewModels/PlaybackViewModel.cs Toolbar toggle binding
OpenUtau/ViewModels/PreferencesViewModel.cs Volume / frequency settings, test, reset
OpenUtau/Views/MainWindow.axaml Metronome toggle button
OpenUtau/Views/PreferencesDialog.axaml(.cs) Metronome settings UI
OpenUtau/Strings/Strings.axaml 4 new strings (English only — other locales not translated yet)

Testing

Verified manually on Windows:

  • Playback stops at the end of the project; loops back to the start when loop playback is enabled.
  • Range playback and range loop stop at the range end.
  • One-shot previews with an explicit end tick (e.g. Alt+Space) stop at the expected position.
  • Release tails past the last part are not cut off.
  • The metronome alone plays on an empty project.
  • The position marker no longer freezes while audio keeps playing right after starting playback.

rokujyushi and others added 10 commits April 21, 2026 05:38
PlaybackManagerにメトロノーム生成・ミックス機能を実装し、PreferencesでON/OFFを管理。再生バーと設定画面にトグルUIを追加し、ユーザーが簡単にメトロノームの有効/無効を切り替え可能に。拍・小節ごとに自動でメトロノーム音を再生するロジックも導入。
Newly introduced the MetronomeEngine and MetronomeScheduler classes.
We have separated metronome scheduling from playback management and improved the system to accurately handle changes to tempo and time signature during playback, as well as jumps to specific playback positions.
We have added sliders for metronome volume, high-frequency, and low-frequency settings to the settings screen, allowing you to save and reset each value instantly. We have also implemented a test button that lets you preview the metronome sound using the current settings. Additionally, we have improved the metronome sound generation logic to align with the Preferences settings.
The metronome click scheduling has been significantly refactored to ensure precise timing at the buffer level. A startSampleOffset has been introduced to the SineGenerator, enabling sample-accurate control of the click sound start time. SetGain was added to the ToneGenerator, allowing for real-time reflection of the metronome volume. Overloads for StartTone and StartTones now support sound generation starting from any given offset.

In the MetronomeEngine, a new ScheduleBuffer method has been established to schedule click sounds within the buffer range during mixing, replacing the deprecated TryPlay. TimeAxis management has also been strengthened to improve playback position tracking. Furthermore, volume retrieval has been centralized within the PlaybackManager to unify system settings.
…opilot review feedback

Redundant code in the StartTone series has been organized, and the logic for determining existing tones has been centralized in one place. Thread safety has been improved by removing StartTones overloads and expanding the lock scope of EndTone. Overall, this enhances the consistency and maintainability of tone management.
…tion

Refactored PlaybackMix to add a master completion determination, and unified the management of the metronome enabled state via a property in PlaybackManager. Also made MetronomeEngine thread-safe, optimized gain settings, and simplified bindings in PlaybackViewModel. Improved the accuracy of playback completion determination and the robustness of metronome state management.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants