mirror of
https://gitlab.com/veloren/veloren.git
synced 2025-07-25 21:02:31 +00:00
Switch from Rodio to Kira
This commit is contained in:
@ -68,6 +68,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- (un)equipping sceptres and staffs changed to default sound
|
||||
- Sprite deletion timeouts added for spider webs and Harvester vines
|
||||
- RiposteMelee attacks have a recovery phase after missing
|
||||
- Switched from Rodio to Kira
|
||||
- Ambient noise now gets filtered when underwater.
|
||||
|
||||
### Removed
|
||||
|
||||
@ -93,6 +95,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Naming of entries in controls settings.
|
||||
- Percentage values in descriptions of potion sickness and agility potion.
|
||||
- Potentially fixed graphical issues with the vulkan backend on AMD windows platforms.
|
||||
- Default audio settings button now works properly.
|
||||
- Positional audio is less glitchy.
|
||||
- Thunder sfx (corresponding with lightning) is now controlled by ambience volume.
|
||||
|
||||
## [0.16.0] - 2024-03-30
|
||||
|
||||
|
181
Cargo.lock
generated
181
Cargo.lock
generated
@ -521,7 +521,7 @@ dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.37",
|
||||
"regex",
|
||||
@ -2684,6 +2684,15 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28091a37a5d09b555cb6628fd954da299b536433834f5b8e59eba78e0cbbf8a"
|
||||
dependencies = [
|
||||
"mint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
@ -3161,7 +3170,7 @@ version = "0.2.0"
|
||||
source = "git+https://github.com/Imberflur/iced?tag=veloren-winit-0.28#47243c257c8b8dd6c506b060804cb00b618aa0aa"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"glam",
|
||||
"glam 0.10.2",
|
||||
"iced_native",
|
||||
"iced_style",
|
||||
"raw-window-handle 0.5.2",
|
||||
@ -3605,6 +3614,22 @@ dependencies = [
|
||||
"ubyte",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kira"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc562c3c440485a06d529f68bcff850c4eb58ba4caddf14fa12cd6077acce17c"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"glam 0.29.0",
|
||||
"mint",
|
||||
"paste",
|
||||
"ringbuf",
|
||||
"send_wrapper",
|
||||
"symphonia",
|
||||
"triple_buffer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.0.8"
|
||||
@ -3649,17 +3674,6 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760"
|
||||
|
||||
[[package]]
|
||||
name = "lewton"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"ogg",
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
@ -4016,6 +4030,12 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mint"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
@ -4472,7 +4492,7 @@ version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.37",
|
||||
"syn 1.0.109",
|
||||
@ -4484,7 +4504,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.37",
|
||||
"syn 2.0.79",
|
||||
@ -4496,7 +4516,7 @@ version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2 1.0.86",
|
||||
"quote 1.0.37",
|
||||
"syn 2.0.79",
|
||||
@ -4707,15 +4727,6 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ogg"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.1"
|
||||
@ -5044,15 +5055,6 @@ dependencies = [
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
|
||||
dependencies = [
|
||||
"toml_edit 0.22.22",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-hack"
|
||||
version = "0.5.20+deprecated"
|
||||
@ -5552,6 +5554,15 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ringbuf"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a"
|
||||
dependencies = [
|
||||
"crossbeam-utils 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.14"
|
||||
@ -5574,17 +5585,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1fceb9d127d515af1586d8d0cc601e1245bdb0af38e75c865a156290184f5b3"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"lewton",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.8.1"
|
||||
@ -5992,6 +5992,12 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
|
||||
[[package]]
|
||||
name = "send_wrapper"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
@ -6525,6 +6531,77 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20e16a0f46cf5fd675563ef54f26e83e20f2366bcf027bcb3cc3ed2b98aaf2ca"
|
||||
|
||||
[[package]]
|
||||
name = "symphonia"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"symphonia-codec-vorbis",
|
||||
"symphonia-core",
|
||||
"symphonia-format-ogg",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-codec-vorbis"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-utils-xiph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-core"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 1.3.2",
|
||||
"bytemuck",
|
||||
"lazy_static",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-ogg"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
"symphonia-utils-xiph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-metadata"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-utils-xiph"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe"
|
||||
dependencies = [
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "0.15.44"
|
||||
@ -7017,6 +7094,15 @@ dependencies = [
|
||||
"vek 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triple_buffer"
|
||||
version = "8.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e66931c8eca6381f0d34656a9341f09bd462010488c1a3bc0acd3f2d08dffce"
|
||||
dependencies = [
|
||||
"crossbeam-utils 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.5"
|
||||
@ -7222,6 +7308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86dce3b89992dbfee9b6f46d8a98a4a5ecf79f93f3b077fad3cc2759ebe92214"
|
||||
dependencies = [
|
||||
"approx 0.5.1",
|
||||
"mint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"rustc_version 0.4.1",
|
||||
@ -7675,6 +7762,7 @@ dependencies = [
|
||||
"inline_tweak",
|
||||
"itertools 0.13.0",
|
||||
"keyboard-keynames",
|
||||
"kira",
|
||||
"lazy_static",
|
||||
"levenshtein",
|
||||
"mimalloc",
|
||||
@ -7686,7 +7774,6 @@ dependencies = [
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rayon",
|
||||
"rodio",
|
||||
"ron",
|
||||
"serde",
|
||||
"serde_with",
|
||||
@ -8552,7 +8639,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"khronos-egl",
|
||||
"libc",
|
||||
"libloading 0.7.4",
|
||||
"libloading 0.8.5",
|
||||
"log",
|
||||
"metal",
|
||||
"naga",
|
||||
|
@ -149,7 +149,7 @@ crossbeam-channel = { version = "0.5" }
|
||||
ordered-float = { version = "4.2", default-features = true }
|
||||
num = { version = "0.4" }
|
||||
num-traits = { version = "0.2" }
|
||||
vek = { version = "0.17.0", features = ["serde"] }
|
||||
vek = { version = "0.17.0", features = ["serde", "mint"] }
|
||||
itertools = { version = "0.13" }
|
||||
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
|
@ -2,27 +2,22 @@
|
||||
tracks: [
|
||||
(
|
||||
path: "voxygen.audio.ambience.wind",
|
||||
length: 14.2,
|
||||
tag: Wind,
|
||||
),
|
||||
(
|
||||
path: "voxygen.audio.ambience.rain",
|
||||
length: 17.0,
|
||||
tag: Rain,
|
||||
),
|
||||
(
|
||||
path:"voxygen.audio.ambience.thunder",
|
||||
length: 32.0,
|
||||
tag: Thunder,
|
||||
tag: ThunderRumbling,
|
||||
),
|
||||
(
|
||||
path:"voxygen.audio.ambience.leaves",
|
||||
length: 26.0,
|
||||
tag: Leaves,
|
||||
),
|
||||
(
|
||||
path:"voxygen.audio.ambience.cave",
|
||||
length: 75.5,
|
||||
tag: Cave,
|
||||
)
|
||||
]
|
@ -443,50 +443,50 @@
|
||||
|
||||
// Combat Music
|
||||
|
||||
Segmented(
|
||||
title: "Barred Paths",
|
||||
timing: None,
|
||||
weather: None,
|
||||
biomes: [],
|
||||
sites: [
|
||||
Dungeon(Myrmidon),
|
||||
],
|
||||
segments: [
|
||||
("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-start", 61.818, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-loop", 54.545, Activity(Combat(High)), None),
|
||||
("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-end", 6.0, Transition(Combat(High), Explore), None),
|
||||
],
|
||||
artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
),
|
||||
Segmented(
|
||||
title: "Reversal",
|
||||
timing: None,
|
||||
weather: None,
|
||||
biomes: [],
|
||||
sites: [
|
||||
Dungeon(Myrmidon),
|
||||
],
|
||||
segments: [
|
||||
("voxygen.audio.soundtrack.combat.reversal.reversal-start", 61.666, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
("voxygen.audio.soundtrack.combat.reversal.reversal-loop", 60.0, Activity(Combat(High)), None),
|
||||
("voxygen.audio.soundtrack.combat.reversal.reversal-end", 3.666, Transition(Combat(High), Explore), None),
|
||||
],
|
||||
artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
),
|
||||
Segmented(
|
||||
title: "Clash",
|
||||
timing: None,
|
||||
weather: None,
|
||||
biomes: [],
|
||||
sites: [
|
||||
Dungeon(Myrmidon),
|
||||
],
|
||||
segments: [
|
||||
("voxygen.audio.soundtrack.combat.clash.clash-start", 121.5, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
("voxygen.audio.soundtrack.combat.clash.clash-loop", 81.0, Activity(Combat(High)), None),
|
||||
("voxygen.audio.soundtrack.combat.clash.clash-end", 1.5, Transition(Combat(High), Explore), None),
|
||||
],
|
||||
artist: ("Alfredo Pompa D & Rodriogo Plata", None),
|
||||
),
|
||||
// Segmented(
|
||||
// title: "Barred Paths",
|
||||
// timing: None,
|
||||
// weather: None,
|
||||
// biomes: [],
|
||||
// sites: [
|
||||
// Dungeon(Old),
|
||||
// ],
|
||||
// segments: [
|
||||
// ("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-start", 61.818, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
// ("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-loop", 54.545, Activity(Combat(High)), None),
|
||||
// ("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-end", 6.0, Transition(Combat(High), Explore), None),
|
||||
// ],
|
||||
// artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
// ),
|
||||
// Segmented(
|
||||
// title: "Reversal",
|
||||
// timing: None,
|
||||
// weather: None,
|
||||
// biomes: [],
|
||||
// sites: [
|
||||
// Dungeon(Old),
|
||||
// ],
|
||||
// segments: [
|
||||
// ("voxygen.audio.soundtrack.combat.reversal.reversal-start", 61.666, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
// ("voxygen.audio.soundtrack.combat.reversal.reversal-loop", 60.0, Activity(Combat(High)), None),
|
||||
// ("voxygen.audio.soundtrack.combat.reversal.reversal-end", 3.666, Transition(Combat(High), Explore), None),
|
||||
// ],
|
||||
// artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
// ),
|
||||
// Segmented(
|
||||
// title: "Clash",
|
||||
// timing: None,
|
||||
// weather: None,
|
||||
// biomes: [],
|
||||
// sites: [
|
||||
// Dungeon(Old),
|
||||
// ],
|
||||
// segments: [
|
||||
// ("voxygen.audio.soundtrack.combat.clash.clash-start", 121.5, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
// ("voxygen.audio.soundtrack.combat.clash.clash-loop", 81.0, Activity(Combat(High)), None),
|
||||
// ("voxygen.audio.soundtrack.combat.clash.clash-end", 1.5, Transition(Combat(High), Explore), None),
|
||||
// ],
|
||||
// artist: ("Alfredo Pompa D & Rodriogo Plata", None),
|
||||
// ),
|
||||
]
|
||||
)
|
||||
)
|
@ -301,50 +301,50 @@
|
||||
|
||||
// Combat Music
|
||||
|
||||
Segmented(
|
||||
title: "Barred Paths",
|
||||
timing: None,
|
||||
weather: None,
|
||||
biomes: [],
|
||||
sites: [
|
||||
Dungeon(Myrmidon),
|
||||
],
|
||||
segments: [
|
||||
("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-start", 61.818, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-loop", 54.545, Activity(Combat(High)), None),
|
||||
("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-end", 6.0, Transition(Combat(High), Explore), None),
|
||||
],
|
||||
artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
),
|
||||
Segmented(
|
||||
title: "Reversal",
|
||||
timing: None,
|
||||
weather: None,
|
||||
biomes: [],
|
||||
sites: [
|
||||
Dungeon(Myrmidon),
|
||||
],
|
||||
segments: [
|
||||
("voxygen.audio.soundtrack.combat.reversal.reversal-start", 61.666, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
("voxygen.audio.soundtrack.combat.reversal.reversal-loop", 60.0, Activity(Combat(High)), None),
|
||||
("voxygen.audio.soundtrack.combat.reversal.reversal-end", 3.666, Transition(Combat(High), Explore), None),
|
||||
],
|
||||
artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
),
|
||||
Segmented(
|
||||
title: "Clash",
|
||||
timing: None,
|
||||
weather: None,
|
||||
biomes: [],
|
||||
sites: [
|
||||
Dungeon(Myrmidon),
|
||||
],
|
||||
segments: [
|
||||
("voxygen.audio.soundtrack.combat.clash.clash-start", 121.5, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
("voxygen.audio.soundtrack.combat.clash.clash-loop", 81.0, Activity(Combat(High)), None),
|
||||
("voxygen.audio.soundtrack.combat.clash.clash-end", 1.5, Transition(Combat(High), Explore), None),
|
||||
],
|
||||
artist: ("Alfredo Pompa D & Rodriogo Plata", None),
|
||||
),
|
||||
// Segmented(
|
||||
// title: "Barred Paths",
|
||||
// timing: None,
|
||||
// weather: None,
|
||||
// biomes: [],
|
||||
// sites: [
|
||||
// Dungeon(Old),
|
||||
// ],
|
||||
// segments: [
|
||||
// ("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-start", 61.818, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
// ("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-loop", 54.545, Activity(Combat(High)), None),
|
||||
// ("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-end", 6.0, Transition(Combat(High), Explore), None),
|
||||
// ],
|
||||
// artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
// ),
|
||||
// Segmented(
|
||||
// title: "Reversal",
|
||||
// timing: None,
|
||||
// weather: None,
|
||||
// biomes: [],
|
||||
// sites: [
|
||||
// Dungeon(Old),
|
||||
// ],
|
||||
// segments: [
|
||||
// ("voxygen.audio.soundtrack.combat.reversal.reversal-start", 61.666, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
// ("voxygen.audio.soundtrack.combat.reversal.reversal-loop", 60.0, Activity(Combat(High)), None),
|
||||
// ("voxygen.audio.soundtrack.combat.reversal.reversal-end", 3.666, Transition(Combat(High), Explore), None),
|
||||
// ],
|
||||
// artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
// ),
|
||||
// Segmented(
|
||||
// title: "Clash",
|
||||
// timing: None,
|
||||
// weather: None,
|
||||
// biomes: [],
|
||||
// sites: [
|
||||
// Dungeon(Old),
|
||||
// ],
|
||||
// segments: [
|
||||
// ("voxygen.audio.soundtrack.combat.clash.clash-start", 121.5, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
// ("voxygen.audio.soundtrack.combat.clash.clash-loop", 81.0, Activity(Combat(High)), None),
|
||||
// ("voxygen.audio.soundtrack.combat.clash.clash-end", 1.5, Transition(Combat(High), Explore), None),
|
||||
// ],
|
||||
// artist: ("Alfredo Pompa D & Rodriogo Plata", None),
|
||||
// ),
|
||||
]
|
||||
)
|
||||
)
|
@ -8,7 +8,7 @@
|
||||
(TitleMusic, Exploration): (4.0, 4.0),
|
||||
(TitleMusic, Combat): (4.0, 4.0),
|
||||
(Exploration, TitleMusic): (2.0, 2.0),
|
||||
(Exploration, Combat): (0.5, 0.5),
|
||||
(Exploration, Combat): (1.0, 0.5),
|
||||
(Combat, Exploration): (2.0, 5.0),
|
||||
(Combat, TitleMusic): (2.0, 2.0),
|
||||
},
|
||||
|
@ -894,50 +894,50 @@
|
||||
|
||||
// Combat Music
|
||||
|
||||
Segmented(
|
||||
title: "Barred Paths",
|
||||
timing: None,
|
||||
weather: None,
|
||||
biomes: [],
|
||||
sites: [
|
||||
Dungeon(Myrmidon),
|
||||
],
|
||||
segments: [
|
||||
("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-start", 61.818, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-loop", 54.545, Activity(Combat(High)), None),
|
||||
("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-end", 6.0, Transition(Combat(High), Explore), None),
|
||||
],
|
||||
artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
),
|
||||
Segmented(
|
||||
title: "Reversal",
|
||||
timing: None,
|
||||
weather: None,
|
||||
biomes: [],
|
||||
sites: [
|
||||
Dungeon(Myrmidon),
|
||||
],
|
||||
segments: [
|
||||
("voxygen.audio.soundtrack.combat.reversal.reversal-start", 61.666, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
("voxygen.audio.soundtrack.combat.reversal.reversal-loop", 60.0, Activity(Combat(High)), None),
|
||||
("voxygen.audio.soundtrack.combat.reversal.reversal-end", 3.666, Transition(Combat(High), Explore), None),
|
||||
],
|
||||
artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
),
|
||||
Segmented(
|
||||
title: "Clash",
|
||||
timing: None,
|
||||
weather: None,
|
||||
biomes: [],
|
||||
sites: [
|
||||
Dungeon(Myrmidon),
|
||||
],
|
||||
segments: [
|
||||
("voxygen.audio.soundtrack.combat.clash.clash-start", 121.5, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
("voxygen.audio.soundtrack.combat.clash.clash-loop", 81.0, Activity(Combat(High)), None),
|
||||
("voxygen.audio.soundtrack.combat.clash.clash-end", 1.5, Transition(Combat(High), Explore), None),
|
||||
],
|
||||
artist: ("Alfredo Pompa D & Rodriogo Plata", None),
|
||||
),
|
||||
// Segmented(
|
||||
// title: "Barred Paths",
|
||||
// timing: None,
|
||||
// weather: None,
|
||||
// biomes: [],
|
||||
// sites: [
|
||||
// Dungeon(Old),
|
||||
// ],
|
||||
// segments: [
|
||||
// ("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-start", 61.818, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
// ("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-loop", 54.545, Activity(Combat(High)), None),
|
||||
// ("voxygen.audio.soundtrack.combat.barred_paths.barred_paths-end", 6.0, Transition(Combat(High), Explore), None),
|
||||
// ],
|
||||
// artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
// ),
|
||||
// Segmented(
|
||||
// title: "Reversal",
|
||||
// timing: None,
|
||||
// weather: None,
|
||||
// biomes: [],
|
||||
// sites: [
|
||||
// Dungeon(Old),
|
||||
// ],
|
||||
// segments: [
|
||||
// ("voxygen.audio.soundtrack.combat.reversal.reversal-start", 61.666, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
// ("voxygen.audio.soundtrack.combat.reversal.reversal-loop", 60.0, Activity(Combat(High)), None),
|
||||
// ("voxygen.audio.soundtrack.combat.reversal.reversal-end", 3.666, Transition(Combat(High), Explore), None),
|
||||
// ],
|
||||
// artist: ("DaforLynx", "https://daforlynx.neocities.org/"),
|
||||
// ),
|
||||
// Segmented(
|
||||
// title: "Clash",
|
||||
// timing: None,
|
||||
// weather: None,
|
||||
// biomes: [],
|
||||
// sites: [
|
||||
// Dungeon(Old),
|
||||
// ],
|
||||
// segments: [
|
||||
// ("voxygen.audio.soundtrack.combat.clash.clash-start", 121.5, Transition(Explore, Combat(High)), Some(Combat(High))),
|
||||
// ("voxygen.audio.soundtrack.combat.clash.clash-loop", 81.0, Activity(Combat(High)), None),
|
||||
// ("voxygen.audio.soundtrack.combat.clash.clash-end", 1.5, Transition(Combat(High), Explore), None),
|
||||
// ],
|
||||
// artist: ("Alfredo Pompa D & Rodriogo Plata", None),
|
||||
// ),
|
||||
]
|
||||
)
|
||||
|
@ -77,6 +77,9 @@ where
|
||||
"veloren_server::persistence::character=info",
|
||||
"veloren_server::settings=info",
|
||||
"veloren_query_server=info",
|
||||
"symphonia_format_ogg::demuxer=off",
|
||||
"symphonia_core::probe=off",
|
||||
"wgpu_hal::dx12::device=off",
|
||||
];
|
||||
|
||||
for s in default_directives {
|
||||
|
@ -123,6 +123,11 @@ dot_vox = "5.1"
|
||||
guillotiere = "0.6.2"
|
||||
hashbrown = { workspace = true }
|
||||
image = { workspace = true, features = ["ico"] }
|
||||
kira = { version = "0.9.5", default-features = false, features = [
|
||||
"cpal",
|
||||
"symphonia",
|
||||
"ogg",
|
||||
] }
|
||||
lazy_static = { workspace = true }
|
||||
native-dialog = { version = "0.7.0", optional = true }
|
||||
num = { workspace = true }
|
||||
@ -130,7 +135,6 @@ ordered-float = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rodio = { version = "0.18", default-features = false, features = ["vorbis"] }
|
||||
ron = { workspace = true }
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_with = { version = "3.9.0", features = ["hashbrown_0_14"] }
|
||||
|
250
voxygen/src/audio/ambience.rs
Normal file
250
voxygen/src/audio/ambience.rs
Normal file
@ -0,0 +1,250 @@
|
||||
//! Handles ambient non-positional sounds
|
||||
use crate::{
|
||||
audio::{channel::AmbienceChannelTag, AudioFrontend},
|
||||
scene::Camera,
|
||||
};
|
||||
use client::Client;
|
||||
use common::{
|
||||
assets::{self, AssetExt, AssetHandle},
|
||||
terrain::site::SiteKindMeta,
|
||||
vol::ReadVol,
|
||||
};
|
||||
use common_state::State;
|
||||
use serde::Deserialize;
|
||||
use strum::IntoEnumIterator;
|
||||
use tracing::warn;
|
||||
use vek::*;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct AmbienceCollection {
|
||||
tracks: Vec<AmbienceItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AmbienceItem {
|
||||
path: String,
|
||||
/// Specifies which ambient channel to play on
|
||||
tag: AmbienceChannelTag,
|
||||
}
|
||||
|
||||
pub struct AmbienceMgr {
|
||||
pub ambience: AssetHandle<AmbienceCollection>,
|
||||
}
|
||||
|
||||
impl AmbienceMgr {
|
||||
pub fn maintain(
|
||||
&mut self,
|
||||
audio: &mut AudioFrontend,
|
||||
state: &State,
|
||||
client: &Client,
|
||||
camera: &Camera,
|
||||
) {
|
||||
if !audio.ambience_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let ambience_sounds = self.ambience.read();
|
||||
|
||||
let cam_pos = camera.get_pos_with_focus();
|
||||
|
||||
// Lowpass if underwater
|
||||
if state
|
||||
.terrain()
|
||||
.get(cam_pos.map(|e| e.floor() as i32))
|
||||
.map(|b| b.is_liquid())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
for channel in audio.ambience_channels.iter_mut() {
|
||||
channel.set_filter(1000);
|
||||
}
|
||||
} else {
|
||||
for channel in audio.ambience_channels.iter_mut() {
|
||||
channel.set_filter(20000);
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate through each tag
|
||||
for tag in AmbienceChannelTag::iter() {
|
||||
// Init: Spawn a channel for each tag
|
||||
// TODO: Find a good way to cull unneeded channels
|
||||
if audio.get_ambience_channel(tag).is_none() {
|
||||
audio.new_ambience_channel(tag);
|
||||
let track = ambience_sounds.tracks.iter().find(|track| track.tag == tag);
|
||||
if let Some(track) = track {
|
||||
audio.play_ambience_looping(tag, &track.path);
|
||||
}
|
||||
}
|
||||
if let Some(channel) = audio.get_ambience_channel(tag) {
|
||||
// Maintain: get the correct volume of whatever the tag of the current
|
||||
// channel is
|
||||
let target_volume = get_target_volume(tag, client, camera);
|
||||
|
||||
// Fade to the target volume over a short period of time
|
||||
channel.fade_to(target_volume, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AmbienceChannelTag {
|
||||
pub fn tag_max_volume(tag: AmbienceChannelTag) -> f32 {
|
||||
match tag {
|
||||
AmbienceChannelTag::Wind => 1.0,
|
||||
AmbienceChannelTag::Rain => 0.95,
|
||||
AmbienceChannelTag::ThunderRumbling => 1.33,
|
||||
AmbienceChannelTag::Leaves => 1.33,
|
||||
AmbienceChannelTag::Cave => 1.0,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
// Gets appropriate volume for each tag
|
||||
pub fn get_tag_volume(tag: AmbienceChannelTag, client: &Client, camera: &Camera) -> f32 {
|
||||
match tag {
|
||||
AmbienceChannelTag::Wind => {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() {
|
||||
(chunk.meta().alt(), chunk.meta().tree_density())
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
// Wind volume increases with altitude
|
||||
let alt_factor = (cam_pos.z / 1200.0).abs();
|
||||
|
||||
// Tree density factors into wind volume. The more trees,
|
||||
// the lower wind volume. The trees make more of an impact
|
||||
// the closer the camera is to the ground.
|
||||
let tree_factor = ((1.0 - (tree_density * 0.5))
|
||||
+ ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2))
|
||||
.min(1.0);
|
||||
|
||||
// Lastly, we of course have to take into account actual wind speed from
|
||||
// weathersim
|
||||
// Client wind speed is a float approx. -30.0 to 30.0 (polarity depending on
|
||||
// direction)
|
||||
let wind_speed_factor = (client.weather_at_player().wind.magnitude_squared()
|
||||
/ 15.0_f32.powi(2))
|
||||
.min(1.33);
|
||||
|
||||
(alt_factor
|
||||
* tree_factor
|
||||
* (wind_speed_factor + ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2)))
|
||||
+ (alt_factor * 0.15) * tree_factor
|
||||
},
|
||||
AmbienceChannelTag::Rain => {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let terrain_alt = if let Some(chunk) = client.current_chunk() {
|
||||
chunk.meta().alt()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// Make rain diminish with camera distance above terrain
|
||||
let camera_factor = 1.0 - ((cam_pos.z - terrain_alt).abs() / 75.0).powi(2).min(1.0);
|
||||
|
||||
(client.weather_at_player().rain * 3.0) * camera_factor
|
||||
},
|
||||
AmbienceChannelTag::ThunderRumbling => {
|
||||
let rain_intensity = client.weather_at_player().rain * 3.0;
|
||||
|
||||
if rain_intensity < 0.7 {
|
||||
0.0
|
||||
} else {
|
||||
rain_intensity
|
||||
}
|
||||
},
|
||||
AmbienceChannelTag::Leaves => {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() {
|
||||
(chunk.meta().alt(), chunk.meta().tree_density())
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
// Tree density factors into leaves volume. The more trees,
|
||||
// the higher volume. The trees make more of an impact
|
||||
// the closer the camera is to the ground
|
||||
let tree_factor = 1.0
|
||||
- (((1.0 - tree_density)
|
||||
+ ((cam_pos.z - terrain_alt - 20.0).abs() / 150.0).powi(2))
|
||||
.min(1.1));
|
||||
|
||||
// Take into account wind speed too, which amplifies tree noise
|
||||
let wind_speed_factor = (client.weather_at_player().wind.magnitude_squared()
|
||||
/ 20.0_f32.powi(2))
|
||||
.min(1.0);
|
||||
|
||||
if tree_factor > 0.1 {
|
||||
tree_factor * (1.0 + wind_speed_factor)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
},
|
||||
AmbienceChannelTag::Cave => {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let terrain_alt = if let Some(chunk) = client.current_chunk() {
|
||||
chunk.meta().alt()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// When the camera is roughly above ground, don't play cave sounds
|
||||
let camera_factor = (-(cam_pos.z - terrain_alt) / 100.0).max(0.0);
|
||||
|
||||
if client.current_site() == SiteKindMeta::Cave {
|
||||
camera_factor
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
},
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks various factors to determine the target volume to lerp to
|
||||
fn get_target_volume(tag: AmbienceChannelTag, client: &Client, camera: &Camera) -> f32 {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let volume: f32 = AmbienceChannelTag::get_tag_volume(tag, client, camera);
|
||||
|
||||
let terrain_alt = if let Some(chunk) = client.current_chunk() {
|
||||
chunk.meta().alt()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Is the camera underneath the terrain? Fade out the lower it goes beneath.
|
||||
// Unless, of course, the player is in a cave.
|
||||
if tag != AmbienceChannelTag::Cave {
|
||||
(volume * ((cam_pos.z - terrain_alt) / 50.0 + 1.0).clamped(0.0, 1.0))
|
||||
.min(AmbienceChannelTag::tag_max_volume(tag))
|
||||
} else {
|
||||
volume.min(AmbienceChannelTag::tag_max_volume(tag))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_ambience_items() -> AssetHandle<AmbienceCollection> {
|
||||
AmbienceCollection::load_or_insert_with("voxygen.audio.ambience", |error| {
|
||||
warn!(
|
||||
"Error reading ambience config file, ambience will not be available: {:#?}",
|
||||
error
|
||||
);
|
||||
AmbienceCollection::default()
|
||||
})
|
||||
}
|
||||
|
||||
impl assets::Asset for AmbienceCollection {
|
||||
type Loader = assets::RonLoader;
|
||||
|
||||
const EXTENSION: &'static str = "ron";
|
||||
}
|
@ -1,285 +0,0 @@
|
||||
//! Handles ambient non-positional sounds
|
||||
use crate::{
|
||||
audio::{channel::AmbientChannelTag, AudioFrontend},
|
||||
scene::Camera,
|
||||
};
|
||||
use client::Client;
|
||||
use common::{
|
||||
assets::{self, AssetExt, AssetHandle},
|
||||
terrain::site::SiteKindMeta,
|
||||
vol::ReadVol,
|
||||
};
|
||||
use common_state::State;
|
||||
use serde::Deserialize;
|
||||
use std::time::Instant;
|
||||
use strum::IntoEnumIterator;
|
||||
use tracing::warn;
|
||||
use vek::*;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct AmbientCollection {
|
||||
tracks: Vec<AmbientItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AmbientItem {
|
||||
path: String,
|
||||
/// Length of the track in seconds
|
||||
length: f32,
|
||||
/// Specifies which ambient channel to play on
|
||||
tag: AmbientChannelTag,
|
||||
}
|
||||
|
||||
pub struct AmbientMgr {
|
||||
pub ambience: AssetHandle<AmbientCollection>,
|
||||
}
|
||||
|
||||
impl AmbientMgr {
|
||||
pub fn maintain(
|
||||
&mut self,
|
||||
audio: &mut AudioFrontend,
|
||||
state: &State,
|
||||
client: &Client,
|
||||
camera: &Camera,
|
||||
) {
|
||||
// Checks if the ambience volume is set to zero or audio is disabled
|
||||
// This prevents us from running all the following code unnecessarily
|
||||
if !audio.ambience_enabled() {
|
||||
return;
|
||||
}
|
||||
let ambience_volume = audio.get_ambience_volume();
|
||||
let ambience = self.ambience.read();
|
||||
// Iterate through each tag
|
||||
for tag in AmbientChannelTag::iter() {
|
||||
// If the conditions warrant creating a channel of that tag
|
||||
if AmbientChannelTag::get_tag_volume(tag, client, camera)
|
||||
> match tag {
|
||||
AmbientChannelTag::Wind => 0.1,
|
||||
AmbientChannelTag::Rain => 0.1,
|
||||
AmbientChannelTag::Thunder => 0.1,
|
||||
AmbientChannelTag::Leaves => 0.05,
|
||||
AmbientChannelTag::Cave => 0.1,
|
||||
}
|
||||
&& audio.get_ambient_channel(tag).is_none()
|
||||
{
|
||||
audio.new_ambient_channel(tag);
|
||||
}
|
||||
// If a channel exists run volume code
|
||||
if let Some(channel_index) = audio.get_ambient_channel_index(tag) {
|
||||
let channel = &mut audio.ambient_channels[channel_index];
|
||||
|
||||
// Maintain: get the correct multiplier of whatever the tag of the current
|
||||
// channel is
|
||||
let target_volume = get_target_volume(tag, state, client, camera);
|
||||
// Get multiplier of the current channel
|
||||
let initial_volume = channel.multiplier;
|
||||
|
||||
// Lerp multiplier of current channel
|
||||
// TODO: Make this not framerate dependent
|
||||
channel.multiplier = Lerp::lerp(initial_volume, target_volume, 0.02);
|
||||
|
||||
// Update with sfx volume
|
||||
channel.set_volume(ambience_volume);
|
||||
|
||||
// If the sound should loop at this point:
|
||||
if channel.began_playing.elapsed().as_secs_f32() > channel.next_track_change {
|
||||
let track = ambience.tracks.iter().find(|track| track.tag == tag);
|
||||
// Set the channel's start point to this instant
|
||||
channel.began_playing = Instant::now();
|
||||
if let Some(track) = track {
|
||||
// Set loop duration to the one specified in the ron
|
||||
channel.next_track_change = track.length;
|
||||
// Play the file of the current tag at the current multiplier;
|
||||
let current_multiplier = channel.multiplier;
|
||||
audio.play_ambient(
|
||||
tag,
|
||||
&track.path,
|
||||
Some(current_multiplier * ambience_volume),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Remove channel if not playing
|
||||
if audio.ambient_channels[channel_index].multiplier <= 0.001 {
|
||||
audio.ambient_channels.remove(channel_index);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AmbientChannelTag {
|
||||
pub fn tag_max_volume(tag: AmbientChannelTag) -> f32 {
|
||||
match tag {
|
||||
AmbientChannelTag::Wind => 1.0,
|
||||
AmbientChannelTag::Rain => 0.95,
|
||||
AmbientChannelTag::Thunder => 1.33,
|
||||
AmbientChannelTag::Leaves => 1.33,
|
||||
AmbientChannelTag::Cave => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
// Gets appropriate volume for each tag
|
||||
pub fn get_tag_volume(tag: AmbientChannelTag, client: &Client, camera: &Camera) -> f32 {
|
||||
match tag {
|
||||
AmbientChannelTag::Wind => {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() {
|
||||
(chunk.meta().alt(), chunk.meta().tree_density())
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
// Wind volume increases with altitude
|
||||
let alt_multiplier = (cam_pos.z / 1200.0).abs();
|
||||
|
||||
// Tree density factors into wind volume. The more trees,
|
||||
// the lower wind volume. The trees make more of an impact
|
||||
// the closer the camera is to the ground.
|
||||
let tree_multiplier = ((1.0 - (tree_density * 0.5))
|
||||
+ ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2))
|
||||
.min(1.0);
|
||||
|
||||
// Lastly, we of course have to take into account actual wind speed from
|
||||
// weathersim
|
||||
// Client wind speed is a float approx. -30.0 to 30.0 (polarity depending on
|
||||
// direction)
|
||||
let wind_speed_multiplier = (client.weather_at_player().wind.magnitude_squared()
|
||||
/ 15.0_f32.powi(2))
|
||||
.min(1.33);
|
||||
|
||||
(alt_multiplier
|
||||
* tree_multiplier
|
||||
* (wind_speed_multiplier + ((cam_pos.z - terrain_alt).abs() / 150.0).powi(2)))
|
||||
+ (alt_multiplier * 0.15) * tree_multiplier
|
||||
},
|
||||
AmbientChannelTag::Rain => {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let terrain_alt = if let Some(chunk) = client.current_chunk() {
|
||||
chunk.meta().alt()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// Make rain diminish with camera distance above terrain
|
||||
let camera_multiplier =
|
||||
1.0 - ((cam_pos.z - terrain_alt).abs() / 75.0).powi(2).min(1.0);
|
||||
|
||||
(client.weather_at_player().rain * 3.0) * camera_multiplier
|
||||
},
|
||||
AmbientChannelTag::Thunder => {
|
||||
let rain_intensity = client.weather_at_player().rain * 3.0;
|
||||
|
||||
if rain_intensity < 0.7 {
|
||||
0.0
|
||||
} else {
|
||||
rain_intensity
|
||||
}
|
||||
},
|
||||
AmbientChannelTag::Leaves => {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let (terrain_alt, tree_density) = if let Some(chunk) = client.current_chunk() {
|
||||
(chunk.meta().alt(), chunk.meta().tree_density())
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
|
||||
// Tree density factors into leaves volume. The more trees,
|
||||
// the higher volume. The trees make more of an impact
|
||||
// the closer the camera is to the ground
|
||||
let tree_multiplier = 1.0
|
||||
- (((1.0 - tree_density)
|
||||
+ ((cam_pos.z - terrain_alt - 20.0).abs() / 150.0).powi(2))
|
||||
.min(1.1));
|
||||
|
||||
// Take into account wind speed too, which amplifies tree noise
|
||||
let wind_speed_multiplier = (client.weather_at_player().wind.magnitude_squared()
|
||||
/ 20.0_f32.powi(2))
|
||||
.min(1.0);
|
||||
|
||||
if tree_multiplier > 0.1 {
|
||||
tree_multiplier * (1.0 + wind_speed_multiplier)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
},
|
||||
AmbientChannelTag::Cave => {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let terrain_alt = if let Some(chunk) = client.current_chunk() {
|
||||
chunk.meta().alt()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// When the camera is roughly above ground, don't play cave sounds
|
||||
let camera_multiplier = (-(cam_pos.z - terrain_alt) / 100.0).max(0.0);
|
||||
|
||||
if client.current_site() == SiteKindMeta::Cave {
|
||||
camera_multiplier
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks various factors to determine the target volume to lerp to
|
||||
fn get_target_volume(
|
||||
tag: AmbientChannelTag,
|
||||
state: &State,
|
||||
client: &Client,
|
||||
camera: &Camera,
|
||||
) -> f32 {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let mut volume_multiplier: f32 = AmbientChannelTag::get_tag_volume(tag, client, camera);
|
||||
|
||||
let terrain_alt = if let Some(chunk) = client.current_chunk() {
|
||||
chunk.meta().alt()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
// Checks if the camera is underwater to diminish ambient sounds
|
||||
if state
|
||||
.terrain()
|
||||
.get((cam_pos).map(|e| e.floor() as i32))
|
||||
.map(|b| b.is_liquid())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
volume_multiplier *= 0.1;
|
||||
}
|
||||
|
||||
// Is the camera underneath the terrain? Fade out the lower it goes beneath.
|
||||
// Unless, of course, the player is in a cave.
|
||||
if tag != AmbientChannelTag::Cave {
|
||||
(volume_multiplier * ((cam_pos.z - terrain_alt) / 50.0 + 1.0).clamped(0.0, 1.0))
|
||||
.min(AmbientChannelTag::tag_max_volume(tag))
|
||||
} else {
|
||||
volume_multiplier.min(AmbientChannelTag::tag_max_volume(tag))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_ambience_items() -> AssetHandle<AmbientCollection> {
|
||||
AmbientCollection::load_or_insert_with("voxygen.audio.ambient", |error| {
|
||||
warn!(
|
||||
"Error reading ambience config file, ambience will not be available: {:#?}",
|
||||
error
|
||||
);
|
||||
AmbientCollection::default()
|
||||
})
|
||||
}
|
||||
|
||||
impl assets::Asset for AmbientCollection {
|
||||
type Loader = assets::RonLoader;
|
||||
|
||||
const EXTENSION: &'static str = "ron";
|
||||
}
|
@ -4,318 +4,438 @@
|
||||
//! sounds simultaneously. Each additional channel used decreases performance
|
||||
//! in-game, so the amount of channels utilized should be kept to a minimum.
|
||||
//!
|
||||
//! When constructing a new [`AudioFrontend`](../struct.AudioFrontend.html), two
|
||||
//! music channels are created internally (to achieve crossover fades) while the
|
||||
//! When constructing a new [`AudioFrontend`](../struct.AudioFrontend.html), the
|
||||
//! number of sfx channels are determined by the `num_sfx_channels` value
|
||||
//! defined in the client
|
||||
//! [`AudioSettings`](../../settings/struct.AudioSettings.html)
|
||||
//!
|
||||
//! When the AudioFrontend's
|
||||
//! [`emit_sfx`](../struct.AudioFrontend.html#method.emit_sfx)
|
||||
//! methods is called, it attempts to retrieve an SfxChannel for playback. If
|
||||
//! the channel capacity has been reached and all channels are occupied, a
|
||||
//! warning is logged, and no sound is played.
|
||||
|
||||
use crate::audio::{
|
||||
fader::{FadeDirection, Fader},
|
||||
Listener,
|
||||
use kira::{
|
||||
effect::filter::{FilterBuilder, FilterHandle},
|
||||
manager::AudioManager,
|
||||
sound::{static_sound::StaticSoundHandle, PlaybackState},
|
||||
spatial::emitter::EmitterHandle,
|
||||
track::{TrackBuilder, TrackHandle, TrackId, TrackRoutes},
|
||||
tween::{Easing, Tween, Value},
|
||||
StartTime, Volume,
|
||||
};
|
||||
use rodio::{cpal::FromSample, OutputStreamHandle, Sample, Sink, Source, SpatialSink};
|
||||
use serde::Deserialize;
|
||||
use std::time::Instant;
|
||||
use std::time::Duration;
|
||||
use strum::EnumIter;
|
||||
use tracing::warn;
|
||||
use vek::*;
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
enum ChannelState {
|
||||
Playing,
|
||||
Fading,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
/// Each `MusicChannel` has a `MusicChannelTag` which help us determine when we
|
||||
/// should transition between two types of in-game music. For example, we
|
||||
/// transition between `TitleMusic` and `Exploration` when a player enters the
|
||||
/// world by crossfading over a slow duration. In the future, transitions in the
|
||||
/// world such as `Exploration` -> `BossBattle` would transition more rapidly.
|
||||
#[derive(PartialEq, Clone, Copy, Hash, Eq, Deserialize)]
|
||||
#[derive(PartialEq, Clone, Copy, Hash, Eq, Deserialize, Debug)]
|
||||
pub enum MusicChannelTag {
|
||||
TitleMusic,
|
||||
Exploration,
|
||||
Combat,
|
||||
}
|
||||
|
||||
/// A MusicChannel uses a non-positional audio sink designed to play music which
|
||||
/// A MusicChannel is designed to play music which
|
||||
/// is always heard at the player's position.
|
||||
///
|
||||
/// See also: [`Rodio::Sink`](https://docs.rs/rodio/0.11.0/rodio/struct.Sink.html)
|
||||
pub struct MusicChannel {
|
||||
tag: MusicChannelTag,
|
||||
sink: Sink,
|
||||
state: ChannelState,
|
||||
fader: Fader,
|
||||
track: Option<TrackHandle>,
|
||||
source: Option<StaticSoundHandle>,
|
||||
length: f32,
|
||||
loop_data: (bool, LoopPoint, LoopPoint), // Loops?, Start, End
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum LoopPoint {
|
||||
Start,
|
||||
End,
|
||||
Point(f64),
|
||||
}
|
||||
|
||||
impl MusicChannel {
|
||||
pub fn new(stream: &OutputStreamHandle) -> Self {
|
||||
let new_sink = Sink::try_new(stream);
|
||||
match new_sink {
|
||||
Ok(sink) => Self {
|
||||
sink,
|
||||
pub fn new(manager: &mut AudioManager, parent_track: TrackId) -> Self {
|
||||
let new_track = manager.add_sub_track(
|
||||
TrackBuilder::new()
|
||||
.volume(Volume::Amplitude(0.0))
|
||||
.routes(TrackRoutes::parent(parent_track)),
|
||||
);
|
||||
match new_track {
|
||||
Ok(track) => Self {
|
||||
tag: MusicChannelTag::TitleMusic,
|
||||
state: ChannelState::Stopped,
|
||||
fader: Fader::default(),
|
||||
track: Some(track),
|
||||
source: None,
|
||||
length: 0.0,
|
||||
loop_data: (false, LoopPoint::Start, LoopPoint::End),
|
||||
},
|
||||
Err(_) => {
|
||||
warn!("Failed to create a rodio sink. May not play sounds.");
|
||||
warn!(?new_track, "Failed to create track. May not play music.");
|
||||
Self {
|
||||
sink: Sink::new_idle().0,
|
||||
tag: MusicChannelTag::TitleMusic,
|
||||
state: ChannelState::Stopped,
|
||||
fader: Fader::default(),
|
||||
track: None,
|
||||
source: None,
|
||||
length: 0.0,
|
||||
loop_data: (false, LoopPoint::Start, LoopPoint::End),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Play a music track item on this channel. If the channel has an existing
|
||||
/// track playing, the new sounds will be appended and played once they
|
||||
/// complete. Otherwise it will begin playing immediately.
|
||||
pub fn play<S>(&mut self, source: S, tag: MusicChannelTag)
|
||||
where
|
||||
S: Source + Send + 'static,
|
||||
S::Item: Sample,
|
||||
S::Item: Send,
|
||||
<S as Iterator>::Item: std::fmt::Debug,
|
||||
f32: FromSample<<S as Iterator>::Item>,
|
||||
{
|
||||
self.tag = tag;
|
||||
self.sink.append(source);
|
||||
pub fn set_tag(&mut self, tag: MusicChannelTag) { self.tag = tag; }
|
||||
|
||||
self.state = if !self.fader.is_finished() {
|
||||
ChannelState::Fading
|
||||
} else {
|
||||
ChannelState::Playing
|
||||
};
|
||||
pub fn set_source(&mut self, source_handle: Option<StaticSoundHandle>) {
|
||||
self.source = source_handle;
|
||||
}
|
||||
|
||||
/// Stop whatever is playing on a given music channel
|
||||
pub fn stop(&mut self, tag: MusicChannelTag) {
|
||||
self.tag = tag;
|
||||
self.sink.stop();
|
||||
}
|
||||
pub fn set_length(&mut self, length: f32) { self.length = length; }
|
||||
|
||||
/// Set the volume of the current channel. If the channel is currently
|
||||
/// fading, the volume of the fader is updated to this value.
|
||||
pub fn set_volume(&mut self, volume: f32) {
|
||||
if !self.fader.is_finished() {
|
||||
self.fader.update_target_volume(volume);
|
||||
} else {
|
||||
self.sink.set_volume(volume);
|
||||
}
|
||||
}
|
||||
// Gets the currently set loop data
|
||||
pub fn get_loop_data(&self) -> (bool, LoopPoint, LoopPoint) { self.loop_data }
|
||||
|
||||
/// Set a fader for the channel. If a fader exists already, it is replaced.
|
||||
/// If the channel has not begun playing, and the fader is set to fade in,
|
||||
/// we set the volume of the channel to the initial volume of the fader so
|
||||
/// that the volumes match when playing begins.
|
||||
pub fn set_fader(&mut self, fader: Fader) {
|
||||
self.fader = fader;
|
||||
self.state = ChannelState::Fading;
|
||||
|
||||
if self.state == ChannelState::Stopped && fader.direction() == FadeDirection::In {
|
||||
self.sink.set_volume(fader.get_volume());
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if either the channels sink reports itself as empty (no
|
||||
/// more sounds in the queue) or we have forcibly set the channels state to
|
||||
/// the 'Stopped' state
|
||||
pub fn is_done(&self) -> bool { self.sink.empty() || self.state == ChannelState::Stopped }
|
||||
|
||||
pub fn get_tag(&self) -> MusicChannelTag { self.tag }
|
||||
|
||||
/// Maintain the fader attached to this channel. If the channel is not
|
||||
/// fading, no action is taken.
|
||||
pub fn maintain(&mut self, dt: std::time::Duration) {
|
||||
if self.state == ChannelState::Fading {
|
||||
self.fader.update(dt);
|
||||
self.sink.set_volume(self.fader.get_volume());
|
||||
|
||||
if self.fader.is_finished() {
|
||||
match self.fader.direction() {
|
||||
FadeDirection::Out => {
|
||||
self.state = ChannelState::Stopped;
|
||||
self.sink.stop();
|
||||
/// Sets whether the sound loops, and the start and end points of the loop
|
||||
pub fn set_loop_data(&mut self, loops: bool, start: LoopPoint, end: LoopPoint) {
|
||||
if let Some(source) = self.source.as_mut() {
|
||||
self.loop_data = (loops, start, end);
|
||||
if loops {
|
||||
match (start, end) {
|
||||
(LoopPoint::Start, LoopPoint::End) => {
|
||||
source.set_loop_region(0.0..);
|
||||
},
|
||||
FadeDirection::In => {
|
||||
self.state = ChannelState::Playing;
|
||||
(LoopPoint::Start, LoopPoint::Point(end)) => {
|
||||
source.set_loop_region(..end);
|
||||
},
|
||||
(LoopPoint::Point(start), LoopPoint::End) => {
|
||||
source.set_loop_region(start..);
|
||||
},
|
||||
(LoopPoint::Point(start), LoopPoint::Point(end)) => {
|
||||
source.set_loop_region(start..end);
|
||||
},
|
||||
_ => {
|
||||
warn!("Invalid loop points given")
|
||||
},
|
||||
}
|
||||
} else {
|
||||
source.set_loop_region(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop whatever is playing on this channel with an optional fadeout and
|
||||
/// delay
|
||||
pub fn stop(&mut self, duration: Option<f32>, delay: Option<f32>) {
|
||||
if let Some(source) = self.source.as_mut() {
|
||||
let tween = Tween {
|
||||
duration: Duration::from_secs_f32(duration.unwrap_or(0.1)),
|
||||
start_time: StartTime::Delayed(Duration::from_secs_f32(delay.unwrap_or(0.0))),
|
||||
..Default::default()
|
||||
};
|
||||
source.stop(tween)
|
||||
};
|
||||
}
|
||||
|
||||
/// Set the volume of the current channel.
|
||||
pub fn set_volume(&mut self, volume: f32) {
|
||||
if let Some(track) = self.track.as_mut() {
|
||||
track.set_volume(Volume::Amplitude(volume as f64), Tween::default());
|
||||
// } else {
|
||||
// warn!("Music track not present; cannot set volume")
|
||||
}
|
||||
}
|
||||
|
||||
/// Fade to a given amplitude over a given duration, optionally after a
|
||||
/// delay
|
||||
pub fn fade_to(&mut self, volume: f32, duration: f32, delay: Option<f32>) {
|
||||
let mut start_time = StartTime::Immediate;
|
||||
if let Some(delay) = delay {
|
||||
start_time = StartTime::Delayed(Duration::from_secs_f32(delay))
|
||||
}
|
||||
let tween = Tween {
|
||||
start_time,
|
||||
duration: Duration::from_secs_f32(duration),
|
||||
easing: Easing::Linear,
|
||||
};
|
||||
if let Some(track) = self.track.as_mut() {
|
||||
track.set_volume(Volume::Amplitude(volume as f64), tween);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fade to silence over a given duration and stop, optionally after a delay
|
||||
/// Use fade_to() if this fade is temporary
|
||||
pub fn fade_out(&mut self, duration: f32, delay: Option<f32>) {
|
||||
self.stop(Some(duration), delay);
|
||||
}
|
||||
|
||||
/// Returns true if the sound has stopped playing (whether by fading out or
|
||||
/// by finishing)
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.source
|
||||
.as_ref()
|
||||
.map_or(true, |source| source.state() == PlaybackState::Stopped)
|
||||
}
|
||||
|
||||
pub fn get_tag(&self) -> MusicChannelTag { self.tag }
|
||||
|
||||
/// Get an immutable reference to the channel's track for purposes of
|
||||
/// setting the output destination of a sound
|
||||
pub fn get_track(&self) -> Option<&TrackHandle> { self.track.as_ref() }
|
||||
|
||||
/// Get a mutable reference to the channel's track
|
||||
pub fn get_track_mut(&mut self) -> Option<&mut TrackHandle> { self.track.as_mut() }
|
||||
|
||||
pub fn get_source(&mut self) -> Option<&mut StaticSoundHandle> { self.source.as_mut() }
|
||||
|
||||
pub fn get_length(&self) -> f32 { self.length }
|
||||
}
|
||||
|
||||
/// AmbientChannelTags are used for non-positional sfx. Currently the only use
|
||||
/// AmbienceChannelTags are used for non-positional sfx. Currently the only use
|
||||
/// is for wind.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, EnumIter)]
|
||||
pub enum AmbientChannelTag {
|
||||
pub enum AmbienceChannelTag {
|
||||
Wind,
|
||||
Rain,
|
||||
Thunder,
|
||||
ThunderRumbling,
|
||||
Leaves,
|
||||
Cave,
|
||||
Thunder,
|
||||
}
|
||||
|
||||
/// A AmbientChannel uses a non-positional audio sink designed to play sounds
|
||||
/// An AmbienceChannel uses a non-positional audio sink designed to play sounds
|
||||
/// which are always heard at the camera's position.
|
||||
pub struct AmbientChannel {
|
||||
tag: AmbientChannelTag,
|
||||
pub multiplier: f32,
|
||||
sink: Sink,
|
||||
pub began_playing: Instant,
|
||||
pub next_track_change: f32,
|
||||
#[derive(Debug)]
|
||||
pub struct AmbienceChannel {
|
||||
tag: AmbienceChannelTag,
|
||||
target_volume: f32,
|
||||
track: Option<TrackHandle>,
|
||||
filter: Option<FilterHandle>,
|
||||
source: Option<StaticSoundHandle>,
|
||||
pub looping: bool,
|
||||
}
|
||||
|
||||
impl AmbientChannel {
|
||||
pub fn new(stream: &OutputStreamHandle, tag: AmbientChannelTag, multiplier: f32) -> Self {
|
||||
let new_sink = Sink::try_new(stream);
|
||||
match new_sink {
|
||||
Ok(sink) => Self {
|
||||
impl AmbienceChannel {
|
||||
pub fn new(
|
||||
tag: AmbienceChannelTag,
|
||||
init_volume: f32,
|
||||
manager: &mut AudioManager,
|
||||
parent_track: TrackId,
|
||||
looping: bool,
|
||||
) -> Self {
|
||||
let ambience_filter_builder = FilterBuilder::new().cutoff(Value::Fixed(20000.0));
|
||||
let mut ambience_track_builder = TrackBuilder::new();
|
||||
let filter = ambience_track_builder.add_effect(ambience_filter_builder);
|
||||
let new_track = manager.add_sub_track(
|
||||
ambience_track_builder
|
||||
.volume(0.0)
|
||||
.routes(TrackRoutes::parent(parent_track)),
|
||||
);
|
||||
match new_track {
|
||||
Ok(track) => Self {
|
||||
tag,
|
||||
multiplier,
|
||||
sink,
|
||||
began_playing: Instant::now(),
|
||||
next_track_change: 0.0,
|
||||
target_volume: init_volume,
|
||||
track: Some(track),
|
||||
filter: Some(filter),
|
||||
source: None,
|
||||
looping,
|
||||
},
|
||||
Err(_) => {
|
||||
warn!("Failed to create rodio sink. May not play ambient sounds.");
|
||||
warn!(
|
||||
?new_track,
|
||||
"Failed to create track. May not play ambient sounds."
|
||||
);
|
||||
Self {
|
||||
tag,
|
||||
multiplier,
|
||||
sink: Sink::new_idle().0,
|
||||
began_playing: Instant::now(),
|
||||
next_track_change: 0.0,
|
||||
target_volume: init_volume,
|
||||
track: None,
|
||||
filter: None,
|
||||
source: None,
|
||||
looping,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play<S>(&mut self, source: S)
|
||||
where
|
||||
S: Source + Send + 'static,
|
||||
S::Item: Sample,
|
||||
S::Item: Send,
|
||||
<S as Iterator>::Item: std::fmt::Debug,
|
||||
f32: FromSample<<S as Iterator>::Item>,
|
||||
{
|
||||
self.sink.append(source);
|
||||
pub fn set_source(&mut self, source_handle: Option<StaticSoundHandle>) {
|
||||
self.source = source_handle;
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) { self.sink.stop(); }
|
||||
/// Stop whatever is playing on this channel with an optional fadeout and
|
||||
/// delay
|
||||
pub fn stop(&mut self, duration: Option<f32>, delay: Option<f32>) {
|
||||
if let Some(source) = self.source.as_mut() {
|
||||
let tween = Tween {
|
||||
duration: Duration::from_secs_f32(duration.unwrap_or(0.1)),
|
||||
start_time: StartTime::Delayed(Duration::from_secs_f32(delay.unwrap_or(0.0))),
|
||||
..Default::default()
|
||||
};
|
||||
source.stop(tween)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_volume(&mut self, volume: f32) { self.sink.set_volume(volume * self.multiplier); }
|
||||
/// Set the channel to a volume, fading over a given duration
|
||||
pub fn fade_to(&mut self, volume: f32, duration: f32) {
|
||||
if let Some(track) = self.track.as_mut() {
|
||||
track.set_volume(Volume::Amplitude(volume as f64), Tween {
|
||||
start_time: StartTime::Immediate,
|
||||
duration: Duration::from_secs_f32(duration),
|
||||
easing: Easing::Linear,
|
||||
});
|
||||
self.target_volume = volume;
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn get_volume(&mut self) -> f32 { self.sink.volume() }
|
||||
/// Set the cutoff for the lowpass filter on this channel
|
||||
pub fn set_filter(&mut self, frequency: u32) {
|
||||
if let Some(filter) = self.filter.as_mut() {
|
||||
filter.set_cutoff(Value::Fixed(frequency as f64), Tween::default());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_tag(&self) -> AmbientChannelTag { self.tag }
|
||||
/// Set whether this channel's sound loops or not
|
||||
pub fn set_looping(&mut self, loops: bool) {
|
||||
if let Some(source) = self.source.as_mut() {
|
||||
if loops {
|
||||
source.set_loop_region(0.0..);
|
||||
} else {
|
||||
source.set_loop_region(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn set_tag(&mut self, tag: AmbientChannelTag) { self.tag = tag }
|
||||
pub fn get_source(&mut self) -> Option<&mut StaticSoundHandle> { self.source.as_mut() }
|
||||
|
||||
/// Get an immutable reference to the channel's track for purposes of
|
||||
/// setting the output destination of a sound
|
||||
pub fn get_track(&self) -> Option<&TrackHandle> { self.track.as_ref() }
|
||||
|
||||
/// Get a mutable reference to the channel's track
|
||||
pub fn get_track_mut(&mut self) -> Option<&mut TrackHandle> { self.track.as_mut() }
|
||||
|
||||
/// Get the volume of this channel. The volume may be in the process of
|
||||
/// being faded to.
|
||||
pub fn get_target_volume(&self) -> f32 { self.target_volume }
|
||||
|
||||
pub fn get_tag(&self) -> AmbienceChannelTag { self.tag }
|
||||
|
||||
pub fn set_tag(&mut self, tag: AmbienceChannelTag) { self.tag = tag }
|
||||
|
||||
pub fn is_active(&self) -> bool { self.get_target_volume() == 0.0 }
|
||||
|
||||
pub fn is_stopped(&self) -> bool {
|
||||
if let Some(source) = self.source.as_ref() {
|
||||
source.state() == PlaybackState::Stopped
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An SfxChannel uses a positional audio sink, and is designed for short-lived
|
||||
/// audio which can be spatially controlled, but does not need control over
|
||||
/// playback or fading/transitions
|
||||
///
|
||||
/// See also: [`Rodio::SpatialSink`](https://docs.rs/rodio/0.11.0/rodio/struct.SpatialSink.html)
|
||||
/// Note: currently, emitters are static once spawned
|
||||
#[derive(Debug)]
|
||||
pub struct SfxChannel {
|
||||
sink: SpatialSink,
|
||||
source: Option<StaticSoundHandle>,
|
||||
emitter: Option<EmitterHandle>,
|
||||
pub pos: Vec3<f32>,
|
||||
}
|
||||
|
||||
impl SfxChannel {
|
||||
pub fn new(stream: &OutputStreamHandle) -> Self {
|
||||
pub fn new(emitter: Option<EmitterHandle>) -> Self {
|
||||
Self {
|
||||
sink: SpatialSink::try_new(stream, [0.0; 3], [1.0, 0.0, 0.0], [-1.0, 0.0, 0.0])
|
||||
.unwrap(),
|
||||
source: None,
|
||||
emitter,
|
||||
pos: Vec3::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play<S>(&mut self, source: S)
|
||||
where
|
||||
S: Source + Send + 'static,
|
||||
S::Item: Sample,
|
||||
S::Item: Send,
|
||||
<S as Iterator>::Item: std::fmt::Debug,
|
||||
f32: FromSample<<S as Iterator>::Item>,
|
||||
{
|
||||
self.sink.append(source);
|
||||
pub fn set_source(&mut self, source_handle: Option<StaticSoundHandle>) {
|
||||
self.source = source_handle;
|
||||
}
|
||||
|
||||
/// Same as SfxChannel::play but with the source passed through
|
||||
/// a low pass filter at 300 Hz
|
||||
pub fn play_with_low_pass_filter<S>(&mut self, source: S, freq: u32)
|
||||
where
|
||||
S: Sized + Send + 'static,
|
||||
S: Source<Item = f32>,
|
||||
{
|
||||
let source = source.low_pass(freq);
|
||||
self.sink.append(source);
|
||||
pub fn stop(&mut self) {
|
||||
if let Some(source) = self.source.as_mut() {
|
||||
source.stop(Tween::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_volume(&mut self, volume: f32) { self.sink.set_volume(volume); }
|
||||
pub fn set_volume(&mut self, volume: f32) {
|
||||
if let Some(source) = self.source.as_mut() {
|
||||
source.set_volume(Volume::Amplitude(volume as f64), Tween::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) { self.sink.stop(); }
|
||||
|
||||
pub fn is_done(&self) -> bool { self.sink.empty() }
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.source
|
||||
.as_ref()
|
||||
.map_or(true, |source| source.state() == PlaybackState::Stopped)
|
||||
}
|
||||
|
||||
pub fn set_pos(&mut self, pos: Vec3<f32>) { self.pos = pos; }
|
||||
|
||||
pub fn update(&mut self, listener: &Listener) {
|
||||
const FALLOFF: f32 = 0.13;
|
||||
pub fn update(&mut self, pos: Vec3<f32>) {
|
||||
let tween = Tween {
|
||||
duration: Duration::from_secs_f32(0.01),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.sink
|
||||
.set_emitter_position(((self.pos - listener.pos) * FALLOFF).into_array());
|
||||
self.sink
|
||||
.set_left_ear_position(listener.ear_left_rpos.into_array());
|
||||
self.sink
|
||||
.set_right_ear_position(listener.ear_right_rpos.into_array());
|
||||
if let Some(emitter) = self.emitter.as_mut() {
|
||||
emitter.set_position(pos, tween);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An UiChannel uses a non-spatial audio sink, and is designed for short-lived
|
||||
/// audio which is not spatially controlled, but does not need control over
|
||||
/// playback or fading/transitions
|
||||
///
|
||||
/// See also: [`Rodio::Sink`](https://docs.rs/rodio/0.11.0/rodio/struct.Sink.html)
|
||||
pub struct UiChannel {
|
||||
sink: Sink,
|
||||
track: Option<TrackHandle>,
|
||||
source: Option<StaticSoundHandle>,
|
||||
}
|
||||
|
||||
impl UiChannel {
|
||||
pub fn new(stream: &OutputStreamHandle) -> Self {
|
||||
Self {
|
||||
sink: Sink::try_new(stream).unwrap(),
|
||||
pub fn new(manager: &mut AudioManager, parent_track: TrackId) -> Self {
|
||||
let new_track = manager
|
||||
.add_sub_track(TrackBuilder::default().routes(TrackRoutes::parent(parent_track)));
|
||||
match new_track {
|
||||
Ok(track) => Self {
|
||||
track: Some(track),
|
||||
source: None,
|
||||
},
|
||||
Err(_) => {
|
||||
warn!(
|
||||
?new_track,
|
||||
"Failed to create track. May not play UI sounds."
|
||||
);
|
||||
Self {
|
||||
track: None,
|
||||
source: None,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play<S>(&mut self, source: S)
|
||||
where
|
||||
S: Source + Send + 'static,
|
||||
S::Item: Sample,
|
||||
S::Item: Send,
|
||||
<S as Iterator>::Item: std::fmt::Debug,
|
||||
f32: FromSample<<S as Iterator>::Item>,
|
||||
{
|
||||
self.sink.append(source);
|
||||
pub fn set_source(&mut self, source_handle: Option<StaticSoundHandle>) {
|
||||
self.source = source_handle;
|
||||
}
|
||||
|
||||
pub fn set_volume(&mut self, volume: f32) { self.sink.set_volume(volume); }
|
||||
pub fn stop(&mut self) {
|
||||
if let Some(source) = self.source.as_mut() {
|
||||
source.stop(Tween::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) { self.sink.stop(); }
|
||||
pub fn set_volume(&mut self, volume: f32) {
|
||||
if let Some(track) = self.track.as_mut() {
|
||||
track.set_volume(Volume::Amplitude(volume as f64), Tween::default())
|
||||
// } else {
|
||||
// warn!("UI track not present; cannot set volume")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_done(&self) -> bool { self.sink.empty() }
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.source
|
||||
.as_ref()
|
||||
.map_or(true, |source| source.state() == PlaybackState::Stopped)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -53,10 +53,10 @@ use common::{
|
||||
};
|
||||
use common_state::State;
|
||||
use hashbrown::HashMap;
|
||||
use rand::{prelude::SliceRandom, thread_rng, Rng};
|
||||
use kira::clock::ClockTime;
|
||||
use rand::{prelude::SliceRandom, rngs::ThreadRng, thread_rng, Rng};
|
||||
use serde::Deserialize;
|
||||
use std::time::Instant;
|
||||
use tracing::{debug, trace};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
/// Collection of all the tracks
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -78,6 +78,7 @@ pub struct SoundtrackItem {
|
||||
path: String,
|
||||
/// Length of the track in seconds
|
||||
length: f32,
|
||||
loop_points: Option<(f32, f32)>,
|
||||
/// Whether this track should play during day or night
|
||||
timing: Option<DayPeriod>,
|
||||
/// Whether this track should play during a certain weather
|
||||
@ -108,6 +109,7 @@ enum RawSoundtrackItem {
|
||||
biomes: Vec<(BiomeKind, u8)>,
|
||||
sites: Vec<SiteKindMeta>,
|
||||
segments: Vec<(String, f32, MusicState, Option<MusicActivity>)>,
|
||||
loop_points: (f32, f32),
|
||||
artist: (String, Option<String>),
|
||||
},
|
||||
}
|
||||
@ -144,21 +146,26 @@ pub struct MusicMgr {
|
||||
/// Collection of all the tracks
|
||||
soundtrack: SoundtrackCollection<SoundtrackItem>,
|
||||
/// Instant at which the current track began playing
|
||||
began_playing: Instant,
|
||||
/// Time until the next track should be played
|
||||
next_track_change: f32,
|
||||
began_playing: Option<ClockTime>,
|
||||
/// Instant at which the current track should stop
|
||||
song_end: Option<ClockTime>,
|
||||
/// Time until the next track should be played after a track ends
|
||||
gap_length: f32,
|
||||
/// Time remaining for gap
|
||||
gap_time: f64,
|
||||
/// The title of the last track played. Used to prevent a track
|
||||
/// being played twice in a row
|
||||
last_track: String,
|
||||
last_combat_track: String,
|
||||
/// Time of the last interrupt (to avoid rapid switching)
|
||||
last_interrupt: Instant,
|
||||
last_interrupt_attempt: Option<ClockTime>,
|
||||
/// The previous track's activity kind, for transitions
|
||||
last_activity: MusicState,
|
||||
// For debug menu
|
||||
current_track: String,
|
||||
current_artist: String,
|
||||
track_length: f32,
|
||||
loop_points: Option<(f32, f32)>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -197,42 +204,35 @@ impl assets::Asset for MusicTransitionManifest {
|
||||
const EXTENSION: &'static str = "ron";
|
||||
}
|
||||
|
||||
fn time_f64(clock_time: ClockTime) -> f64 { clock_time.ticks as f64 + clock_time.fraction }
|
||||
|
||||
impl MusicMgr {
|
||||
pub fn new(calendar: &Calendar) -> Self {
|
||||
Self {
|
||||
soundtrack: Self::load_soundtrack_items(calendar),
|
||||
began_playing: Instant::now(),
|
||||
next_track_change: 0.0,
|
||||
began_playing: None,
|
||||
song_end: None,
|
||||
gap_length: 0.0,
|
||||
gap_time: -1.0,
|
||||
last_track: String::from("None"),
|
||||
last_combat_track: String::from("None"),
|
||||
last_interrupt: Instant::now(),
|
||||
last_interrupt_attempt: None,
|
||||
last_activity: MusicState::Activity(MusicActivity::Explore),
|
||||
current_track: String::from("None"),
|
||||
current_artist: String::from("None"),
|
||||
track_length: 0.0,
|
||||
loop_points: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether the previous track has completed. If so, sends a
|
||||
/// request to play the next (random) track
|
||||
pub fn maintain(&mut self, audio: &mut AudioFrontend, state: &State, client: &Client) {
|
||||
//if let Some(current_chunk) = client.current_chunk() {
|
||||
//println!("biome: {:?}", current_chunk.meta().biome());
|
||||
//println!("chaos: {}", current_chunk.meta().chaos());
|
||||
//println!("alt: {}", current_chunk.meta().alt());
|
||||
//println!("tree_density: {}",
|
||||
// current_chunk.meta().tree_density());
|
||||
// let current_site = client.current_site();
|
||||
// println!("{:?}", current_site);
|
||||
//if let Some(position) = client.current::<comp::Pos>() {
|
||||
// player_alt = position.0.z;
|
||||
//}
|
||||
|
||||
use common::comp::{group::ENEMY, Group, Health, Pos};
|
||||
use specs::{Join, WorldExt};
|
||||
// Checks if the music volume is set to zero or audio is disabled
|
||||
// This prevents us from running all the following code unnecessarily
|
||||
if !audio.music_enabled() {
|
||||
|
||||
if !audio.music_enabled() || audio.get_clock().is_none() || audio.get_clock_time().is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@ -245,6 +245,7 @@ impl MusicMgr {
|
||||
let healths = ecs.read_component::<Health>();
|
||||
let groups = ecs.read_component::<Group>();
|
||||
let mtm = audio.mtm.read();
|
||||
let mut rng = thread_rng();
|
||||
|
||||
if audio.combat_music_enabled {
|
||||
if let Some(player_pos) = positions.get(player) {
|
||||
@ -281,7 +282,7 @@ impl MusicMgr {
|
||||
}
|
||||
}
|
||||
|
||||
let music_state = match self.last_activity {
|
||||
let mut music_state = match self.last_activity {
|
||||
MusicState::Activity(prev) => {
|
||||
if prev != activity_state {
|
||||
MusicState::Transition(prev, activity_state)
|
||||
@ -289,35 +290,127 @@ impl MusicMgr {
|
||||
MusicState::Activity(activity_state)
|
||||
}
|
||||
},
|
||||
MusicState::Transition(_, next) => MusicState::Activity(next),
|
||||
MusicState::Transition(_, next) => {
|
||||
warn!("Transitioning: {:?}", self.last_activity);
|
||||
MusicState::Activity(next)
|
||||
},
|
||||
};
|
||||
|
||||
let now = audio.get_clock_time().unwrap();
|
||||
|
||||
let began_playing = *self.began_playing.get_or_insert(now);
|
||||
let last_interrupt_attempt = *self.last_interrupt_attempt.get_or_insert(now);
|
||||
let song_end = *self.song_end.get_or_insert(now);
|
||||
let mut time_since_began_playing = time_f64(now) - time_f64(began_playing);
|
||||
|
||||
// TODO: Instead of a constant tick, make this a timer that starts only when
|
||||
// combat might end, providing a proper "buffer".
|
||||
// interrupt_delay dictates the time between attempted interrupts
|
||||
let interrupt = matches!(music_state, MusicState::Transition(_, _))
|
||||
&& self.last_interrupt.elapsed().as_secs_f32() > mtm.interrupt_delay;
|
||||
&& time_f64(now) - time_f64(last_interrupt_attempt) > mtm.interrupt_delay as f64;
|
||||
|
||||
// When the current track ends, clear the debug values
|
||||
if self.began_playing.elapsed().as_secs_f32() > self.track_length {
|
||||
self.current_track = String::from("None");
|
||||
self.current_artist = String::from("None");
|
||||
// Hack to end combat music since there is currently nothing that detects
|
||||
// transitions away
|
||||
if matches!(
|
||||
music_state,
|
||||
MusicState::Transition(
|
||||
MusicActivity::Combat(CombatIntensity::High),
|
||||
MusicActivity::Explore
|
||||
)
|
||||
) {
|
||||
music_state = MusicState::Activity(MusicActivity::Explore)
|
||||
}
|
||||
|
||||
if audio.music_enabled()
|
||||
&& !self.soundtrack.tracks.is_empty()
|
||||
&& (self.began_playing.elapsed().as_secs_f32() > self.next_track_change || interrupt)
|
||||
&& (time_since_began_playing
|
||||
> time_f64(song_end) - time_f64(began_playing) // Amount of time between when the song ends and when it began playing
|
||||
|| interrupt)
|
||||
{
|
||||
time_since_began_playing = time_f64(now) - time_f64(began_playing);
|
||||
if time_since_began_playing > self.track_length as f64
|
||||
&& self.last_activity
|
||||
!= MusicState::Activity(MusicActivity::Combat(CombatIntensity::High))
|
||||
{
|
||||
self.current_track = String::from("None");
|
||||
self.current_artist = String::from("None");
|
||||
}
|
||||
|
||||
if interrupt {
|
||||
self.last_interrupt = Instant::now();
|
||||
self.last_interrupt_attempt = Some(now);
|
||||
if let Ok(next_activity) =
|
||||
self.play_random_track(audio, state, client, &music_state, &mut rng)
|
||||
{
|
||||
trace!(
|
||||
"pre-play_random_track: {:?} {:?}",
|
||||
self.last_activity, music_state
|
||||
);
|
||||
self.last_activity = next_activity;
|
||||
}
|
||||
} else if music_state == MusicState::Activity(MusicActivity::Explore)
|
||||
|| music_state
|
||||
== MusicState::Transition(
|
||||
MusicActivity::Explore,
|
||||
MusicActivity::Combat(CombatIntensity::High),
|
||||
)
|
||||
{
|
||||
// If current state is Explore, insert a gap now.
|
||||
if self.gap_time == 0.0 {
|
||||
self.gap_length = self.generate_silence_between_tracks(
|
||||
audio.music_spacing,
|
||||
client,
|
||||
&music_state,
|
||||
&mut rng,
|
||||
);
|
||||
self.gap_time = self.gap_length as f64;
|
||||
self.song_end = audio.get_clock_time();
|
||||
} else if self.gap_time < 0.0 {
|
||||
// Gap time is up, play a track
|
||||
// Hack to make combat situations not cancel explore music
|
||||
if music_state
|
||||
== MusicState::Transition(
|
||||
MusicActivity::Explore,
|
||||
MusicActivity::Combat(CombatIntensity::High),
|
||||
)
|
||||
{
|
||||
music_state = MusicState::Activity(MusicActivity::Explore)
|
||||
}
|
||||
if let Ok(next_activity) =
|
||||
self.play_random_track(audio, state, client, &music_state, &mut rng)
|
||||
{
|
||||
self.last_activity = next_activity;
|
||||
self.gap_time = 0.0;
|
||||
self.gap_length = 0.0;
|
||||
}
|
||||
}
|
||||
} else if music_state
|
||||
== MusicState::Activity(MusicActivity::Combat(CombatIntensity::High))
|
||||
{
|
||||
// Keep playing! The track should loop automatically.
|
||||
self.began_playing = Some(now);
|
||||
self.song_end = Some(ClockTime::from_ticks_f64(
|
||||
audio.get_clock().unwrap().id(),
|
||||
time_f64(now) + self.loop_points.unwrap_or((0.0, 0.0)).1 as f64
|
||||
- self.loop_points.unwrap_or((0.0, 0.0)).0 as f64,
|
||||
));
|
||||
} else {
|
||||
trace!(
|
||||
"pre-play_random_track: {:?} {:?}",
|
||||
self.last_activity, music_state
|
||||
);
|
||||
}
|
||||
trace!(
|
||||
"pre-play_random_track: {:?} {:?}",
|
||||
self.last_activity, music_state
|
||||
);
|
||||
if let Ok(next_activity) = self.play_random_track(audio, state, client, &music_state) {
|
||||
self.last_activity = next_activity;
|
||||
} else {
|
||||
if self.began_playing.is_none() {
|
||||
self.began_playing = Some(now)
|
||||
}
|
||||
if self.soundtrack.tracks.is_empty() {
|
||||
warn!("No tracks available to play")
|
||||
}
|
||||
}
|
||||
|
||||
if time_since_began_playing > self.track_length as f64 {
|
||||
// Time remaining = Max time - (current time - time song ended)
|
||||
self.gap_time = (self.gap_length as f64) - (time_f64(now) - time_f64(song_end));
|
||||
}
|
||||
}
|
||||
|
||||
@ -327,41 +420,8 @@ impl MusicMgr {
|
||||
state: &State,
|
||||
client: &Client,
|
||||
music_state: &MusicState,
|
||||
) -> Result<MusicState, ()> {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// Adds a bit of randomness between plays, depending on whether the player is in
|
||||
// a town, or exploring.
|
||||
// TODO: make this something that is decided when a song ends, instead of when
|
||||
// it begins
|
||||
let spacing_multiplier = audio.music_spacing;
|
||||
let mut silence_between_tracks_seconds: f32 = 0.0;
|
||||
if spacing_multiplier > f32::EPSILON {
|
||||
silence_between_tracks_seconds =
|
||||
if matches!(music_state, MusicState::Activity(MusicActivity::Explore))
|
||||
&& matches!(client.current_site(), SiteKindMeta::Settlement(_))
|
||||
{
|
||||
rng.gen_range(120.0 * spacing_multiplier..180.0 * spacing_multiplier)
|
||||
} else if matches!(music_state, MusicState::Activity(MusicActivity::Explore))
|
||||
&& matches!(client.current_site(), SiteKindMeta::Dungeon(_))
|
||||
{
|
||||
rng.gen_range(10.0 * spacing_multiplier..20.0 * spacing_multiplier)
|
||||
} else if matches!(music_state, MusicState::Activity(MusicActivity::Explore))
|
||||
&& matches!(client.current_site(), SiteKindMeta::Cave)
|
||||
{
|
||||
rng.gen_range(20.0 * spacing_multiplier..40.0 * spacing_multiplier)
|
||||
} else if matches!(music_state, MusicState::Activity(MusicActivity::Explore)) {
|
||||
rng.gen_range(120.0 * spacing_multiplier..240.0 * spacing_multiplier)
|
||||
} else if matches!(
|
||||
music_state,
|
||||
MusicState::Activity(MusicActivity::Combat(_)) | MusicState::Transition(_, _)
|
||||
) {
|
||||
0.0
|
||||
} else {
|
||||
rng.gen_range(30.0 * spacing_multiplier..60.0 * spacing_multiplier)
|
||||
};
|
||||
}
|
||||
|
||||
rng: &mut ThreadRng,
|
||||
) -> Result<MusicState, String> {
|
||||
let is_dark = state.get_day_period().is_dark();
|
||||
let current_period_of_day = Self::get_current_day_period(is_dark);
|
||||
let current_weather = client.weather_at_player();
|
||||
@ -394,7 +454,15 @@ impl MusicMgr {
|
||||
.filter(|track| &track.music_state == music_state)
|
||||
.collect::<Vec<&SoundtrackItem>>();
|
||||
if maybe_tracks.is_empty() {
|
||||
return Err(());
|
||||
let error_string = format!(
|
||||
"No tracks for {:?}, {:?}, {:?}, {:?}, {:?}",
|
||||
¤t_period_of_day,
|
||||
¤t_weather,
|
||||
¤t_site,
|
||||
¤t_biome,
|
||||
&music_state
|
||||
);
|
||||
return Err(error_string);
|
||||
}
|
||||
// Second, prevent playing the last track (when not in combat, because then it
|
||||
// needs to loop)
|
||||
@ -428,7 +496,7 @@ impl MusicMgr {
|
||||
|
||||
// Randomly selects a track from the remaining tracks weighted based
|
||||
// on the biome
|
||||
let new_maybe_track = maybe_tracks.choose_weighted(&mut rng, |track| {
|
||||
let new_maybe_track = maybe_tracks.choose_weighted(rng, |track| {
|
||||
// If no biome is listed, the song is still added to the
|
||||
// rotation to allow for site specific songs to play
|
||||
// in any biome
|
||||
@ -444,11 +512,16 @@ impl MusicMgr {
|
||||
);
|
||||
|
||||
if let Ok(track) = new_maybe_track {
|
||||
let now = audio.get_clock_time().unwrap();
|
||||
// println!("Now playing {:?}", track.title);
|
||||
self.last_track = String::from(&track.title);
|
||||
self.began_playing = Instant::now();
|
||||
self.began_playing = Some(now);
|
||||
self.song_end = Some(ClockTime::from_ticks_f64(
|
||||
audio.get_clock().unwrap().id(),
|
||||
time_f64(now) + track.length as f64,
|
||||
));
|
||||
self.track_length = track.length;
|
||||
self.next_track_change = track.length + silence_between_tracks_seconds;
|
||||
self.gap_length = 0.0;
|
||||
if audio.music_enabled() {
|
||||
self.current_track = String::from(&track.title);
|
||||
self.current_artist = String::from(&track.artist.0);
|
||||
@ -463,7 +536,17 @@ impl MusicMgr {
|
||||
self.last_combat_track = String::from(&track.title);
|
||||
MusicChannelTag::Combat
|
||||
};
|
||||
audio.play_music(&track.path, tag);
|
||||
audio.play_music(&track.path, tag, track.length);
|
||||
if tag == MusicChannelTag::Combat {
|
||||
audio.set_loop_points(
|
||||
tag,
|
||||
track.loop_points.unwrap_or((0.0, 0.0)).0,
|
||||
track.loop_points.unwrap_or((0.0, 0.0)).1,
|
||||
);
|
||||
self.loop_points = track.loop_points
|
||||
} else {
|
||||
self.loop_points = None
|
||||
};
|
||||
|
||||
if let Some(state) = track.activity_override {
|
||||
Ok(MusicState::Activity(state))
|
||||
@ -471,10 +554,71 @@ impl MusicMgr {
|
||||
Ok(*music_state)
|
||||
}
|
||||
} else {
|
||||
Err(())
|
||||
Err(format!("{:?}", new_maybe_track))
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_silence_between_tracks(
|
||||
&self,
|
||||
spacing_multiplier: f32,
|
||||
client: &Client,
|
||||
music_state: &MusicState,
|
||||
rng: &mut ThreadRng,
|
||||
) -> f32 {
|
||||
let mut silence_between_tracks_seconds: f32 = 0.0;
|
||||
if spacing_multiplier > f32::EPSILON {
|
||||
silence_between_tracks_seconds =
|
||||
if matches!(
|
||||
music_state,
|
||||
MusicState::Activity(MusicActivity::Explore)
|
||||
| MusicState::Transition(
|
||||
MusicActivity::Explore,
|
||||
MusicActivity::Combat(CombatIntensity::High)
|
||||
)
|
||||
) && matches!(client.current_site(), SiteKindMeta::Settlement(_))
|
||||
{
|
||||
rng.gen_range(120.0 * spacing_multiplier..180.0 * spacing_multiplier)
|
||||
} else if matches!(
|
||||
music_state,
|
||||
MusicState::Activity(MusicActivity::Explore)
|
||||
| MusicState::Transition(
|
||||
MusicActivity::Explore,
|
||||
MusicActivity::Combat(CombatIntensity::High)
|
||||
)
|
||||
) && matches!(client.current_site(), SiteKindMeta::Dungeon(_))
|
||||
{
|
||||
rng.gen_range(10.0 * spacing_multiplier..20.0 * spacing_multiplier)
|
||||
} else if matches!(
|
||||
music_state,
|
||||
MusicState::Activity(MusicActivity::Explore)
|
||||
| MusicState::Transition(
|
||||
MusicActivity::Explore,
|
||||
MusicActivity::Combat(CombatIntensity::High)
|
||||
)
|
||||
) && matches!(client.current_site(), SiteKindMeta::Cave)
|
||||
{
|
||||
rng.gen_range(20.0 * spacing_multiplier..40.0 * spacing_multiplier)
|
||||
} else if matches!(
|
||||
music_state,
|
||||
MusicState::Activity(MusicActivity::Explore)
|
||||
| MusicState::Transition(
|
||||
MusicActivity::Explore,
|
||||
MusicActivity::Combat(CombatIntensity::High)
|
||||
)
|
||||
) {
|
||||
rng.gen_range(120.0 * spacing_multiplier..240.0 * spacing_multiplier)
|
||||
} else if matches!(
|
||||
music_state,
|
||||
MusicState::Activity(MusicActivity::Combat(_)) | MusicState::Transition(_, _)
|
||||
) {
|
||||
0.0
|
||||
} else {
|
||||
rng.gen_range(30.0 * spacing_multiplier..60.0 * spacing_multiplier)
|
||||
};
|
||||
}
|
||||
silence_between_tracks_seconds
|
||||
}
|
||||
|
||||
fn get_current_day_period(is_dark: bool) -> DayPeriod {
|
||||
if is_dark {
|
||||
DayPeriod::Night
|
||||
@ -488,8 +632,6 @@ impl MusicMgr {
|
||||
pub fn current_artist(&self) -> String { self.current_artist.clone() }
|
||||
|
||||
pub fn reset_track(&mut self) {
|
||||
self.began_playing = Instant::now();
|
||||
self.next_track_change = 0.0;
|
||||
self.current_artist = String::from("None");
|
||||
self.current_track = String::from("None");
|
||||
}
|
||||
@ -581,6 +723,7 @@ impl assets::Compound for SoundtrackCollection<SoundtrackItem> {
|
||||
biomes,
|
||||
sites,
|
||||
segments,
|
||||
loop_points,
|
||||
artist,
|
||||
} => {
|
||||
for (path, length, music_state, activity_override) in segments.into_iter() {
|
||||
@ -588,6 +731,7 @@ impl assets::Compound for SoundtrackCollection<SoundtrackItem> {
|
||||
title: title.clone(),
|
||||
path,
|
||||
length,
|
||||
loop_points: Some(loop_points),
|
||||
timing: timing.clone(),
|
||||
weather,
|
||||
biomes: biomes.clone(),
|
||||
|
@ -8,12 +8,7 @@ use crate::{
|
||||
|
||||
use super::EventMapper;
|
||||
use client::Client;
|
||||
use common::{
|
||||
comp::Pos,
|
||||
spiral::Spiral2d,
|
||||
terrain::TerrainChunk,
|
||||
vol::{ReadVol, RectRasterableVol},
|
||||
};
|
||||
use common::{comp::Pos, spiral::Spiral2d, terrain::TerrainChunk, vol::RectRasterableVol};
|
||||
use common_state::State;
|
||||
use hashbrown::HashMap;
|
||||
use rand::{prelude::*, seq::SliceRandom, thread_rng, Rng};
|
||||
@ -53,9 +48,6 @@ impl EventMapper for BlockEventMapper {
|
||||
terrain: &Terrain<TerrainChunk>,
|
||||
client: &Client,
|
||||
) {
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let mut rng = ChaCha8Rng::from_seed(thread_rng().gen());
|
||||
|
||||
// Get the player position and chunk
|
||||
@ -229,6 +221,9 @@ impl EventMapper for BlockEventMapper {
|
||||
let block_pos: Vec3<i32> = absolute_pos + block;
|
||||
let internal_state = self.history.entry(block_pos).or_default();
|
||||
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let block_pos = block_pos.map(|x| x as f32);
|
||||
|
||||
if Self::should_emit(
|
||||
@ -238,29 +233,12 @@ impl EventMapper for BlockEventMapper {
|
||||
) {
|
||||
// If the camera is within SFX distance
|
||||
if (block_pos.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR {
|
||||
let underwater = state
|
||||
.terrain()
|
||||
.get(cam_pos.map(|e| e.floor() as i32))
|
||||
.map(|b| b.is_liquid())
|
||||
.unwrap_or(false);
|
||||
|
||||
let sfx_trigger_item = triggers.get_key_value(&sounds.sfx);
|
||||
if sounds.sfx == SfxEvent::RunningWaterFast {
|
||||
audio.emit_filtered_sfx(
|
||||
sfx_trigger_item,
|
||||
block_pos,
|
||||
Some(sounds.volume),
|
||||
Some(8000),
|
||||
underwater,
|
||||
);
|
||||
} else {
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
block_pos,
|
||||
Some(sounds.volume),
|
||||
underwater,
|
||||
);
|
||||
}
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
block_pos,
|
||||
Some(sounds.volume),
|
||||
);
|
||||
}
|
||||
internal_state.time = Instant::now();
|
||||
internal_state.event = sounds.sfx.clone();
|
||||
|
@ -11,7 +11,6 @@ use client::Client;
|
||||
use common::{
|
||||
comp::{object, Body, Pos},
|
||||
terrain::TerrainChunk,
|
||||
vol::ReadVol,
|
||||
};
|
||||
use common_state::State;
|
||||
use hashbrown::HashMap;
|
||||
@ -49,8 +48,9 @@ impl EventMapper for CampfireEventMapper {
|
||||
_client: &Client,
|
||||
) {
|
||||
let ecs = state.ecs();
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
|
||||
let cam_pos = camera.get_pos_with_focus();
|
||||
|
||||
for (entity, body, pos) in (
|
||||
&ecs.entities(),
|
||||
&ecs.read_storage::<Body>(),
|
||||
@ -66,14 +66,9 @@ impl EventMapper for CampfireEventMapper {
|
||||
|
||||
// Check for SFX config entry for this movement
|
||||
if Self::should_emit(internal_state, triggers.get_key_value(&mapped_event)) {
|
||||
let underwater = state
|
||||
.terrain()
|
||||
.get(cam_pos.map(|e| e.floor() as i32))
|
||||
.map(|b| b.is_liquid())
|
||||
.unwrap_or(false);
|
||||
let sfx_trigger_item = triggers.get_key_value(&mapped_event);
|
||||
const CAMPFIRE_VOLUME: f32 = 0.8;
|
||||
audio.emit_sfx(sfx_trigger_item, pos.0, Some(CAMPFIRE_VOLUME), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.0, Some(CAMPFIRE_VOLUME));
|
||||
internal_state.time = Instant::now();
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,6 @@ use common::{
|
||||
Inventory, Pos,
|
||||
},
|
||||
terrain::TerrainChunk,
|
||||
vol::ReadVol,
|
||||
};
|
||||
use common_state::State;
|
||||
use hashbrown::HashMap;
|
||||
@ -56,8 +55,7 @@ impl EventMapper for CombatEventMapper {
|
||||
) {
|
||||
let ecs = state.ecs();
|
||||
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
let cam_pos = camera.get_pos_with_focus();
|
||||
|
||||
for (entity, pos, inventory, character) in (
|
||||
&ecs.entities(),
|
||||
@ -77,14 +75,8 @@ impl EventMapper for CombatEventMapper {
|
||||
|
||||
// Check for SFX config entry for this movement
|
||||
if Self::should_emit(sfx_state, triggers.get_key_value(&mapped_event)) {
|
||||
let underwater = state
|
||||
.terrain()
|
||||
.get(cam_pos.map(|e| e.floor() as i32))
|
||||
.map(|b| b.is_liquid())
|
||||
.unwrap_or(false);
|
||||
|
||||
let sfx_trigger_item = triggers.get_key_value(&mapped_event);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.0, None, underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.0, None);
|
||||
sfx_state.time = Instant::now();
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ use common::{
|
||||
comp::{Body, CharacterState, PhysicsState, Pos, Scale, Vel},
|
||||
resources::DeltaTime,
|
||||
terrain::{BlockKind, TerrainChunk},
|
||||
vol::ReadVol,
|
||||
};
|
||||
use common_state::State;
|
||||
use hashbrown::HashMap;
|
||||
@ -58,8 +57,7 @@ impl EventMapper for MovementEventMapper {
|
||||
) {
|
||||
let ecs = state.ecs();
|
||||
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
let cam_pos = camera.get_pos_with_focus();
|
||||
|
||||
for (entity, pos, vel, body, scale, physics, character) in (
|
||||
&ecs.entities(),
|
||||
@ -102,18 +100,11 @@ impl EventMapper for MovementEventMapper {
|
||||
|
||||
// Check for SFX config entry for this movement
|
||||
if Self::should_emit(internal_state, triggers.get_key_value(&mapped_event)) {
|
||||
let underwater = state
|
||||
.terrain()
|
||||
.get(cam_pos.map(|e| e.floor() as i32))
|
||||
.map(|b| b.is_liquid())
|
||||
.unwrap_or(false);
|
||||
|
||||
let sfx_trigger_item = triggers.get_key_value(&mapped_event);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
pos.0,
|
||||
Some(Self::get_volume_for_body_type(body)),
|
||||
underwater,
|
||||
);
|
||||
internal_state.time = Instant::now();
|
||||
internal_state.steps_taken = 0.0;
|
||||
|
@ -96,6 +96,7 @@ use common::{
|
||||
outcome::Outcome,
|
||||
terrain::{BlockKind, SpriteKind, TerrainChunk},
|
||||
uid::Uid,
|
||||
vol::ReadVol,
|
||||
DamageSource,
|
||||
};
|
||||
use common_state::State;
|
||||
@ -418,8 +419,7 @@ impl SfxMgr {
|
||||
return;
|
||||
}
|
||||
|
||||
let focus_off = camera.get_focus_pos().map(f32::trunc);
|
||||
let cam_pos = camera.dependents().cam_pos + focus_off;
|
||||
let cam_pos = camera.get_pos_with_focus();
|
||||
|
||||
// Sets the listener position to the camera position facing the
|
||||
// same direction as the camera
|
||||
@ -427,6 +427,18 @@ impl SfxMgr {
|
||||
|
||||
let triggers = self.triggers.read();
|
||||
|
||||
let underwater = state
|
||||
.terrain()
|
||||
.get(cam_pos.map(|e| e.floor() as i32))
|
||||
.map(|b| b.is_liquid())
|
||||
.unwrap_or(false);
|
||||
|
||||
if underwater {
|
||||
audio.set_sfx_master_filter(888);
|
||||
} else {
|
||||
audio.set_sfx_master_filter(20000);
|
||||
}
|
||||
|
||||
self.event_mapper.maintain(
|
||||
audio,
|
||||
state,
|
||||
@ -444,82 +456,85 @@ impl SfxMgr {
|
||||
outcome: &Outcome,
|
||||
audio: &mut AudioFrontend,
|
||||
client: &Client,
|
||||
underwater: bool,
|
||||
) {
|
||||
if !audio.sfx_enabled() && !audio.subtitles_enabled {
|
||||
return;
|
||||
}
|
||||
let triggers = self.triggers.read();
|
||||
let uids = client.state().ecs().read_storage::<Uid>();
|
||||
|
||||
// TODO handle underwater
|
||||
if audio.listener.is_none() {
|
||||
return;
|
||||
}
|
||||
match outcome {
|
||||
Outcome::Explosion { pos, power, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Explosion);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
*pos,
|
||||
Some((power.abs() / 2.5).min(1.5)),
|
||||
underwater,
|
||||
);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some((power.abs() / 2.5).min(1.5)));
|
||||
},
|
||||
Outcome::Lightning { pos } => {
|
||||
let power = (1.0 - pos.distance(audio.listener.pos) / 5_000.0)
|
||||
let power = (1.0 - pos.distance(audio.listener_pos) / 6_000.0)
|
||||
.max(0.0)
|
||||
.powi(7);
|
||||
if power > 0.0 {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Lightning);
|
||||
// TODO: Don't use UI sfx, add a way to control position falloff
|
||||
audio.emit_ui_sfx(sfx_trigger_item, Some((power * 3.0).min(2.9)));
|
||||
let volume = (power * 3.0).min(2.9);
|
||||
// Delayed based on power (which in turn is based on distance) within a range of
|
||||
// 0.17 to 1.5 seconds
|
||||
// TODO: Make this more physically accurate
|
||||
audio.play_ambience_oneshot(
|
||||
super::channel::AmbienceChannelTag::Thunder,
|
||||
sfx_trigger_item,
|
||||
Some(volume),
|
||||
Some((1.0 / volume * 2.0).max(1.5)),
|
||||
);
|
||||
}
|
||||
},
|
||||
Outcome::GroundSlam { pos, .. } | Outcome::ClayGolemDash { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GroundSlam);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::SurpriseEgg { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SurpriseEgg);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::LaserBeam { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::LaserBeam);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::CyclopsCharge { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::CyclopsCharge);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::FlamethrowerCharge { pos, .. }
|
||||
| Outcome::TerracottaStatueCharge { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::CyclopsCharge);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::FuseCharge { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FuseCharge);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::Charge { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::CyclopsCharge);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::FlashFreeze { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlashFreeze);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::SummonedCreature { pos, body, .. } => {
|
||||
match body {
|
||||
Body::BipedSmall(body) => match body.species {
|
||||
biped_small::Species::IronDwarf => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Bleep);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
biped_small::Species::Boreal => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GigaRoar);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
biped_small::Species::ShamanicSpirit | biped_small::Species::Jiangshi => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Klonk);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
@ -527,7 +542,7 @@ impl SfxMgr {
|
||||
biped_large::Species::TerracottaBesieger
|
||||
| biped_large::Species::TerracottaPursuer => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Klonk);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
@ -535,25 +550,25 @@ impl SfxMgr {
|
||||
bird_medium::Species::Bat => {
|
||||
let sfx_trigger_item =
|
||||
triggers.get_key_value(&SfxEvent::BloodmoonHeiressSummon);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
Body::Crustacean(body) => match body.species {
|
||||
crustacean::Species::SoldierCrab => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Hiss);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
Body::Object(object::Body::Lavathrower) => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::DeepLaugh);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Body::Object(object::Body::Tornado)
|
||||
| Body::Object(object::Body::FieryTornado) => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Swoosh);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
_ => { // not mapped to sfx file
|
||||
},
|
||||
@ -561,35 +576,35 @@ impl SfxMgr {
|
||||
},
|
||||
Outcome::GroundDig { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GroundDig);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::PortalActivated { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::PortalActivated);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::TeleportedByPortal { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::TeleportedByPortal);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::IceSpikes { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::IceSpikes);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::IceCrack { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::IceCrack);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::Steam { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Steam);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::FireShockwave { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlameThrower);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::FromTheAshes { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FromTheAshes);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
},
|
||||
Outcome::ProjectileShot { pos, body, .. } => {
|
||||
match body {
|
||||
@ -605,7 +620,7 @@ impl SfxMgr {
|
||||
| object::Body::SpectralSwordLarge,
|
||||
) => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowShot);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None, underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None);
|
||||
},
|
||||
Body::Object(
|
||||
object::Body::BoltFire
|
||||
@ -617,7 +632,7 @@ impl SfxMgr {
|
||||
| object::Body::SpitPoison,
|
||||
) => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FireShot);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None, underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None);
|
||||
},
|
||||
Body::Object(
|
||||
object::Body::IronPikeBomb
|
||||
@ -626,7 +641,7 @@ impl SfxMgr {
|
||||
| object::Body::Pebble,
|
||||
) => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Whoosh);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None, underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None);
|
||||
},
|
||||
Body::Object(
|
||||
object::Body::LaserBeam
|
||||
@ -634,15 +649,15 @@ impl SfxMgr {
|
||||
| object::Body::LightningBolt,
|
||||
) => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::LaserBeam);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None, underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None);
|
||||
},
|
||||
Body::Object(object::Body::AdletTrap | object::Body::Mine) => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Yeet);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None, underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None);
|
||||
},
|
||||
Body::Object(object::Body::StrigoiHead) => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::StrigoiHead);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None, underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None);
|
||||
},
|
||||
_ => {
|
||||
// not mapped to sfx file
|
||||
@ -670,18 +685,17 @@ impl SfxMgr {
|
||||
) => {
|
||||
if target.is_none() {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowMiss);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
} else if *source == client.uid() {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
client.position().unwrap_or(*pos),
|
||||
Some(2.0),
|
||||
underwater,
|
||||
);
|
||||
} else {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
}
|
||||
},
|
||||
Body::Object(
|
||||
@ -689,18 +703,17 @@ impl SfxMgr {
|
||||
) => {
|
||||
if target.is_none() {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Klonk);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
} else if *source == client.uid() {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SmashKlonk);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
client.position().unwrap_or(*pos),
|
||||
Some(2.0),
|
||||
underwater,
|
||||
);
|
||||
} else {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SmashKlonk);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0));
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
@ -723,7 +736,7 @@ impl SfxMgr {
|
||||
| beam::FrontendSpecifier::Bubbles => {
|
||||
if thread_rng().gen_bool(0.5) {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SceptreBeam);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None, underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None);
|
||||
};
|
||||
},
|
||||
beam::FrontendSpecifier::Flamethrower
|
||||
@ -731,7 +744,7 @@ impl SfxMgr {
|
||||
| beam::FrontendSpecifier::PhoenixLaser => {
|
||||
if thread_rng().gen_bool(0.5) {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlameThrower);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None, underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, None);
|
||||
}
|
||||
},
|
||||
beam::FrontendSpecifier::Gravewarden | beam::FrontendSpecifier::WebStrand => {},
|
||||
@ -739,22 +752,12 @@ impl SfxMgr {
|
||||
Outcome::SpriteUnlocked { pos } => {
|
||||
// TODO: Dedicated sound effect!
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderOpen);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
pos.map(|e| e as f32 + 0.5),
|
||||
Some(2.0),
|
||||
underwater,
|
||||
);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
|
||||
},
|
||||
Outcome::FailedSpriteUnlock { pos } => {
|
||||
// TODO: Dedicated sound effect!
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::BreakBlock);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
pos.map(|e| e as f32 + 0.5),
|
||||
Some(2.0),
|
||||
underwater,
|
||||
);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(2.0));
|
||||
},
|
||||
Outcome::BreakBlock { pos, tool, .. } => {
|
||||
let sfx_trigger_item =
|
||||
@ -763,12 +766,7 @@ impl SfxMgr {
|
||||
} else {
|
||||
SfxEvent::BreakBlock
|
||||
});
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
pos.map(|e| e as f32 + 0.5),
|
||||
Some(3.0),
|
||||
underwater,
|
||||
);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.map(|e| e as f32 + 0.5), Some(3.0));
|
||||
},
|
||||
Outcome::DamagedBlock {
|
||||
pos,
|
||||
@ -788,7 +786,6 @@ impl SfxMgr {
|
||||
sfx_trigger_item,
|
||||
pos.map(|e| e as f32 + 0.5),
|
||||
Some(if *stage_changed { 3.0 } else { 2.0 }),
|
||||
underwater,
|
||||
);
|
||||
},
|
||||
Outcome::HealthChange { pos, info, .. } => {
|
||||
@ -797,20 +794,20 @@ impl SfxMgr {
|
||||
&& !matches!(info.cause, Some(DamageSource::Buff(_)))
|
||||
{
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Damage);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
|
||||
}
|
||||
},
|
||||
Outcome::Death { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Death);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
|
||||
},
|
||||
Outcome::Block { pos, parry, .. } => {
|
||||
if *parry {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Parry);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
|
||||
} else {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Block);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
|
||||
}
|
||||
},
|
||||
Outcome::PoiseChange { pos, state, .. } => match state {
|
||||
@ -818,22 +815,22 @@ impl SfxMgr {
|
||||
PoiseState::Interrupted => {
|
||||
let sfx_trigger_item =
|
||||
triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Interrupted));
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
|
||||
},
|
||||
PoiseState::Stunned => {
|
||||
let sfx_trigger_item =
|
||||
triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Stunned));
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
|
||||
},
|
||||
PoiseState::Dazed => {
|
||||
let sfx_trigger_item =
|
||||
triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Dazed));
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
|
||||
},
|
||||
PoiseState::KnockedDown => {
|
||||
let sfx_trigger_item =
|
||||
triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::KnockedDown));
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5));
|
||||
},
|
||||
},
|
||||
Outcome::Utterance { pos, kind, body } => {
|
||||
@ -841,7 +838,7 @@ impl SfxMgr {
|
||||
let sfx_trigger_item =
|
||||
triggers.get_key_value(&SfxEvent::Utterance(*kind, voice));
|
||||
if let Some(sfx_trigger_item) = sfx_trigger_item {
|
||||
audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5), underwater);
|
||||
audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5));
|
||||
} else {
|
||||
debug!(
|
||||
"No utterance sound effect exists for ({:?}, {:?})",
|
||||
@ -853,65 +850,40 @@ impl SfxMgr {
|
||||
Outcome::Glider { pos, wielded } => {
|
||||
if *wielded {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderOpen);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
|
||||
} else {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderClose);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0));
|
||||
}
|
||||
},
|
||||
Outcome::SpriteDelete { pos, sprite } => {
|
||||
match sprite {
|
||||
SpriteKind::SeaUrchin => {
|
||||
let pos = pos.map(|e| e as f32 + 0.5);
|
||||
let power = (0.6 - pos.distance(audio.listener.pos) / 5_000.0)
|
||||
let power = (0.6 - pos.distance(audio.listener_pos) / 5_000.0)
|
||||
.max(0.0)
|
||||
.powi(7);
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Explosion);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
pos,
|
||||
Some((power.abs() / 2.5).min(0.3)),
|
||||
underwater,
|
||||
);
|
||||
audio.emit_sfx(sfx_trigger_item, pos, Some((power.abs() / 2.5).min(0.3)));
|
||||
},
|
||||
_ => {},
|
||||
};
|
||||
},
|
||||
Outcome::Whoosh { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Whoosh);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
pos.map(|e| e + 0.5),
|
||||
Some(3.0),
|
||||
underwater,
|
||||
);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
|
||||
},
|
||||
Outcome::Swoosh { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Swoosh);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
pos.map(|e| e + 0.5),
|
||||
Some(3.0),
|
||||
underwater,
|
||||
);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
|
||||
},
|
||||
Outcome::Slash { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SmashKlonk);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
pos.map(|e| e + 0.5),
|
||||
Some(3.0),
|
||||
underwater,
|
||||
);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
|
||||
},
|
||||
Outcome::Bleep { pos, .. } => {
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Bleep);
|
||||
audio.emit_sfx(
|
||||
sfx_trigger_item,
|
||||
pos.map(|e| e + 0.5),
|
||||
Some(3.0),
|
||||
underwater,
|
||||
);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.map(|e| e + 0.5), Some(3.0));
|
||||
},
|
||||
Outcome::HeadLost { uid, .. } => {
|
||||
let positions = client.state().ecs().read_storage::<common::comp::Pos>();
|
||||
@ -923,7 +895,7 @@ impl SfxMgr {
|
||||
.and_then(|entity| positions.get(entity))
|
||||
{
|
||||
let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Death);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.0, Some(2.0), underwater);
|
||||
audio.emit_sfx(sfx_trigger_item, pos.0, Some(2.0));
|
||||
} else {
|
||||
error!("Couldn't get position of entity that lost head");
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Handles caching and retrieval of decoded `.ogg` sfx sound data, eliminating
|
||||
//! the need to decode files on each playback
|
||||
use common::assets::{self, AssetExt, Loader};
|
||||
use rodio::{source::Buffered, Decoder, Source};
|
||||
use kira::sound::static_sound::StaticSoundData;
|
||||
use std::{borrow::Cow, io};
|
||||
use tracing::warn;
|
||||
|
||||
@ -10,11 +10,11 @@ use tracing::warn;
|
||||
|
||||
struct SoundLoader;
|
||||
#[derive(Clone)]
|
||||
struct OggSound(Buffered<Decoder<io::Cursor<Vec<u8>>>>);
|
||||
struct OggSound(StaticSoundData);
|
||||
|
||||
impl Loader<OggSound> for SoundLoader {
|
||||
fn load(content: Cow<[u8]>, _: &str) -> Result<OggSound, assets::BoxedError> {
|
||||
let source = Decoder::new_vorbis(io::Cursor::new(content.into_owned()))?.buffered();
|
||||
let source = StaticSoundData::from_cursor(io::Cursor::new(content.into_owned()))?;
|
||||
Ok(OggSound(source))
|
||||
}
|
||||
}
|
||||
@ -37,7 +37,7 @@ impl OggSound {
|
||||
}
|
||||
|
||||
#[allow(clippy::implied_bounds_in_impls)]
|
||||
pub fn load_ogg(specifier: &str) -> impl Source + Iterator<Item = i16> {
|
||||
pub fn load_ogg(specifier: &str) -> StaticSoundData {
|
||||
OggSound::load_or_insert_with(specifier, |error| {
|
||||
warn!(?specifier, ?error, "Failed to load sound");
|
||||
OggSound::empty()
|
||||
|
@ -288,6 +288,7 @@ widget_ids! {
|
||||
gpu_timings[],
|
||||
weather,
|
||||
song_info,
|
||||
active_channels,
|
||||
|
||||
// Game Version
|
||||
version,
|
||||
@ -667,6 +668,7 @@ pub struct DebugInfo {
|
||||
pub num_particles_visible: u32,
|
||||
pub current_track: String,
|
||||
pub current_artist: String,
|
||||
pub active_channels: [usize; 4],
|
||||
}
|
||||
|
||||
pub struct HudInfo {
|
||||
@ -2897,11 +2899,23 @@ impl Hud {
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.set(self.ids.song_info, ui_widgets);
|
||||
Text::new(&format!(
|
||||
"Active channels: M{}, A{}, S{}, U{}",
|
||||
debug_info.active_channels[0],
|
||||
debug_info.active_channels[1],
|
||||
debug_info.active_channels[2],
|
||||
debug_info.active_channels[3],
|
||||
))
|
||||
.color(TEXT_COLOR)
|
||||
.down_from(self.ids.song_info, V_PAD)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.set(self.ids.active_channels, ui_widgets);
|
||||
|
||||
// Number of lights
|
||||
Text::new(&format!("Lights: {}", debug_info.num_lights,))
|
||||
.color(TEXT_COLOR)
|
||||
.down_from(self.ids.song_info, V_PAD)
|
||||
.down_from(self.ids.active_channels, V_PAD)
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.set(self.ids.num_lights, ui_widgets);
|
||||
@ -3481,7 +3495,8 @@ impl Hud {
|
||||
Subtitles::new(
|
||||
client,
|
||||
&global_state.settings,
|
||||
&global_state.audio.get_listener().clone(),
|
||||
global_state.audio.get_listener_pos(),
|
||||
global_state.audio.get_listener_ori(),
|
||||
&mut global_state.audio.subtitles,
|
||||
&self.fonts,
|
||||
i18n,
|
||||
|
@ -434,28 +434,28 @@ impl<'a> Widget for Sound<'a> {
|
||||
.set(state.ids.music_spacing_number, ui);
|
||||
|
||||
// Combat music toggle
|
||||
let audio = &self.global_state.audio;
|
||||
// let audio = &self.global_state.audio;
|
||||
|
||||
Text::new(&self.localized_strings.get_msg("hud-settings-combat_music"))
|
||||
.font_size(self.fonts.cyri.scale(14))
|
||||
.font_id(self.fonts.cyri.conrod_id)
|
||||
.down_from(state.ids.music_spacing_slider, 10.0)
|
||||
.x_align_to(state.ids.music_spacing_text, Align::Start)
|
||||
.color(TEXT_COLOR)
|
||||
.set(state.ids.combat_music_toggle_label, ui);
|
||||
// Text::new(&self.localized_strings.get_msg("hud-settings-combat_music"))
|
||||
// .font_size(self.fonts.cyri.scale(14))
|
||||
// .font_id(self.fonts.cyri.conrod_id)
|
||||
// .down_from(state.ids.music_spacing_slider, 10.0)
|
||||
// .x_align_to(state.ids.music_spacing_text, Align::Start)
|
||||
// .color(TEXT_COLOR)
|
||||
// .set(state.ids.combat_music_toggle_label, ui);
|
||||
|
||||
let combat_music_enabled = ToggleButton::new(
|
||||
audio.combat_music_enabled,
|
||||
self.imgs.checkbox,
|
||||
self.imgs.checkbox_checked,
|
||||
)
|
||||
.w_h(18.0, 18.0)
|
||||
.right_from(state.ids.combat_music_toggle_label, 10.0)
|
||||
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
|
||||
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
|
||||
.set(state.ids.combat_music_toggle_button, ui);
|
||||
// let combat_music_enabled = ToggleButton::new(
|
||||
// audio.combat_music_enabled,
|
||||
// self.imgs.checkbox,
|
||||
// self.imgs.checkbox_checked,
|
||||
// )
|
||||
// .w_h(18.0, 18.0)
|
||||
// .right_from(state.ids.combat_music_toggle_label, 10.0)
|
||||
// .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
|
||||
// .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
|
||||
// .set(state.ids.combat_music_toggle_button, ui);
|
||||
|
||||
events.push(ToggleCombatMusic(combat_music_enabled));
|
||||
// events.push(ToggleCombatMusic(combat_music_enabled));
|
||||
|
||||
// Audio Device Selector
|
||||
// --------------------------------------------
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{cmp::Ordering, collections::VecDeque};
|
||||
|
||||
use crate::{audio::Listener, settings::Settings, ui::fonts::Fonts};
|
||||
use crate::{settings::Settings, ui::fonts::Fonts};
|
||||
use client::Client;
|
||||
use conrod_core::{
|
||||
widget::{self, Id, Rectangle, Text},
|
||||
@ -22,7 +22,8 @@ widget_ids! {
|
||||
pub struct Subtitles<'a> {
|
||||
client: &'a Client,
|
||||
settings: &'a Settings,
|
||||
listener: &'a Listener,
|
||||
listener_pos: Vec3<f32>,
|
||||
listener_ori: Vec3<f32>,
|
||||
|
||||
fonts: &'a Fonts,
|
||||
|
||||
@ -38,7 +39,8 @@ impl<'a> Subtitles<'a> {
|
||||
pub fn new(
|
||||
client: &'a Client,
|
||||
settings: &'a Settings,
|
||||
listener: &'a Listener,
|
||||
listener_pos: Vec3<f32>,
|
||||
listener_ori: Vec3<f32>,
|
||||
new_subtitles: &'a mut VecDeque<Subtitle>,
|
||||
fonts: &'a Fonts,
|
||||
localized_strings: &'a Localization,
|
||||
@ -46,7 +48,8 @@ impl<'a> Subtitles<'a> {
|
||||
Self {
|
||||
client,
|
||||
settings,
|
||||
listener,
|
||||
listener_pos,
|
||||
listener_ori,
|
||||
fonts,
|
||||
new_subtitles,
|
||||
common: widget::CommonBuilder::default(),
|
||||
@ -186,8 +189,8 @@ impl<'a> Widget for Subtitles<'a> {
|
||||
|
||||
let widget::UpdateArgs { state, ui, .. } = args;
|
||||
let time = self.client.state().get_time();
|
||||
let listener_pos = self.listener.pos;
|
||||
let listener_forward = self.listener.ori;
|
||||
let listener_pos = self.listener_pos;
|
||||
let listener_forward = self.listener_ori;
|
||||
|
||||
// Update subtitles and look for changes
|
||||
let mut subtitles = state.subtitles.clone();
|
||||
|
@ -103,9 +103,9 @@ impl GlobalState {
|
||||
self.window.needs_refresh_resize();
|
||||
}
|
||||
|
||||
pub fn maintain(&mut self, dt: std::time::Duration) {
|
||||
pub fn maintain(&mut self) {
|
||||
span!(_guard, "maintain", "GlobalState::maintain");
|
||||
self.audio.maintain(dt);
|
||||
self.audio.maintain();
|
||||
self.window.renderer().maintain()
|
||||
}
|
||||
|
||||
|
@ -158,7 +158,8 @@ fn main() {
|
||||
settings.audio.num_sfx_channels,
|
||||
settings.audio.num_ui_channels,
|
||||
settings.audio.subtitles,
|
||||
settings.audio.combat_music_enabled,
|
||||
// settings.audio.combat_music_enabled,
|
||||
false, // We're disabling combat music for now
|
||||
),
|
||||
// AudioOutput::Device(ref dev) => Some(dev.clone()),
|
||||
};
|
||||
|
@ -274,6 +274,6 @@ fn handle_main_events_cleared(
|
||||
common_base::tracy_client::frame_mark();
|
||||
|
||||
// Maintain global state.
|
||||
global_state.maintain(global_state.clock.dt());
|
||||
global_state.maintain();
|
||||
}
|
||||
}
|
||||
|
@ -773,4 +773,9 @@ impl Camera {
|
||||
/// Return a unit vector in the right direction on the XY plane for
|
||||
/// the current camera orientation
|
||||
pub fn right_xy(&self) -> Vec2<f32> { Vec2::new(f32::cos(self.ori.x), -f32::sin(self.ori.x)) }
|
||||
|
||||
pub fn get_pos_with_focus(&self) -> Vec3<f32> {
|
||||
let focus_off = self.get_focus_pos().map(f32::trunc);
|
||||
self.dependents().cam_pos + focus_off
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ pub use self::{
|
||||
trail::TrailMgr,
|
||||
};
|
||||
use crate::{
|
||||
audio::{ambient, ambient::AmbientMgr, music::MusicMgr, sfx::SfxMgr, AudioFrontend},
|
||||
audio::{ambience, ambience::AmbienceMgr, music::MusicMgr, sfx::SfxMgr, AudioFrontend},
|
||||
render::{
|
||||
create_skybox_mesh, CloudsLocals, Consts, CullingMode, Drawer, GlobalModel, Globals,
|
||||
GlobalsBindGroup, Light, Model, PointLightMatrix, PostProcessLocals, RainOcclusionLocals,
|
||||
@ -116,7 +116,7 @@ pub struct Scene {
|
||||
tether_mgr: TetherMgr,
|
||||
pub sfx_mgr: SfxMgr,
|
||||
pub music_mgr: MusicMgr,
|
||||
ambient_mgr: AmbientMgr,
|
||||
ambience_mgr: AmbienceMgr,
|
||||
|
||||
integrated_rain_vel: f32,
|
||||
wind_vel: Vec2<f32>,
|
||||
@ -356,8 +356,8 @@ impl Scene {
|
||||
tether_mgr: TetherMgr::new(renderer),
|
||||
sfx_mgr: SfxMgr::default(),
|
||||
music_mgr: MusicMgr::new(&calendar),
|
||||
ambient_mgr: AmbientMgr {
|
||||
ambience: ambient::load_ambience_items(),
|
||||
ambience_mgr: AmbienceMgr {
|
||||
ambience: ambience::load_ambience_items(),
|
||||
},
|
||||
integrated_rain_vel: 0.0,
|
||||
wind_vel: Vec2::zero(),
|
||||
@ -466,19 +466,12 @@ impl Scene {
|
||||
outcome: &Outcome,
|
||||
scene_data: &SceneData,
|
||||
audio: &mut AudioFrontend,
|
||||
state: &State,
|
||||
cam_pos: Vec3<f32>,
|
||||
) {
|
||||
span!(_guard, "handle_outcome", "Scene::handle_outcome");
|
||||
let underwater = state
|
||||
.terrain()
|
||||
.get(cam_pos.map(|e| e.floor() as i32))
|
||||
.map(|b| b.is_liquid())
|
||||
.unwrap_or(false);
|
||||
self.particle_mgr
|
||||
.handle_outcome(outcome, scene_data, &self.figure_mgr);
|
||||
self.sfx_mgr
|
||||
.handle_outcome(outcome, audio, scene_data.client, underwater);
|
||||
.handle_outcome(outcome, audio, scene_data.client);
|
||||
|
||||
match outcome {
|
||||
Outcome::Lightning { pos } => {
|
||||
@ -1355,7 +1348,7 @@ impl Scene {
|
||||
client,
|
||||
);
|
||||
|
||||
self.ambient_mgr
|
||||
self.ambience_mgr
|
||||
.maintain(audio, scene_data.state, client, &self.camera);
|
||||
|
||||
self.music_mgr.maintain(audio, scene_data.state, client);
|
||||
|
@ -250,21 +250,6 @@ impl SessionState {
|
||||
);
|
||||
self.scene.maintain_debug_vectors(&client, &mut self.lines);
|
||||
|
||||
// All this camera code is just to determine if it's underwater for the sfx
|
||||
// filter
|
||||
let camera = self.scene.camera_mut();
|
||||
camera.compute_dependents(&client.state().terrain());
|
||||
let camera::Dependents { cam_pos, .. } = self.scene.camera().dependents();
|
||||
let focus_pos = self.scene.camera().get_focus_pos();
|
||||
let focus_off = focus_pos.map(|e| e.trunc());
|
||||
let cam_pos = cam_pos + focus_off;
|
||||
let underwater = client
|
||||
.state()
|
||||
.terrain()
|
||||
.get(cam_pos.map(|e| e.floor() as i32))
|
||||
.map(|b| b.is_liquid())
|
||||
.unwrap_or(false);
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
// Update mumble positional audio
|
||||
@ -361,7 +346,6 @@ impl SessionState {
|
||||
sfx_trigger_item,
|
||||
client.position().unwrap_or_default(),
|
||||
Some(1.0),
|
||||
underwater,
|
||||
),
|
||||
}
|
||||
|
||||
@ -419,7 +403,9 @@ impl SessionState {
|
||||
client::Event::CharacterCreated(_) => {},
|
||||
client::Event::CharacterEdited(_) => {},
|
||||
client::Event::CharacterError(_) => {},
|
||||
client::Event::CharacterJoined(_) => self.scene.music_mgr.reset_track(),
|
||||
client::Event::CharacterJoined(_) => {
|
||||
self.scene.music_mgr.reset_track();
|
||||
},
|
||||
client::Event::MapMarker(event) => {
|
||||
self.hud.show.update_map_markers(event);
|
||||
},
|
||||
@ -1639,6 +1625,7 @@ impl PlayState for SessionState {
|
||||
as u32,
|
||||
current_track: self.scene.music_mgr().current_track(),
|
||||
current_artist: self.scene.music_mgr().current_artist(),
|
||||
active_channels: global_state.audio.get_num_active_channels(),
|
||||
}
|
||||
});
|
||||
|
||||
@ -2204,13 +2191,8 @@ impl PlayState for SessionState {
|
||||
|
||||
// Process outcomes from client
|
||||
for outcome in outcomes {
|
||||
self.scene.handle_outcome(
|
||||
&outcome,
|
||||
&scene_data,
|
||||
&mut global_state.audio,
|
||||
client.state(),
|
||||
cam_pos,
|
||||
);
|
||||
self.scene
|
||||
.handle_outcome(&outcome, &scene_data, &mut global_state.audio);
|
||||
self.hud
|
||||
.handle_outcome(&outcome, scene_data.client, global_state);
|
||||
}
|
||||
|
@ -298,21 +298,16 @@ impl SettingsChange {
|
||||
Audio::ToggleCombatMusic(combat_music_enabled) => {
|
||||
global_state.audio.combat_music_enabled = combat_music_enabled
|
||||
},
|
||||
//Audio::ChangeAudioDevice(name) => {
|
||||
// global_state.audio.set_device(name.clone());
|
||||
|
||||
// settings.audio.output = AudioOutput::Device(name);
|
||||
//},
|
||||
Audio::ResetAudioSettings => {
|
||||
settings.audio = AudioSettings::default();
|
||||
|
||||
let audio = &mut global_state.audio;
|
||||
|
||||
// TODO: check if updating the master volume is necessary
|
||||
// (it wasn't done before)
|
||||
audio.set_master_volume(settings.audio.master_volume.get_checked());
|
||||
audio.set_music_volume(settings.audio.music_volume.get_checked());
|
||||
audio.set_ambience_volume(settings.audio.ambience_volume.get_checked());
|
||||
audio.set_sfx_volume(settings.audio.sfx_volume.get_checked());
|
||||
audio.set_music_spacing(settings.audio.music_spacing);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -59,15 +59,15 @@ impl Default for AudioSettings {
|
||||
Self {
|
||||
master_volume: AudioVolume::new(0.8, false),
|
||||
inactive_master_volume_perc: AudioVolume::new(0.5, false),
|
||||
music_volume: AudioVolume::new(0.3, false),
|
||||
sfx_volume: AudioVolume::new(0.6, false),
|
||||
ambience_volume: AudioVolume::new(0.6, false),
|
||||
num_sfx_channels: 60,
|
||||
num_ui_channels: 10,
|
||||
music_volume: AudioVolume::new(0.5, false),
|
||||
sfx_volume: AudioVolume::new(0.8, false),
|
||||
ambience_volume: AudioVolume::new(0.8, false),
|
||||
num_sfx_channels: 64,
|
||||
num_ui_channels: 16,
|
||||
music_spacing: 1.0,
|
||||
subtitles: false,
|
||||
output: AudioOutput::Automatic,
|
||||
combat_music_enabled: true,
|
||||
combat_music_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user