diff --git a/README.md b/README.md index 9cdf6eb..0944706 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,8 @@ Plays through a playlist one song at a time, blending three intensity layers per | `Playlist` | `DynamicSoundPlaylist` | `null` | Songs to play in order. Mutated as songs finish. | | `Intensity` | `float` (0–1) | `0` | Runtime mix between the three layers. `0` = layer 1 dominates, `1` = layer 3 dominates. | | `Loop` | `bool` | `false` | When true, finished songs are re-queued at the back so the playlist cycles forever. | -| `PitchMode` | enum | `Constant` | `Constant` or `Random` (placeholder — pitch logic not yet implemented). | -| `RandomMinPitchScale` | `float` | `-0.5` | Minimum pitch when `PitchMode = Random`. Hidden in the inspector when `PitchMode = Constant`. | -| `RandomMaxPitchScale` | `float` | `0.5` | Maximum pitch when `PitchMode = Random`. Hidden in the inspector when `PitchMode = Constant`. | -The `stream` property is auto-assigned an `AudioStreamPolyphonic` when the node enters the tree in the editor, so you don't need to configure it manually. +When the node enters the tree in the editor, `stream` is auto-assigned an `AudioStreamPolyphonic` and `bus` is set to `"Music"` if a bus by that name exists in the project (otherwise it's left at `"Master"`). User-customised buses are not overridden. #### Methods @@ -83,12 +80,19 @@ Each `play_fx()` call fires a fresh trigger of the assigned `DynamicSound`. Trig | --- | --- | --- | --- | | `Sound` | `DynamicSound` | `null` | The sound to trigger. Layers with intensity blending if all three are set. | | `Intensity` | `float` (0–1) | `0` | Volume mix between layers. Only blended when all three layers are present; otherwise the first non-null layer plays at full volume. | +| `PitchMode` | enum | `Constant` | `Constant` plays each trigger at pitch `1.0`. `Random` picks a fresh pitch per trigger via `1.0 + randf_range(RandomMinPitchScale, RandomMaxPitchScale)`. | +| `RandomMinPitchScale` | `float` | `-0.5` | Minimum pitch offset (added to `1.0`) when `PitchMode = Random`. Hidden in the inspector when `PitchMode = Constant`. | +| `RandomMaxPitchScale` | `float` | `0.5` | Maximum pitch offset (added to `1.0`) when `PitchMode = Random`. Hidden in the inspector when `PitchMode = Constant`. | + +When the node enters the tree in the editor, `stream` is auto-assigned an `AudioStreamPolyphonic` and `bus` is set to `"FX"` if a bus by that name exists in the project (otherwise it's left at `"Master"`). User-customised buses are not overridden. + +For a *constant non-1.0* pitch, set the player's built-in `pitch_scale` property — that multiplies with whatever `PitchMode` produces, so e.g. `pitch_scale = 0.8` + `PitchMode = Random` with default range gives an effective pitch in `[0.4, 1.2]`. #### Methods | Method | Description | | --- | --- | -| `play_fx()` | Triggers a fresh play of `Sound`. Volumes are computed at trigger time and locked in — later changes to `Intensity` or `volume_db` don't affect already-playing triggers. | +| `play_fx()` | Triggers a fresh play of `Sound`. Pitch is sampled at trigger time per `PitchMode`. Volumes are computed at trigger time and locked in — later changes to `Intensity` or `volume_db` don't affect already-playing triggers. | #### Example: collision SFX diff --git a/src/DynamicSoundFXPlayer.gd b/src/DynamicSoundFXPlayer.gd index c9151dc..83b110e 100644 --- a/src/DynamicSoundFXPlayer.gd +++ b/src/DynamicSoundFXPlayer.gd @@ -15,6 +15,17 @@ extends AudioStreamPlayer ## Volume mix between intensity layers. Only blended when all three layers are set. @export_range(0, 1) var Intensity : float = 0; +## How playback pitch is selected for each [method play_fx] trigger. +@export var PitchMode : DynamicSoundConstants.PitchMode = DynamicSoundConstants.PitchMode.Constant: + set(value): + PitchMode = value; + notify_property_list_changed(); + +## Minimum pitch offset (added to 1.0) when [member PitchMode] is Random. +@export var RandomMinPitchScale : float = -0.5; +## Maximum pitch offset (added to 1.0) when [member PitchMode] is Random. +@export var RandomMaxPitchScale : float = 0.5; + var _core : DynamicSoundFXPlayerCore; func _enter_tree() -> void: @@ -25,6 +36,11 @@ func _enter_tree() -> void: if bus == &"Master" and AudioServer.get_bus_index("FX") != -1: bus = "FX"; +func _validate_property(property: Dictionary) -> void: + if property.name in ["RandomMinPitchScale", "RandomMaxPitchScale"]: + if PitchMode != DynamicSoundConstants.PitchMode.Random: + property.usage &= ~PROPERTY_USAGE_EDITOR; + func _ready() -> void: _core = DynamicSoundFXPlayerCore.new(self); _core.ready(); diff --git a/src/DynamicSoundFXPlayer2d.gd b/src/DynamicSoundFXPlayer2d.gd index 71c4447..1e493cb 100644 --- a/src/DynamicSoundFXPlayer2d.gd +++ b/src/DynamicSoundFXPlayer2d.gd @@ -14,6 +14,17 @@ extends AudioStreamPlayer2D ## Volume mix between intensity layers. Only blended when all three layers are set. @export_range(0, 1) var Intensity : float = 0; +## How playback pitch is selected for each [method play_fx] trigger. +@export var PitchMode : DynamicSoundConstants.PitchMode = DynamicSoundConstants.PitchMode.Constant: + set(value): + PitchMode = value; + notify_property_list_changed(); + +## Minimum pitch offset (added to 1.0) when [member PitchMode] is Random. +@export var RandomMinPitchScale : float = -0.5; +## Maximum pitch offset (added to 1.0) when [member PitchMode] is Random. +@export var RandomMaxPitchScale : float = 0.5; + var _core : DynamicSoundFXPlayerCore; func _enter_tree() -> void: @@ -24,6 +35,11 @@ func _enter_tree() -> void: if bus == &"Master" and AudioServer.get_bus_index("FX") != -1: bus = "FX"; +func _validate_property(property: Dictionary) -> void: + if property.name in ["RandomMinPitchScale", "RandomMaxPitchScale"]: + if PitchMode != DynamicSoundConstants.PitchMode.Random: + property.usage &= ~PROPERTY_USAGE_EDITOR; + func _ready() -> void: _core = DynamicSoundFXPlayerCore.new(self); _core.ready(); diff --git a/src/DynamicSoundFXPlayer3d.gd b/src/DynamicSoundFXPlayer3d.gd index fdd6613..68eea25 100644 --- a/src/DynamicSoundFXPlayer3d.gd +++ b/src/DynamicSoundFXPlayer3d.gd @@ -14,6 +14,17 @@ extends AudioStreamPlayer3D ## Volume mix between intensity layers. Only blended when all three layers are set. @export_range(0, 1) var Intensity : float = 0; +## How playback pitch is selected for each [method play_fx] trigger. +@export var PitchMode : DynamicSoundConstants.PitchMode = DynamicSoundConstants.PitchMode.Constant: + set(value): + PitchMode = value; + notify_property_list_changed(); + +## Minimum pitch offset (added to 1.0) when [member PitchMode] is Random. +@export var RandomMinPitchScale : float = -0.5; +## Maximum pitch offset (added to 1.0) when [member PitchMode] is Random. +@export var RandomMaxPitchScale : float = 0.5; + var _core : DynamicSoundFXPlayerCore; func _enter_tree() -> void: @@ -24,6 +35,11 @@ func _enter_tree() -> void: if bus == &"Master" and AudioServer.get_bus_index("FX") != -1: bus = "FX"; +func _validate_property(property: Dictionary) -> void: + if property.name in ["RandomMinPitchScale", "RandomMaxPitchScale"]: + if PitchMode != DynamicSoundConstants.PitchMode.Random: + property.usage &= ~PROPERTY_USAGE_EDITOR; + func _ready() -> void: _core = DynamicSoundFXPlayerCore.new(self); _core.ready(); diff --git a/src/DynamicSoundFXPlayerCore.gd b/src/DynamicSoundFXPlayerCore.gd index e80f770..ec41aae 100644 --- a/src/DynamicSoundFXPlayerCore.gd +++ b/src/DynamicSoundFXPlayerCore.gd @@ -3,8 +3,9 @@ class_name DynamicSoundFXPlayerCore ## ## Owned by [DynamicSoundFXPlayer], [DynamicSoundFXPlayer2D], and [DynamicSoundFXPlayer3D]. ## Operates on its owner via duck-typed property access — the owner must expose -## [code]Sound[/code], [code]Intensity[/code], [code]volume_db[/code], [code]play()[/code], -## and [code]get_stream_playback()[/code]. +## [code]Sound[/code], [code]Intensity[/code], [code]PitchMode[/code], +## [code]RandomMinPitchScale[/code], [code]RandomMaxPitchScale[/code], +## [code]volume_db[/code], [code]play()[/code], and [code]get_stream_playback()[/code]. extends RefCounted var _player; @@ -23,14 +24,20 @@ func play_fx() -> void: var sound : DynamicSound = _player.Sound; if sound == null or _playback == null: return; + var pitch : float = _resolve_pitch(); var layers : Array[AudioStream] = []; if sound.Intensity1 != null: layers.append(sound.Intensity1); if sound.Intensity2 != null: layers.append(sound.Intensity2); if sound.Intensity3 != null: layers.append(sound.Intensity3); var real_intensity : float = _player.Intensity if layers.size() == 3 else 0.0; for i in layers.size(): - var id : int = _playback.play_stream(layers[i]); + var id : int = _playback.play_stream(layers[i], 0.0, 0.0, pitch); if id == AudioStreamPlaybackPolyphonic.INVALID_ID: continue; var vol_lin : float = max((.5 - abs(float(i) / 2 - real_intensity)) / .5, 0.0); _playback.set_stream_volume(id, linear_to_db(db_to_linear(_player.volume_db) * vol_lin)); + +func _resolve_pitch() -> float: + if _player.PitchMode == DynamicSoundConstants.PitchMode.Random: + return 1.0 + randf_range(_player.RandomMinPitchScale, _player.RandomMaxPitchScale); + return 1.0; diff --git a/src/DynamicSoundPlayer.gd b/src/DynamicSoundPlayer.gd index 54b7c10..a09d34a 100644 --- a/src/DynamicSoundPlayer.gd +++ b/src/DynamicSoundPlayer.gd @@ -22,17 +22,6 @@ extends AudioStreamPlayer ## When true, finished songs are re-appended to the back of the queue so the playlist cycles forever. @export var Loop : bool; -## How playback pitch is selected. -@export var PitchMode : DynamicSoundConstants.PitchMode = DynamicSoundConstants.PitchMode.Constant: - set(value): - PitchMode = value; - notify_property_list_changed(); - -## Minimum pitch scale when [member PitchMode] is Random. -@export var RandomMinPitchScale : float = -0.5; -## Maximum pitch scale when [member PitchMode] is Random. -@export var RandomMaxPitchScale : float = 0.5; - var _core : DynamicSoundPlayerCore; func _set(property: StringName, value: Variant) -> bool: @@ -49,11 +38,6 @@ func _enter_tree() -> void: if bus == &"Master" and AudioServer.get_bus_index("Music") != -1: bus = "Music"; -func _validate_property(property: Dictionary) -> void: - if property.name in ["RandomMinPitchScale", "RandomMaxPitchScale"]: - if PitchMode != DynamicSoundConstants.PitchMode.Random: - property.usage &= ~PROPERTY_USAGE_EDITOR; - func _ready() -> void: _core = DynamicSoundPlayerCore.new(self); _core.ready(); diff --git a/src/DynamicSoundPlayer2d.gd b/src/DynamicSoundPlayer2d.gd index e16a155..d9d57cf 100644 --- a/src/DynamicSoundPlayer2d.gd +++ b/src/DynamicSoundPlayer2d.gd @@ -21,17 +21,6 @@ extends AudioStreamPlayer2D ## When true, finished songs are re-appended to the back of the queue so the playlist cycles forever. @export var Loop : bool; -## How playback pitch is selected. -@export var PitchMode : DynamicSoundConstants.PitchMode = DynamicSoundConstants.PitchMode.Constant: - set(value): - PitchMode = value; - notify_property_list_changed(); - -## Minimum pitch scale when [member PitchMode] is Random. -@export var RandomMinPitchScale : float = -0.5; -## Maximum pitch scale when [member PitchMode] is Random. -@export var RandomMaxPitchScale : float = 0.5; - var _core : DynamicSoundPlayerCore; func _set(property: StringName, value: Variant) -> bool: @@ -48,11 +37,6 @@ func _enter_tree() -> void: if bus == &"Master" and AudioServer.get_bus_index("Music") != -1: bus = "Music"; -func _validate_property(property: Dictionary) -> void: - if property.name in ["RandomMinPitchScale", "RandomMaxPitchScale"]: - if PitchMode != DynamicSoundConstants.PitchMode.Random: - property.usage &= ~PROPERTY_USAGE_EDITOR; - func _ready() -> void: _core = DynamicSoundPlayerCore.new(self); _core.ready(); diff --git a/src/DynamicSoundPlayer3d.gd b/src/DynamicSoundPlayer3d.gd index 4cc5ca7..f6ec709 100644 --- a/src/DynamicSoundPlayer3d.gd +++ b/src/DynamicSoundPlayer3d.gd @@ -21,17 +21,6 @@ extends AudioStreamPlayer3D ## When true, finished songs are re-appended to the back of the queue so the playlist cycles forever. @export var Loop : bool; -## How playback pitch is selected. -@export var PitchMode : DynamicSoundConstants.PitchMode = DynamicSoundConstants.PitchMode.Constant: - set(value): - PitchMode = value; - notify_property_list_changed(); - -## Minimum pitch scale when [member PitchMode] is Random. -@export var RandomMinPitchScale : float = -0.5; -## Maximum pitch scale when [member PitchMode] is Random. -@export var RandomMaxPitchScale : float = 0.5; - var _core : DynamicSoundPlayerCore; func _set(property: StringName, value: Variant) -> bool: @@ -48,11 +37,6 @@ func _enter_tree() -> void: if bus == &"Master" and AudioServer.get_bus_index("Music") != -1: bus = "Music"; -func _validate_property(property: Dictionary) -> void: - if property.name in ["RandomMinPitchScale", "RandomMaxPitchScale"]: - if PitchMode != DynamicSoundConstants.PitchMode.Random: - property.usage &= ~PROPERTY_USAGE_EDITOR; - func _ready() -> void: _core = DynamicSoundPlayerCore.new(self); _core.ready();