diff --git a/C7/Audio/AudioLoader.cs.uid b/C7/Audio/AudioLoader.cs.uid new file mode 100644 index 000000000..25106e617 --- /dev/null +++ b/C7/Audio/AudioLoader.cs.uid @@ -0,0 +1 @@ +uid://d3ppdw6qcpc4k diff --git a/C7/Audio/AudioManager.cs b/C7/Audio/AudioManager.cs new file mode 100644 index 000000000..34f9caec5 --- /dev/null +++ b/C7/Audio/AudioManager.cs @@ -0,0 +1,143 @@ + +using System; +using C7Engine; +using Godot; +using Serilog; + +[GlobalClass] +public partial class AudioManager : Node { + private ILogger log; + + [Export] AudioStreamPlayer musicPlayer; + [Export] AudioStreamPlayer sfxAudioPlayer; + [Export] AudioStreamPlayer uiAudioPlayer; + [Export] AudioStreamPlayer ambienceAudioPlayer; + + private PolyphonicAudioPlayer _polyMusicPlayer; + private PolyphonicAudioPlayer _polySfxAudioPlayer; + private PolyphonicAudioPlayer _polyUiAudioPlayer; + private PolyphonicAudioPlayer _polyAmbienceAudioPlayer; + + private static string AudioSettingsSection = "audio"; + + public static string MusicBus = "Music"; + public static string SfxAudioBus = "Sfx"; + public static string UIAudioBus = "UI"; + public static string AmbienceAudioBus = "Ambience"; + + private bool musicEnabled = true; + private bool sfxAudioEnabled = true; + private bool uiAudioEnabled = true; + private bool ambienceAudioEnabled = true; + + public override void _Ready() { + log = LogManager.ForContext(); + + musicEnabled = ConfigureVolume("musicVolume", MusicBus); + _polyMusicPlayer = new PolyphonicAudioPlayer(musicPlayer, 2); + + sfxAudioEnabled = ConfigureVolume("sfxAudioVolume", SfxAudioBus); + _polySfxAudioPlayer = new PolyphonicAudioPlayer(sfxAudioPlayer, 32); + + uiAudioEnabled = ConfigureVolume("uiAudioVolume", UIAudioBus); + _polyUiAudioPlayer = new PolyphonicAudioPlayer(uiAudioPlayer, 8); + + ambienceAudioEnabled = ConfigureVolume("ambienceAudioVolume", AmbienceAudioBus); + _polyAmbienceAudioPlayer = new PolyphonicAudioPlayer(ambienceAudioPlayer, 8); + } + + private bool ConfigureVolume(string volumeKey, string audioBus) { + try { + string volume = C7Settings.GetSettingValue(AudioSettingsSection, volumeKey); + float volumeDb = LogicalVolumeAsDecibel(volume, volumeKey); + + if (volumeDb == float.MinValue) { + return false; + } + + log.Debug("setting {volumeKey} to {volume}, which is {offset} decibel (offset)", + volumeKey, volume, volumeDb); + + int busIndex = AudioServer.GetBusIndex(audioBus); + AudioServer.SetBusVolumeDb(busIndex, volumeDb); + + return true; + } catch (ApplicationException ex) { + log.Error(ex, "could not configure {volumeKey}", volumeKey); + return false; + } + } + + /** + * Godot uses a decibel offset volume system, described at https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html + * This is what audio professionals would use, but is not intuitive to end users. + * In this system, a 6db difference halves or doubles the volume. + * Our users are probably more used to a 0% to 100% system. + * So this method converts between them. + */ + private float LogicalVolumeAsDecibel(string volume, string volumeKey) { + if (volume == null) { + //First run. Save the setting. + C7Settings.SetValue(AudioSettingsSection, volumeKey, "100"); + C7Settings.SaveSettings(); + return 0; + } + int userVolumeSetting = int.Parse(volume); + if (userVolumeSetting == 100) { + return 0; + } else if (userVolumeSetting == 0) { + return float.MinValue; + } else { + //Conversion math based on https://stackoverflow.com/a/37810295/3534605 + return 20.0f * (float)(Math.Log10(userVolumeSetting / 100.0f)); + } + } + + // TODO: playlists, mixing, transitions + // See: https://www.youtube.com/watch?app=desktop&v=07Kyqqg31FI&t=346s + + // TODO: polyphony via AudioStreamPolyphonic + AudioStreamPlaybackPolyphonic + // https://docs.godotengine.org/en/4.0/classes/class_audiostreampolyphonic.html + + private long currentMusicId = AudioStreamPlaybackPolyphonic.InvalidId; + + public void PlayMusic(string configKey) { + AudioStream stream = AudioLoader.Load(configKey); + + if (stream == null) + return; + + if (currentMusicId != AudioStreamPlaybackPolyphonic.InvalidId) { + _polyMusicPlayer.Stop(currentMusicId); + currentMusicId = AudioStreamPlaybackPolyphonic.InvalidId; + } + + currentMusicId = _polyMusicPlayer.Play(stream); + } + + + public void StopMusic() { + if (currentMusicId != AudioStreamPlaybackPolyphonic.InvalidId) { + _polyMusicPlayer.Stop(currentMusicId); + currentMusicId = AudioStreamPlaybackPolyphonic.InvalidId; + } + } + + public void PlaySfxAudio(string configKey) { + var stream = AudioLoader.Load(configKey); + if (stream != null) + _polySfxAudioPlayer.Play(stream); + } + + public void PlayUIAudio(string configKey) { + var stream = AudioLoader.Load(configKey); + if (stream != null) + _polyUiAudioPlayer.Play(stream); + } + + public void PlayAmbienceAudio(string configKey) { + var stream = AudioLoader.Load(configKey); + if (stream != null) + _polyAmbienceAudioPlayer.Play(stream); + } +} diff --git a/C7/Audio/AudioManager.cs.uid b/C7/Audio/AudioManager.cs.uid new file mode 100644 index 000000000..9657eb5af --- /dev/null +++ b/C7/Audio/AudioManager.cs.uid @@ -0,0 +1 @@ +uid://eyk3k7dd7tcy diff --git a/C7/Audio/AudioManager.tscn b/C7/Audio/AudioManager.tscn new file mode 100644 index 000000000..4dc4ea47e --- /dev/null +++ b/C7/Audio/AudioManager.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=2 format=3 uid="uid://4hh28tdyqvfg"] + +[ext_resource type="Script" uid="uid://eyk3k7dd7tcy" path="res://Audio/AudioManager.cs" id="1_l0aok"] + +[node name="AudioManager" type="Node" node_paths=PackedStringArray("musicPlayer", "sfxAudioPlayer", "uiAudioPlayer", "ambienceAudioPlayer")] +script = ExtResource("1_l0aok") +musicPlayer = NodePath("MusicPlayer") +sfxAudioPlayer = NodePath("SfxAudioPlayer") +uiAudioPlayer = NodePath("UIAudioPlayer") +ambienceAudioPlayer = NodePath("AmbienceAudioPlayer") + +[node name="MusicPlayer" type="AudioStreamPlayer" parent="."] +bus = &"Music" + +[node name="SfxAudioPlayer" type="AudioStreamPlayer" parent="."] +bus = &"Sfx" + +[node name="UIAudioPlayer" type="AudioStreamPlayer" parent="."] +bus = &"UI" + +[node name="AmbienceAudioPlayer" type="AudioStreamPlayer" parent="."] +bus = &"Ambience" diff --git a/C7/Audio/PolyphonicAudioPlayer.cs b/C7/Audio/PolyphonicAudioPlayer.cs new file mode 100644 index 000000000..0e3ed9bd9 --- /dev/null +++ b/C7/Audio/PolyphonicAudioPlayer.cs @@ -0,0 +1,35 @@ +using Godot; + +public class PolyphonicAudioPlayer { + private readonly AudioStreamPlayer player; + private AudioStreamPlaybackPolyphonic playback; + + public PolyphonicAudioPlayer(AudioStreamPlayer player, int polyphony = 32) { + this.player = player; + + player.Stream = new AudioStreamPolyphonic { + Polyphony = polyphony + }; + + player.Play(); + + playback = player.GetStreamPlayback() + as AudioStreamPlaybackPolyphonic; + } + + public long Play(AudioStream stream) { + return playback.PlayStream(stream); + } + + public void Stop(long id) { + playback.StopStream(id); + } + + public void StopAll() { + player.Stop(); + player.Play(); + + playback = player.GetStreamPlayback() + as AudioStreamPlaybackPolyphonic; + } +} diff --git a/C7/Game.cs b/C7/Game.cs index ef9bbc354..aa41d7889 100644 --- a/C7/Game.cs +++ b/C7/Game.cs @@ -205,6 +205,13 @@ private async Task InitializeGame() { } InitializeMapView(); + InitializeAudio(); + } + + private void InitializeAudio() { + AudioManager audio = GetNode("/root/GlobalAudioManager"); + audio.StopMusic(); + // TODO: switch to in-game playlist / music logic } private async Task StartGame() { diff --git a/C7/UIElements/MainMenu/MainMenu.cs b/C7/UIElements/MainMenu/MainMenu.cs index 9801f249a..df88633f3 100644 --- a/C7/UIElements/MainMenu/MainMenu.cs +++ b/C7/UIElements/MainMenu/MainMenu.cs @@ -16,10 +16,9 @@ public partial class MainMenu : Node { Civ3FileDialog LoadScenarioDialog; [Export] MenuButtonContainer ButtonContainer; - [Export] - AudioStreamPlayer player; GlobalSingleton Global; + AudioManager AudioManager; public override void _Ready() { log = LogManager.ForContext(); @@ -29,6 +28,9 @@ public override void _Ready() { try { DisplayTitleScreen(); + + AudioManager = GetNode("/root/GlobalAudioManager"); + PlayMusic(); } catch (Exception ex) { log.Error(ex, "Could not set up the main menu"); } @@ -148,16 +150,6 @@ public void _on_Exit_pressed() { GetTree().Quit(); // no need to notify the scene tree } - private void PlayButtonPressedSound() { - AudioStream stream = AudioLoader.Load("buttons.button_1"); - - if (stream == null) - return; - - player.Stream = stream; - player.Play(); - } - private void _on_SetCiv3Home_pressed() { SetCiv3HomeDialog.Popup(); } @@ -174,4 +166,12 @@ private void UseStandaloneModePressed() { Global.ActivateGameMode(GamePaths.standalone); DisplayTitleScreen(); } + + private void PlayMusic() { + AudioManager.PlayMusic("menu.main_menu_1"); + } + + private void PlayButtonPressedSound() { + AudioManager.PlayUIAudio("buttons.button_1"); + } } diff --git a/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs b/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs deleted file mode 100644 index c7b776242..000000000 --- a/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Godot; -using System; -using C7Engine; -using Serilog; - -public partial class MainMenuMusicPlayer : AudioStreamPlayer { - private ILogger log; - - private bool musicEnabled = true; - - // Called when the node enters the scene tree for the first time. - public override void _Ready() { - log = LogManager.ForContext(); - //Figured out how to load the mp3 from this post: https://godotengine.org/qa/30210/how-do-load-resource-works - - try { - AudioStream stream = AudioLoader.Load("menu.main_menu_1"); - this.Stream = stream; - - string volume = C7Settings.GetSettingValue("audio", "musicVolume"); - float targetVolumeOffset = GetVolumeOffset(volume); - //Intentionally comparing to float.MinValue. It will match exactly if the INI volume settings is zero - if (targetVolumeOffset == float.MinValue) { - musicEnabled = false; - } - - if (musicEnabled) { - log.Debug("setting volume to {volume}, which is {offset} decibel offset", volume, targetVolumeOffset); - int busIndex = AudioServer.GetBusIndex(Bus); - AudioServer.SetBusVolumeDb(busIndex, targetVolumeOffset); - Play(); - } - } catch (ApplicationException ex) { - log.Error(ex, "could not load mp3 for main menu music"); - } - } - - /** - * Godot uses a decibel offset volume system, described at https://docs.godotengine.org/en/stable/tutorials/audio/audio_buses.html - * This is what audio professionals would use, but is not intuitive to end users. - * In this system, a 6db difference halves or doubles the volume. - * Our users are probably more used to a 0% to 100% system. - * So this method converts between them. - */ - private float GetVolumeOffset(string volume) { - if (volume == null) { - //First run. Save the setting. - C7Settings.SetValue("audio", "musicVolume", "100"); - C7Settings.SaveSettings(); - return 0; - } - int userVolumeSetting = int.Parse(volume); - if (userVolumeSetting == 100) { - return 0; - } else if (userVolumeSetting == 0) { - return float.MinValue; - } else { - //Conversion math based on https://stackoverflow.com/a/37810295/3534605 - return 20.0f * (float)(Math.Log10(userVolumeSetting / 100.0f)); - } - } -} diff --git a/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs.uid b/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs.uid deleted file mode 100644 index 9cd659738..000000000 --- a/C7/UIElements/MainMenu/MainMenuMusicPlayer.cs.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dauceqein40wj diff --git a/C7/UIElements/MainMenu/main_menu.tscn b/C7/UIElements/MainMenu/main_menu.tscn index 64e9f3a6f..11d419416 100644 --- a/C7/UIElements/MainMenu/main_menu.tscn +++ b/C7/UIElements/MainMenu/main_menu.tscn @@ -1,11 +1,10 @@ -[gd_scene load_steps=13 format=3 uid="uid://gnt0y3og7nk6"] +[gd_scene load_steps=12 format=3 uid="uid://gnt0y3og7nk6"] [ext_resource type="Script" uid="uid://dgpuse88jc2hc" path="res://UIElements/MainMenu/MainMenu.cs" id="1_akneh"] [ext_resource type="Texture2D" uid="uid://s55xvcaini8h" path="res://title-screen.png" id="2_erxyj"] [ext_resource type="Script" uid="uid://ca14gkdccjfcg" path="res://UIElements/MainMenu/MenuButtonContainer.cs" id="3_t1nuq"] [ext_resource type="FontFile" uid="uid://b1xkj8g82jbx5" path="res://Fonts/NotoSans-BoldItalic.ttf" id="3_x68dx"] [ext_resource type="Texture2D" uid="uid://ovwv08a53d7k" path="res://title.png" id="4_brea7"] -[ext_resource type="Script" uid="uid://dauceqein40wj" path="res://UIElements/MainMenu/MainMenuMusicPlayer.cs" id="4_tqr50"] [ext_resource type="PackedScene" uid="uid://bvncm1bgcii5e" path="res://UIElements/civ3_file_dialog.tscn" id="5_rw2g7"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_vbij8"] @@ -204,7 +203,7 @@ offset_top = -48.0 grow_horizontal = 2 grow_vertical = 0 text = "OpenCiv3 0.3 \"Dutch 2\" © OpenCiv3 contributors -OpenCiv3 is not affiliated with civfanatics.com, Firaxis Games, BreakAway Games, +OpenCiv3 is not affiliated with civfanatics.com, Firaxis Games, BreakAway Games, Hasbro Interactive, Infogrames Interactive, Atari Interactive, or Take-Two Interactive Software. All trademarks are property of their respective owners." label_settings = SubResource("LabelSettings_brea7") @@ -212,9 +211,6 @@ horizontal_alignment = 1 vertical_alignment = 2 autowrap_mode = 3 -[node name="MainMenuMusicPlayer" type="AudioStreamPlayer" parent="."] -script = ExtResource("4_tqr50") - [node name="SoundEffectPlayer" type="AudioStreamPlayer" parent="."] [node name="SetCiv3HomeDialog" type="FileDialog" parent="."] diff --git a/C7/default_bus_layout.tres b/C7/default_bus_layout.tres new file mode 100644 index 000000000..29c9ff0da --- /dev/null +++ b/C7/default_bus_layout.tres @@ -0,0 +1,27 @@ +[gd_resource type="AudioBusLayout" format=3 uid="uid://cw11tyqth76rn"] + +[resource] +bus/1/name = &"Music" +bus/1/solo = false +bus/1/mute = false +bus/1/bypass_fx = false +bus/1/volume_db = 0.0 +bus/1/send = &"Master" +bus/2/name = &"Sfx" +bus/2/solo = false +bus/2/mute = false +bus/2/bypass_fx = false +bus/2/volume_db = 0.0 +bus/2/send = &"Master" +bus/3/name = &"UI" +bus/3/solo = false +bus/3/mute = false +bus/3/bypass_fx = false +bus/3/volume_db = 0.0 +bus/3/send = &"Master" +bus/4/name = &"Ambience" +bus/4/solo = false +bus/4/mute = false +bus/4/bypass_fx = false +bus/4/volume_db = 0.0 +bus/4/send = &"Master" diff --git a/C7/project.godot b/C7/project.godot index 6b1280d65..54a5be390 100644 --- a/C7/project.godot +++ b/C7/project.godot @@ -22,6 +22,7 @@ config/icon="res://icon.png" GlobalSingleton="*res://GlobalSingleton.cs" LogManager="*res://LogManager.cs" +GlobalAudioManager="*res://Audio/AudioManager.tscn" [display]