Add a metronome - #2341
Open
rokujyushi wants to merge 10 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
prefs.json(MetronomeEnabled) and restored on startup.MetronomeVolume(0–100, default 60)MetronomeHighFrequency(20–5000 Hz, default 2200) — bar accentMetronomeLowFrequency(20–3000 Hz, default 1320) — beatImplementation
MetronomeEngine(OpenUtau.Core/SignalChain/) is anISignalSourcethat owns a privateToneGenerator. It is mixed into the playback chain by the newPlaybackMix, which sums the rendered master mix with the metronome overlay.MetronomeSchedulerwalks bar/beat positions throughTimeAxis, so tempo maps and time signature changes are handled without any assumption of a constant BPM.BpmCommand,TimeSignatureCommandand the tempo/time-signature add/remove commands rebuild the schedule mid-playback.TimeAxisare 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.MetronomeEnabledis the single source of truth. It readsMetronomeEngine.Enabled, writes the preference on change, and is initialized fromPreferencesin thePlaybackManagerconstructor, so the metronome no longer depends on view model construction order to be in the right state.PlaybackViewModel.MetronomeEnabledis a pass-through property, matching the existingLoopPlaybackpattern.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()recordsplaybackEndTick(the explicit end tick, orUProject.EndTick).PlaybackMixexposesMasterExhausted, 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 reachesplaybackEndTickand the master source is exhausted.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
StartingToPlaywas cleared on the render thread beforeAudioOutput.Play()was called. In that window the 15 ms UI timer could observeStopped && PlayingMaster && !StartingToPlay, treat it as the end of playback, and callStopPlayback(). Playback then started anyway but withPlayingMaster == false, so audio played while the position marker never moved.StartingToPlayis now cleared after playback has actually started, andUpdatePlayPos()null-checksmasterMix.Files changed
OpenUtau.Core/SignalChain/MetronomeEngine.csOpenUtau.Core/SignalChain/MetronomeScheduler.csTimeAxisOpenUtau.Core/PlaybackManager.csPlaybackMix,MetronomeEnabled, click preview, end-of-playback detection, start race fixOpenUtau.Core/Util/Preferences.csOpenUtau/ViewModels/PlaybackViewModel.csOpenUtau/ViewModels/PreferencesViewModel.csOpenUtau/Views/MainWindow.axamlOpenUtau/Views/PreferencesDialog.axaml(.cs)OpenUtau/Strings/Strings.axamlTesting
Verified manually on Windows: