RhythmNotifier - Sync Your Game to the Beat of the Music (Sound & Audio)

by jumpingmechanic

28

RhythmNotifier

A Godot 4 addon for syncing game events and sound effects with the beat of the music.

Hey

This addon provides the RhythmNotifier class, which is a node that emits
rhythmic signals synchronized with the beat of an AudioStreamPlayer. The
signals are precisely synchronized with the audio, accounting for output
latency.

RhythmNotifier lets you define custom signals that emit when a given beat in
the audio stream is reached, or that emit every N beats. You can
also run the RhythmNotifier without playing audio, to generate rhythmic signals
without music.

Note: Beats are 0-indexed to make RhythmNotifier easier to use, while
musicians are accustomed to counting from beat one.

Usage

  1. Add a RhythmNotifier node to your scene. Then in its Inspector,
    • Drag an AudioStreamPlayer onto the Audio Stream Player property.
    • Set the BPM property to the beats per minute of the audio that will be played.
  2. Use the $RhythmNotifier.beats() method to create a custom signal to
    connect to. The signal can emit on a specific beat, or every N beats.

Tips

  • You can put the RhythmNotifier anywhere in your scene, including as a child of its AudioStreamPlayer.

  • You can set the .audio_stream_player property in code to integrate with your own sound management system, instead of an
    AudioStreamPlayer in the scene.

  • You can set the running property to emit signals without starting the
    AudioStreamPlayer. This may be useful for lead-in beats before music starts
    in rhythm games, or if you don't need any music (in which case
    .audio_stream_player may be left null.)

  • A convenience beat signal is available in the Node tab to connect visually, which emits every beat.

Installation

Install from the Godot Asset Library, or copy the addons/rhythm_notifier folder into an addons folder in your project.
The RhythmNotifier node will then be available when you Add Node in a scene.

Usage example

# Set r.bpm and r.audio_stream_player in inspector
@onready var r: RhythmNotifier = $RhythmNotifier  

# The beats method signature is:
#
#   func beats(beat_count: float, repeating := true, start_beat := 0.0) -> Signal
#
# We'll use this below to create repeating signals that emit every `beat_count`
# beats after `start_beat`, and non-repeating signals that emit when we reach
# `beat_count` beats after `start_beat`.

# Play music and emit lots of signals
func _play_some_music():
    # Print on beat 4, 8, 12...
    r.beats(4).connect(func(count): print("Hello from beat %d!" % (count * 4)))

    # Print on beat 5, 8, 11...
    r.beats(3, true, 2).connect(func(count): print("Hello from beat %d!" % 2+(count * 3)))

    # Print anytime beat 8.5 is reached
    r.beats(8.5, false).connect(func(_i): print("Hello from beat eight and a half!"))

    r.audio_stream_player.play()  # Start signaling
    r.audio_stream_player.seek(1.5)  # pausing/stopping/seeking all supported

    # Stop playback on beat 20
    r.beats(20, false).connect(func(_i): r.audio_stream_player.stop())

# Play the music after 4 pickup beats
func _play_with_leadin():
    r.beats(4, false).connect(func(_i):
        r.audio_stream_player.play()
    , CONNECT_ONE_SHOT)

    r.beat.connect(func(count):
        if not r.audio_stream_player.playing:
            print("Pickup beat %d" % count)
        else:
            print("Song beat %d" % count)

    r.running = true  # Start signaling without playing the audio stream

# Change the song tempo partway through
func _change_tempos():
    r.bpm = 60
    r.beats(4).connect(func(count):
        if r.bpm == 60 and count == 4:
            print("Four seconds into the song, we speed up.")
            r.bpm = 120
        elif r.bpm == 120:
            print("We are %.2f seconds into the song." % r.current_position)
    )
    r.audio_stream_player.play()

Usage example

Printing the rhythm musicians say when counting measures ("ONE and two and three and TWO and two and three and THREE and ...")

var r = RhythmNotifier.new()
get_tree().current_scene.add_child(r)
r.running = true

# Say the measure number at the start of each measure
r.beats(3).connect(func(count):
    print("TIME %.2f, BEAT %2d  :    %d!" %
        [r.current_position, r.current_beat, count])
)
# Say the other downbeats in the measure
r.beat.connect(func(count):
    if count % 3 != 0:
        print("TIME %.2f, BEAT %2d  :       (%d)" % 
            [r.current_position, count, (count % 3)+1])
)
# Say the upbeats in the measure
r.beats(.5).connect(func(i):
    if i % 2 != 0:
        print("TIME %.2f, BEAT %4.1f:       (and)" %
            [r.current_position, i/2.])
)

# Output:
#     TIME 0.52, BEAT  0.5:       (and)
#     TIME 1.00, BEAT  1  :       (2)
#     TIME 1.52, BEAT  1.5:       (and)
#     TIME 2.02, BEAT  2  :       (3)
#     TIME 2.52, BEAT  2.5:       (and)
#     TIME 3.02, BEAT  3  :    1!
#     TIME 3.52, BEAT  3.5:       (and)
#     TIME 4.02, BEAT  4  :       (2)
#     TIME 4.52, BEAT  4.5:       (and)
#     TIME 5.02, BEAT  5  :       (3)
#     TIME 5.52, BEAT  5.5:       (and)
#     TIME 6.02, BEAT  6  :    2!
#     TIME 6.52, BEAT  6.5:       (and)
#     TIME 7.02, BEAT  7  :       (2)
#     TIME 7.52, BEAT  7.5:       (and)
#     TIME 8.02, BEAT  8  :       (3)
#     TIME 8.50, BEAT  8.5:       (and)
#     TIME 9.00, BEAT  9  :    3!
#     TIME 9.50, BEAT  9.5:       (and)
# ...etc

Version

1.0

Engine

4.0

Category

Tools

Download

Version1.0
Download Now

Support

If you need help or have questions about this plugin, please contact the author.

Contact Author