Merge branch 'DaforLynx/transition-to-kira' into 'master'

Switch from Rodio to Kira

See merge request veloren/veloren!4627
This commit is contained in:
DaforLynx
2024-11-06 19:50:45 +00:00
32 changed files with 1776 additions and 1391 deletions

View File

@ -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
View File

@ -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",

View File

@ -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"] }

View File

@ -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,
)
]

View File

@ -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),
// ),
]
)
)

View File

@ -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),
// ),
]
)
)

View File

@ -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),
},

View File

@ -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),
// ),
]
)

View File

@ -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 {

View File

@ -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"] }

View 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";
}

View File

@ -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";
}

View File

@ -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

View File

@ -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 {:?}, {:?}, {:?}, {:?}, {:?}",
&current_period_of_day,
&current_weather,
&current_site,
&current_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(),

View File

@ -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();

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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");
}

View File

@ -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()

View File

@ -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,

View File

@ -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
// --------------------------------------------

View File

@ -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();

View File

@ -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()
}

View File

@ -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()),
};

View File

@ -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();
}
}

View File

@ -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
}
}

View File

@ -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);

View File

@ -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);
}

View File

@ -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);
},
}
},

View File

@ -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,
}
}
}