diff --git a/src/main/java/ch/njol/skript/expressions/ExprBlockSound.java b/src/main/java/ch/njol/skript/expressions/ExprBlockSound.java
new file mode 100644
index 00000000000..ef207692089
--- /dev/null
+++ b/src/main/java/ch/njol/skript/expressions/ExprBlockSound.java
@@ -0,0 +1,137 @@
+package ch.njol.skript.expressions;
+
+import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.util.SimpleExpression;
+import ch.njol.util.Kleenean;
+import org.bukkit.Sound;
+import org.bukkit.SoundGroup;
+import org.bukkit.block.Block;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+
+@Name("Block Sound")
+@Description({
+ "Gets the sound that a given block, blockdata, or itemtype will use in a specific scenario.",
+ "This will return a string in the form of \"SOUND_EXAMPLE\", which can be used in the play sound syntax.",
+ "",
+ "Check out this website for a list of sounds in Minecraft, " +
+ "or this one to go to the Sounds wiki page."
+})
+@Examples({
+ "play sound (break sound of dirt) at all players",
+ "set {_sounds::*} to place sounds of dirt, grass block, blue wool and stone"
+})
+@Since("INSERT VERSION")
+public class ExprBlockSound extends SimpleExpression {
+
+ public enum SoundType {
+ BREAK {
+ @Override
+ public Sound getSound(SoundGroup group) {
+ return group.getBreakSound();
+ }
+ },
+
+ FALL {
+ @Override
+ public Sound getSound(SoundGroup group) {
+ return group.getFallSound();
+ }
+ },
+
+ HIT {
+ @Override
+ public Sound getSound(SoundGroup group) {
+ return group.getHitSound();
+ }
+ },
+
+ PLACE {
+ @Override
+ public Sound getSound(SoundGroup group) {
+ return group.getPlaceSound();
+ }
+ },
+
+ STEP {
+ @Override
+ public Sound getSound(SoundGroup group) {
+ return group.getStepSound();
+ }
+ };
+
+ public abstract @Nullable Sound getSound(SoundGroup group);
+ }
+
+ static {
+ SimplePropertyExpression.register(ExprBlockSound.class, String.class, "(1:break|2:fall|3:hit|4:place|5:step) sound[s]", "blocks/blockdatas/itemtypes");
+ }
+
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private SoundType soundType;
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private Expression> objects;
+
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ soundType = SoundType.values()[parseResult.mark - 1];
+ objects = exprs[0];
+ return true;
+ }
+
+ @Override
+ protected String @Nullable [] get(Event event) {
+ return objects.stream(event)
+ .map(this::convertAndGetSound)
+ .filter(Objects::nonNull)
+ .distinct()
+ .map(Sound::name)
+ .toArray(String[]::new);
+ }
+
+ private @Nullable SoundGroup getSoundGroup(Object object) {
+ if (object instanceof Block block) {
+ return block.getBlockData().getSoundGroup();
+ } else if (object instanceof BlockData data) {
+ return data.getSoundGroup();
+ } else if (object instanceof ItemType item) {
+ if (item.hasBlock())
+ return item.getMaterial().createBlockData().getSoundGroup();
+ }
+ return null;
+ }
+
+ private @Nullable Sound convertAndGetSound(Object object) {
+ SoundGroup group = getSoundGroup(object);
+ if (group == null)
+ return null;
+ return this.soundType.getSound(group);
+ }
+
+ @Override
+ public boolean isSingle() {
+ return objects.isSingle();
+ }
+
+ @Override
+ public @NotNull Class extends String> getReturnType() {
+ return String.class;
+ }
+
+ @Override
+ public @NotNull String toString(@Nullable Event event, boolean debug) {
+ return this.soundType.name().toLowerCase() + " sound of " + objects.toString(event, debug);
+ }
+
+}
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprBlockSound.sk b/src/test/skript/tests/syntaxes/expressions/ExprBlockSound.sk
new file mode 100644
index 00000000000..133ea5a7e7c
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/expressions/ExprBlockSound.sk
@@ -0,0 +1,46 @@
+test "block sounds (1.20+)":
+
+ # === SETUP ===
+
+ set {_before} to blockdata of (block at (spawn of world "world"))
+ set {_20} to whether running minecraft "1.20"
+ set {_empty} to "INTENTIONALLY_EMPTY" if running minecraft "1.20" else "BLOCK_STONE_PLACE"
+
+ # === TESTS ===
+
+ set {_stone::*} to getObjects(stone)
+ assert break sound of {_stone::*} is "BLOCK_STONE_BREAK" with "break sound of stone wasn't BLOCK_STONE_BREAK"
+ assert fall sound of {_stone::*} is "BLOCK_STONE_FALL" with "fall sound of stone wasn't BLOCK_STONE_FALL"
+ assert hit sound of {_stone::*} is "BLOCK_STONE_HIT" with "hit sound of stone wasn't BLOCK_STONE_HIT"
+ assert place sound of {_stone::*} is "BLOCK_STONE_PLACE" with "place sound of stone wasn't BLOCK_STONE_PLACE"
+ assert step sound of {_stone::*} is "BLOCK_STONE_STEP" with "step sound of stone wasn't BLOCK_STONE_STEP"
+
+ set {_wool::*} to getObjects(wool)
+ assert break sound of {_wool::*} is "BLOCK_WOOL_BREAK" with "break sound of stone wasn't BLOCK_WOOL_BREAK"
+ assert fall sound of {_wool::*} is "BLOCK_WOOL_FALL" with "fall sound of stone wasn't BLOCK_WOOL_FALL"
+ assert hit sound of {_wool::*} is "BLOCK_WOOL_HIT" with "hit sound of stone wasn't BLOCK_WOOL_HIT"
+ assert place sound of {_wool::*} is "BLOCK_WOOL_PLACE" with "place sound of stone wasn't BLOCK_WOOL_PLACE"
+ assert step sound of {_wool::*} is "BLOCK_WOOL_STEP" with "step sound of stone wasn't BLOCK_WOOL_STEP"
+
+ assert break sound of (water, lava and bubble column) is ("INTENTIONALLY_EMPTY" if {_20} is true else "BLOCK_STONE_BREAK") with "break sound of water, lava, or bubble column wasn't INTENTIONALLY_EMPTY"
+ assert fall sound of (water, lava and bubble column) is ("INTENTIONALLY_EMPTY" if {_20} is true else "BLOCK_STONE_FALL") with "fall sound of water, lava, or bubble column wasn't INTENTIONALLY_EMPTY"
+ assert hit sound of (water, lava and bubble column) is ("INTENTIONALLY_EMPTY" if {_20} is true else "BLOCK_STONE_HIT") with "hit sound of water, lava, or bubble column wasn't INTENTIONALLY_EMPTY"
+ assert place sound of (water, lava and bubble column) is ("INTENTIONALLY_EMPTY" if {_20} is true else "BLOCK_STONE_PLACE") with "place sound of water, lava, or bubble column wasn't INTENTIONALLY_EMPTY"
+ assert step sound of (water, lava and bubble column) is ("INTENTIONALLY_EMPTY" if {_20} is true else "BLOCK_STONE_STEP") with "step sound of water, lava, or bubble column wasn't INTENTIONALLY_EMPTY"
+
+ assert break sound of (diamond, diamond sword and {_none}) is not set with "break sound of non-block item shouldn't be set"
+ assert fall sound of (diamond, diamond sword and {_none}) is not set with "fall sound of non-block item shouldn't be set"
+ assert hit sound of (diamond, diamond sword and {_none}) is not set with "hit sound of non-block item shouldn't be set"
+ assert place sound of (diamond, diamond sword and {_none}) is not set with "place sound of non-block item shouldn't be set"
+ assert step sound of (diamond, diamond sword and {_none}) is not set with "step sound of non-block item shouldn't be set"
+
+ # === CLEANUP ===
+
+ set blockdata of {_block} to {_before}
+
+local function getObjects(i: itemtype) :: objects:
+ if (block at (spawn of world "world")) is not {_i}:
+ set block at (spawn of world "world") to {_i}
+ set {_block} to block at (spawn of world "world")
+ set {_data} to blockdata of {_block}
+ return {_block}, {_data} and {_i}