From 234dc0ac19f512a21d4e356411a3969932d1346e Mon Sep 17 00:00:00 2001 From: Eric Myllyoja Date: Tue, 13 Dec 2022 17:38:55 -0500 Subject: [PATCH] Vocal group work --- Project.xml | 8 + hmm.json | 11 +- source/funkin/audio/FlxAudioGroup.hx | 206 ++++++++++++++++++ source/funkin/audio/VocalGroup.hx | 119 ++++++++++ source/funkin/play/HealthIcon.hx | 10 +- .../ui/debug/charting/ChartEditorState.hx | 73 ++++++- 6 files changed, 406 insertions(+), 21 deletions(-) create mode 100644 source/funkin/audio/FlxAudioGroup.hx create mode 100644 source/funkin/audio/VocalGroup.hx diff --git a/Project.xml b/Project.xml index d7ee924f5..73b384ee0 100644 --- a/Project.xml +++ b/Project.xml @@ -135,6 +135,14 @@ + + + + diff --git a/hmm.json b/hmm.json index c8aa048ce..f14ebdfe8 100644 --- a/hmm.json +++ b/hmm.json @@ -21,13 +21,6 @@ "ref": "157eaf3", "url": "https://github.com/MasterEric/flixel-addons" }, - { - "name": "flixel-addons", - "type": "git", - "dir": null, - "ref": "dev", - "url": "https://github.com/MasterEric/flixel-addons" - }, { "name": "flixel-ui", "type": "haxelib", @@ -100,8 +93,8 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "4f999ac", - "url": "https://github.com/openfl/openfl" + "ref": "3fd5763", + "url": "https://github.com/MasterEric/openfl/" }, { "name": "polymod", diff --git a/source/funkin/audio/FlxAudioGroup.hx b/source/funkin/audio/FlxAudioGroup.hx new file mode 100644 index 000000000..182af3dea --- /dev/null +++ b/source/funkin/audio/FlxAudioGroup.hx @@ -0,0 +1,206 @@ +package funkin.audio; + +import flixel.group.FlxGroup.FlxTypedGroup; +import flixel.system.FlxSound; + +/** + * A group of FlxSounds which can be controlled as a whole. + * + * Add sounds to the group using `add()`, and then control them + * as a whole using the properties and methods of this class. + * + * It is assumed that all the sounds will play at the same time, + * and have the same duration. + */ +class FlxAudioGroup extends FlxTypedGroup +{ + /** + * The position in time of the sounds in the group. + * Measured in milliseconds. + */ + public var time(get, set):Float; + + function get_time():Float + { + if (getFirstAlive() != null) + return getFirstAlive().time; + else + return 0; + } + + function set_time(time:Float):Float + { + forEachAlive(function(sound:FlxSound) + { + // account for different offsets per sound? + sound.time = time; + }); + + return time; + } + + /** + * The volume of the sounds in the group. + */ + public var volume(get, set):Float; + + function get_volume():Float + { + if (getFirstAlive() != null) + return getFirstAlive().volume; + else + return 1.0; + } + + function set_volume(volume:Float):Float + { + forEachAlive(function(sound:FlxSound) + { + sound.volume = volume; + }); + + return volume; + } + + /** + * The pitch of the sounds in the group, as a multiplier of 1.0x. + * `2.0` would play the audio twice as fast with a higher pitch, + * and `0.5` would play the audio at half speed with a lower pitch. + */ + public var pitch(get, set):Float; + + function get_pitch():Float + { + #if FLX_PITCH + if (getFirstAlive() != null) + return getFirstAlive().pitch; + else + #end + return 1; + } + + function set_pitch(val:Float):Float + { + #if FLX_PITCH + trace('Setting audio pitch to ' + val); + forEachAlive(function(sound:FlxSound) + { + sound.pitch = val; + }); + #end + return val; + } + + /** + * Whether members of the group should be destroyed when they finish playing. + */ + public var autoDestroyMembers(default, set):Bool = false; + + function set_autoDestroyMembers(value:Bool):Bool + { + autoDestroyMembers = value; + forEachAlive(function(sound:FlxSound) + { + sound.autoDestroy = value; + }); + return value; + } + + /** + * Add a sound to the group. + */ + public override function add(sound:FlxSound):FlxSound + { + var result:FlxSound = super.add(sound); + + if (result == null) + return; + + // Apply parameters to the new sound. + result.autoDestroy = this.autoDestroyMembers; + result.pitch = this.pitch; + result.volume = this.volume; + + // We have to play, then pause the sound to set the time, + // else the sound will restart immediately when played. + result.play(true, 0.0); + result.pause(); + result.time = this.time; + } + + /** + * Pause all the sounds in the group. + */ + public function pause() + { + forEachAlive(function(sound:FlxSound) + { + sound.pause(); + }); + } + + /** + * Play all the sounds in the group. + */ + public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float) + { + forEachAlive(function(sound:FlxSound) + { + sound.play(forceRestart, startTime, endTime); + }); + } + + /** + * Resume all the sounds in the group. + */ + public function resume() + { + forEachAlive(function(sound:FlxSound) + { + sound.resume(); + }); + } + + /** + * Stop all the sounds in the group. + */ + public function stop() + { + forEachAlive(function(sound:FlxSound) + { + sound.stop(); + }); + } + + public override function clear():Void { + this.stop(); + + super.clear(); + } + + /** + * Calculates the deviation of the sounds in the group from the target time. + * + * @param targetTime The time to compare the sounds to. + * If null, the current time of the first sound in the group is used. + * @return The largest deviation of the sounds in the group from the target time. + */ + public function calcDeviation(?targetTime:Float):Float + { + var deviation:Float = 0; + + forEachAlive(function(sound:FlxSound) + { + if (targetTime == null) + targetTime = sound.time; + else + { + var diff:Float = sound.time - targetTime; + if (Math.abs(diff) > Math.abs(deviation)) + deviation = diff; + } + }); + + return deviation; + } +} diff --git a/source/funkin/audio/VocalGroup.hx b/source/funkin/audio/VocalGroup.hx new file mode 100644 index 000000000..2dce0d4c0 --- /dev/null +++ b/source/funkin/audio/VocalGroup.hx @@ -0,0 +1,119 @@ +package funkin.audio; + +import flixel.system.FlxSound; + +/** + * An audio group that allows for specific control of vocal tracks. + */ +class VocalGroup extends FlxAudioGroup +{ + /** + * The player's vocal track. + */ + var playerVocals:FlxSound; + + /** + * The opponent's vocal track. + */ + var opponentVocals:FlxSound; + + /** + * The volume of the player's vocal track. + * Nore that this value is multiplied by the overall volume of the group. + */ + public var playerVolume(default, set):Float; + + function set_playerVolume(value:Float):Float + { + playerVolume = value; + if (playerVocals != null) + { + // Make sure volume is capped at 1.0. + playerVocals.volume = Math.min(playerVolume * this.volume, 1.0); + } + return playerVolume; + } + + /** + * The volume of the opponent's vocal track. + * Nore that this value is multiplied by the overall volume of the group. + */ + public var opponentVolume(default, set):Float; + + function set_opponentVolume(value:Float):Float + { + opponentVolume = value; + if (opponentVocals != null) + { + // Make sure volume is capped at 1.0. + opponentVocals.volume = opponentVolume * this.volume; + } + return opponentVolume; + } + + /** + * Sets up the player's vocal track. + * Stops and removes the existing player track if one exists. + */ + public function setPlayerVocals(sound:FlxSound):FlxSound + { + if (playerVocals != null) + { + playerVocals.stop(); + remove(playerVocals); + playerVocals = null; + } + + playerVocals = add(sound); + playerVocals.volume = this.playerVolume * this.volume; + + return playerVocals; + } + + /** + * Sets up the opponent's vocal track. + * Stops and removes the existing player track if one exists. + */ + public function setOpponentVocals(sound:FlxSound):FlxSound + { + if (opponentVocals != null) + { + opponentVocals.stop(); + remove(opponentVocals); + opponentVocals = null; + } + + opponentVocals = add(sound); + opponentVocals.volume = this.opponentVolume * this.volume; + + return opponentVocals; + } + + /** + * In this extension of FlxAudioGroup, there is a separate overall volume + * which affects all the members of the group. + */ + var _volume = 1.0; + + override function get_volume():Float + { + return _volume; + } + + override function set_volume(value:Float):Float + { + _volume = super.set_volume(value); + + if (playerVocals != null) + { + playerVocals.volume = playerVolume * _volume; + } + + if (opponentVocals != null) + { + opponentVocals.volume = opponentVolume * _volume; + } + + return _volume; + } +} diff --git a/source/funkin/play/HealthIcon.hx b/source/funkin/play/HealthIcon.hx index 4c4d8caaa..9f2f61b38 100644 --- a/source/funkin/play/HealthIcon.hx +++ b/source/funkin/play/HealthIcon.hx @@ -196,17 +196,11 @@ class HealthIcon extends FlxSprite // Make the health icons bump (the update function causes them to lerp back down). if (this.width > this.height) { - var targetSize = Std.int(CoolUtil.coolLerp(this.width + HEALTH_ICON_SIZE * 0.2, HEALTH_ICON_SIZE, 0.15)); - targetSize = Std.int(Math.min(targetSize, HEALTH_ICON_SIZE * 1.2)); - - setGraphicSize(targetSize, 0); + setGraphicSize(this.width + (HEALTH_ICON_SIZE * this.size.x * 0.2), 0); } else { - var targetSize = Std.int(CoolUtil.coolLerp(this.height + HEALTH_ICON_SIZE * 0.2, HEALTH_ICON_SIZE, 0.15)); - targetSize = Std.int(Math.min(targetSize, HEALTH_ICON_SIZE * 1.2)); - - setGraphicSize(0, targetSize); + setGraphicSize(0, this.height + (HEALTH_ICON_SIZE * this.size.y * 0.2)); } this.updateHitbox(); } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index d349d9cdb..e26ed09d7 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -278,6 +278,26 @@ class ChartEditorState extends HaxeUIState return isViewDownscroll; } + /** + * Whether hitsounds are enabled for at least one character. + */ + var hitsoundsEnabled(get, null):Bool; + + function get_hitsoundsEnabled():Bool + { + return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent; + } + + /** + * Whether hitsounds are enabled for the player. + */ + var hitsoundsEnabledPlayer:Bool = true; + + /** + * Whether hitsounds are enabled for the opponent. + */ + var hitsoundsEnabledOpponent:Bool = true; + /** * Whether the user's mouse cursor is hovering over a SOLID component of the HaxeUI. * If so, ignore mouse events underneath. @@ -1063,13 +1083,25 @@ class ChartEditorState extends HaxeUIState }); setUISelected('menubarItemMetronomeEnabled', shouldPlayMetronome); + addUIChangeListener('menubarItemPlayerHitsounds', (event:UIEvent) -> + { + hitsoundsEnabledPlayer = event.value; + }); + setUISelected('menubarItemPlayerHitsounds', hitsoundsEnabledPlayer); + + addUIChangeListener('menubarItemOpponentHitsounds', (event:UIEvent) -> + { + hitsoundsEnabledOpponent = event.value; + }); + setUISelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent); + var instVolumeLabel:Label = findComponent('menubarLabelVolumeInstrumental', Label); addUIChangeListener('menubarItemVolumeInstrumental', (event:UIEvent) -> { var volume:Float = event.value / 100.0; if (audioInstTrack != null) audioInstTrack.volume = volume; - instVolumeLabel.text = 'Instrumental - ${event.value}%'; + instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%'; }); var vocalsVolumeLabel:Label = findComponent('menubarLabelVolumeVocals', Label); @@ -1078,7 +1110,7 @@ class ChartEditorState extends HaxeUIState var volume:Float = event.value / 100.0; if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume; - vocalsVolumeLabel.text = 'Vocals - ${event.value}%'; + vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%'; }); var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label); @@ -1091,7 +1123,7 @@ class ChartEditorState extends HaxeUIState if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch; #end - playbackSpeedLabel.text = 'Playback Speed - ${pitch}x'; + playbackSpeedLabel.text = 'Playback Speed - ${Std.int(event.value * 100) / 100}x'; }); addUIChangeListener('menubarItemToggleToolboxTools', (event:UIEvent) -> @@ -2265,7 +2297,9 @@ class ChartEditorState extends HaxeUIState // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! var oldStepTime = Conductor.currentStepTime; + var oldSongPosition = Conductor.songPosition; Conductor.update(audioInstTrack.time); + handleHitsounds(oldSongPosition, Conductor.songPosition); // Resync vocals. if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time; @@ -2279,8 +2313,9 @@ class ChartEditorState extends HaxeUIState else { // Else, move the entire view. - + var oldSongPosition = Conductor.songPosition; Conductor.update(audioInstTrack.time); + handleHitsounds(oldSongPosition, Conductor.songPosition); // Resync vocals. if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100) audioVocalTrackGroup.time = audioInstTrack.time; @@ -2302,6 +2337,36 @@ class ChartEditorState extends HaxeUIState } } + /** + * Handle the playback of hitsounds. + */ + function handleHitsounds(oldSongPosition:Float, newSongPosition:Float):Void { + if (!hitsoundsEnabled) + return; + + // Assume notes are sorted by time. + for (noteData in currentSongChartNoteData) { + if (noteData.time < oldSongPosition) + // Note is in the past. + continue; + + if (noteData.time >= newSongPosition) + // Note is in the future. + return; + + // Note was just hit. + switch (noteData.getStrumlineIndex()) { + case 0: // Player + if (hitsoundsEnabledPlayer) + playSound(Paths.sound('funnyNoise/funnyNoise-09')); + case 1: // Opponent + if (hitsoundsEnabledOpponent) + playSound(Paths.sound('funnyNoise/funnyNoise-010')); + } + } + } + + function startAudioPlayback() { if (audioInstTrack != null)