From 601d1936c9ab4787d43f55d67900ed7c46fd3452 Mon Sep 17 00:00:00 2001
From: Perttu Ahola <celeron55@gmail.com>
Date: Sat, 24 Mar 2012 19:01:26 +0200
Subject: [PATCH] Lua API for playing sounds

---
 doc/lua_api.txt                           |  46 +++++++-
 games/mesetint/mods/experimental/init.lua |  36 ++++++
 src/client.cpp                            | 111 +++++++++++++++++-
 src/client.h                              |   9 ++
 src/clientserver.h                        |  39 +++++--
 src/content_cao.cpp                       |  20 +++-
 src/scriptapi.cpp                         |  71 +++++++++++-
 src/server.cpp                            | 134 ++++++++++++++++++++++
 src/server.h                              |  64 +++++++++++
 src/sound.h                               |   4 +
 src/sound_openal.cpp                      |  18 +++
 11 files changed, 532 insertions(+), 20 deletions(-)

diff --git a/doc/lua_api.txt b/doc/lua_api.txt
index 53857ee3c..43f49dde6 100644
--- a/doc/lua_api.txt
+++ b/doc/lua_api.txt
@@ -363,8 +363,40 @@ dump2(obj, name="_", dumped={})
 dump(obj, dumped={})
 ^ Return object serialized as a string
 
+Sounds
+-------
+Examples of sound parameter tables:
+-- Play locationless on all clients
+{
+	gain = 1.0, -- default
+}
+-- Play locationless to a player
+{
+	to_player = name,
+	gain = 1.0, -- default
+}
+-- Play in a location
+{
+	pos = {x=1,y=2,z=3},
+	gain = 1.0, -- default
+	max_hear_distance = 32, -- default
+}
+-- Play connected to an object, looped
+{
+    object = <an ObjectRef>,
+	gain = 1.0, -- default
+	max_hear_distance = 32, -- default
+    loop = true, -- only sounds connected to objects can be looped
+}
+
 minetest namespace reference
 -----------------------------
+minetest.get_current_modname() -> string
+minetest.get_modpath(modname) -> eg. "/home/user/.minetest/usermods/modname"
+^ Useful for loading additional .lua modules or static data from mod
+minetest.get_worldpath(modname) -> eg. "/home/user/.minetest/world"
+^ Useful for storing custom data
+
 minetest.register_entity(name, prototype table)
 minetest.register_abm(abm definition)
 minetest.register_node(name, node definition)
@@ -372,6 +404,7 @@ minetest.register_tool(name, item definition)
 minetest.register_craftitem(name, item definition)
 minetest.register_alias(name, convert_to)
 minetest.register_craft(recipe)
+
 minetest.register_globalstep(func(dtime))
 minetest.register_on_placenode(func(pos, newnode, placer))
 minetest.register_on_dignode(func(pos, oldnode, digger))
@@ -383,20 +416,22 @@ minetest.register_on_respawnplayer(func(ObjectRef))
 ^ return true in func to disable regular player placement
 ^ currently called _before_ repositioning of player occurs
 minetest.register_on_chat_message(func(name, message))
+
 minetest.add_to_creative_inventory(itemstring)
 minetest.setting_get(name) -> string or nil
 minetest.setting_getbool(name) -> boolean value or nil
+
 minetest.chat_send_all(text)
 minetest.chat_send_player(name, text)
 minetest.get_player_privs(name) -> set of privs
 minetest.get_inventory(location) -> InvRef
 ^ location = eg. {type="player", name="celeron55"}
                  {type="node", pos={x=, y=, z=}}
-minetest.get_current_modname() -> string
-minetest.get_modpath(modname) -> eg. "/home/user/.minetest/usermods/modname"
-^ Useful for loading additional .lua modules or static data from mod
-minetest.get_worldpath(modname) -> eg. "/home/user/.minetest/world"
-^ Useful for storing custom data
+
+minetest.sound_play(spec, parameters) -> handle
+^ spec = SimpleSoundSpec
+^ parameters = sound parameter table
+minetest.sound_stop(handle)
 
 minetest.debug(line)
 ^ Goes to dstream
@@ -681,6 +716,7 @@ Node definition (register_node)
     legacy_wallmounted = false, -- Support maps made in and before January 2012
     sounds = {
         footstep = <SimpleSoundSpec>,
+        dig = <SimpleSoundSpec>, -- "__group" = group-based sound (default)
         dug = <SimpleSoundSpec>,
     },
 }
diff --git a/games/mesetint/mods/experimental/init.lua b/games/mesetint/mods/experimental/init.lua
index 364eeb10c..e11086274 100644
--- a/games/mesetint/mods/experimental/init.lua
+++ b/games/mesetint/mods/experimental/init.lua
@@ -6,6 +6,42 @@
 
 experimental = {}
 
+timers_to_add = {}
+timers = {}
+minetest.register_globalstep(function(dtime)
+  for indes, timer in ipairs(timers_to_add) do
+    table.insert(timers, timer)
+  end
+  timers_to_add = {}
+  for index, timer in ipairs(timers) do
+    timer.time = timer.time - dtime
+    if timer.time <= 0 then
+      timer.func()
+      timers[index] = nil
+    end
+  end
+end)
+
+after = function(time, func)
+  table.insert(timers_to_add, {time=time, func=func})
+end
+
+--[[
+stepsound = -1
+function test_sound()
+	print("test_sound")
+	stepsound = minetest.sound_play("default_grass_footstep", {gain=1.0})
+	after(2.0, test_sound)
+	--after(0.1, test_sound_stop)
+end
+function test_sound_stop()
+	print("test_sound_stop")
+	minetest.sound_stop(stepsound)
+	after(2.0, test_sound)
+end
+test_sound()
+--]]
+
 function on_step(dtime)
 	-- print("experimental on_step")
 	--[[
diff --git a/src/client.cpp b/src/client.cpp
index d8fb4eb77..89070d66b 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -261,7 +261,8 @@ Client::Client(
 	m_nodedef_received(false),
 	m_time_of_day_set(false),
 	m_last_time_of_day_f(-1),
-	m_time_of_day_update_timer(0)
+	m_time_of_day_update_timer(0),
+	m_removed_sounds_check_timer(0)
 {
 	m_packetcounter_timer = 0.0;
 	//m_delete_unused_sectors_timer = 0.0;
@@ -733,6 +734,63 @@ void Client::step(float dtime)
 			m_inventory_updated = true;
 		}
 	}
+
+	/*
+		Update positions of sounds attached to objects
+	*/
+	{
+		for(std::map<int, u16>::iterator
+				i = m_sounds_to_objects.begin();
+				i != m_sounds_to_objects.end(); i++)
+		{
+			int client_id = i->first;
+			u16 object_id = i->second;
+			ClientActiveObject *cao = m_env.getActiveObject(object_id);
+			if(!cao)
+				continue;
+			v3f pos = cao->getPosition();
+			m_sound->updateSoundPosition(client_id, pos);
+		}
+	}
+	
+	/*
+		Handle removed remotely initiated sounds
+	*/
+	m_removed_sounds_check_timer += dtime;
+	if(m_removed_sounds_check_timer >= 2.32)
+	{
+		m_removed_sounds_check_timer = 0;
+		// Find removed sounds and clear references to them
+		std::set<s32> removed_server_ids;
+		for(std::map<s32, int>::iterator
+				i = m_sounds_server_to_client.begin();
+				i != m_sounds_server_to_client.end();)
+		{
+			s32 server_id = i->first;
+			int client_id = i->second;
+			i++;
+			if(!m_sound->soundExists(client_id)){
+				m_sounds_server_to_client.erase(server_id);
+				m_sounds_client_to_server.erase(client_id);
+				m_sounds_to_objects.erase(client_id);
+				removed_server_ids.insert(server_id);
+			}
+		}
+		// Sync to server
+		if(removed_server_ids.size() != 0)
+		{
+			std::ostringstream os(std::ios_base::binary);
+			writeU16(os, TOSERVER_REMOVED_SOUNDS);
+			writeU16(os, removed_server_ids.size());
+			for(std::set<s32>::iterator i = removed_server_ids.begin();
+					i != removed_server_ids.end(); i++)
+				writeS32(os, *i);
+			std::string s = os.str();
+			SharedBuffer<u8> data((u8*)s.c_str(), s.size());
+			// Send as reliable
+			Send(0, data, true);
+		}
+	}
 }
 
 // Virtual methods from con::PeerHandler
@@ -1610,6 +1668,57 @@ void Client::ProcessData(u8 *data, u32 datasize, u16 sender_peer_id)
 		m_itemdef->deSerialize(tmp_is2);
 		m_itemdef_received = true;
 	}
+	else if(command == TOCLIENT_PLAY_SOUND)
+	{
+		std::string datastring((char*)&data[2], datasize-2);
+		std::istringstream is(datastring, std::ios_base::binary);
+
+		s32 server_id = readS32(is);
+		std::string name = deSerializeString(is);
+		float gain = readF1000(is);
+		int type = readU8(is); // 0=local, 1=positional, 2=object
+		v3f pos = readV3F1000(is);
+		u16 object_id = readU16(is);
+		bool loop = readU8(is);
+		// Start playing
+		int client_id = -1;
+		switch(type){
+		case 0: // local
+			client_id = m_sound->playSound(name, false, gain);
+			break;
+		case 1: // positional
+			client_id = m_sound->playSoundAt(name, false, gain, pos);
+			break;
+		case 2: { // object
+			ClientActiveObject *cao = m_env.getActiveObject(object_id);
+			if(cao)
+				pos = cao->getPosition();
+			client_id = m_sound->playSoundAt(name, loop, gain, pos);
+			// TODO: Set up sound to move with object
+			break; }
+		default:
+			break;
+		}
+		if(client_id != -1){
+			m_sounds_server_to_client[server_id] = client_id;
+			m_sounds_client_to_server[client_id] = server_id;
+			if(object_id != 0)
+				m_sounds_to_objects[client_id] = object_id;
+		}
+	}
+	else if(command == TOCLIENT_STOP_SOUND)
+	{
+		std::string datastring((char*)&data[2], datasize-2);
+		std::istringstream is(datastring, std::ios_base::binary);
+
+		s32 server_id = readS32(is);
+		std::map<s32, int>::iterator i =
+				m_sounds_server_to_client.find(server_id);
+		if(i != m_sounds_server_to_client.end()){
+			int client_id = i->second;
+			m_sound->stopSound(client_id);
+		}
+	}
 	else
 	{
 		infostream<<"Client: Ignoring unknown command "
diff --git a/src/client.h b/src/client.h
index 13b36106c..3a47a08f6 100644
--- a/src/client.h
+++ b/src/client.h
@@ -376,6 +376,15 @@ class Client : public con::PeerHandler, public InventoryManager, public IGameDef
 	bool m_time_of_day_set;
 	float m_last_time_of_day_f;
 	float m_time_of_day_update_timer;
+
+	// Sounds
+	float m_removed_sounds_check_timer;
+	// Mapping from server sound ids to our sound ids
+	std::map<s32, int> m_sounds_server_to_client;
+	// And the other way!
+	std::map<int, s32> m_sounds_client_to_server;
+	// And relations to objects
+	std::map<int, u16> m_sounds_to_objects;
 };
 
 #endif // !SERVER
diff --git a/src/clientserver.h b/src/clientserver.h
index b4e9ccfc8..9cbb7a685 100644
--- a/src/clientserver.h
+++ b/src/clientserver.h
@@ -46,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 		Compress the contents of TOCLIENT_ITEMDEF and TOCLIENT_NODEDEF
 	PROTOCOL_VERSION 8:
 		Digging based on item groups
+		Many things
 */
 
 #define PROTOCOL_VERSION 8
@@ -268,7 +269,25 @@ enum ToClientCommand
 		u32 length of next item
 		serialized ItemDefManager
 	*/
+	
+	TOCLIENT_PLAY_SOUND = 0x3f,
+	/*
+		u16 command
+		s32 sound_id
+		u16 len
+		u8[len] sound name
+		s32 gain*1000
+		u8 type (0=local, 1=positional, 2=object)
+		s32[3] pos_nodes*10000
+		u16 object_id
+		u8 loop (bool)
+	*/
 
+	TOCLIENT_STOP_SOUND = 0x40,
+	/*
+		u16 command
+		s32 sound_id
+	*/
 };
 
 enum ToServerCommand
@@ -442,15 +461,21 @@ enum ToServerCommand
 		(Obsoletes TOSERVER_GROUND_ACTION and TOSERVER_CLICK_ACTIVEOBJECT.)
 	*/
 	
-	TOSERVER_REQUEST_TEXTURES = 0x40,
+	TOSERVER_REMOVED_SOUNDS = 0x3a,
+	/*
+		u16 command
+		u16 len
+		s32[len] sound_id
+	*/
 
+	TOSERVER_REQUEST_TEXTURES = 0x40,
 	/*
-			u16 command
-			u16 number of textures requested
-			for each texture {
-				u16 length of name
-				string name
-			}
+		u16 command
+		u16 number of textures requested
+		for each texture {
+			u16 length of name
+			string name
+		}
 	 */
 
 };
diff --git a/src/content_cao.cpp b/src/content_cao.cpp
index 3aba4c7cb..fc1df377a 100644
--- a/src/content_cao.cpp
+++ b/src/content_cao.cpp
@@ -34,6 +34,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "itemdef.h"
 #include "tool.h"
 #include "content_cso.h"
+#include "sound.h"
+#include "nodedef.h"
 class Settings;
 struct ToolCapabilities;
 
@@ -1008,6 +1010,7 @@ class PlayerCAO : public ClientActiveObject
 	LocalPlayer *m_local_player;
 	float m_damage_visual_timer;
 	bool m_dead;
+	float m_step_distance_counter;
 
 public:
 	PlayerCAO(IGameDef *gamedef, ClientEnvironment *env):
@@ -1020,7 +1023,8 @@ class PlayerCAO : public ClientActiveObject
 		m_is_local_player(false),
 		m_local_player(NULL),
 		m_damage_visual_timer(0),
-		m_dead(false)
+		m_dead(false),
+		m_step_distance_counter(0)
 	{
 		if(gamedef == NULL)
 			ClientActiveObject::registerType(getType(), create);
@@ -1202,7 +1206,9 @@ class PlayerCAO : public ClientActiveObject
 
 	void step(float dtime, ClientEnvironment *env)
 	{
+		v3f lastpos = pos_translator.vect_show;
 		pos_translator.translate(dtime);
+		float moved = lastpos.getDistanceFrom(pos_translator.vect_show);
 		updateVisibility();
 		updateNodePos();
 
@@ -1212,6 +1218,18 @@ class PlayerCAO : public ClientActiveObject
 				updateTextures("");
 			}
 		}
+		
+		m_step_distance_counter += moved;
+		if(m_step_distance_counter > 1.5*BS){
+			m_step_distance_counter = 0;
+			if(!m_is_local_player){
+				INodeDefManager *ndef = m_gamedef->ndef();
+				v3s16 p = floatToInt(getPosition()+v3f(0,-0.5*BS, 0), BS);
+				MapNode n = m_env->getMap().getNodeNoEx(p);
+				SimpleSoundSpec spec = ndef->get(n).sound_footstep;
+				m_gamedef->sound()->playSoundAt(spec, false, getPosition());
+			}
+		}
 	}
 
 	void processMessage(const std::string &data)
diff --git a/src/scriptapi.cpp b/src/scriptapi.cpp
index 5ce5f3b29..3c7b9bb3e 100644
--- a/src/scriptapi.cpp
+++ b/src/scriptapi.cpp
@@ -2222,7 +2222,7 @@ class ObjectRef
 
 	static const char className[];
 	static const luaL_reg methods[];
-
+public:
 	static ObjectRef *checkobject(lua_State *L, int narg)
 	{
 		luaL_checktype(L, narg, LUA_TUSERDATA);
@@ -2236,7 +2236,7 @@ class ObjectRef
 		ServerActiveObject *co = ref->m_object;
 		return co;
 	}
-	
+private:
 	static LuaEntitySAO* getluaobject(ObjectRef *ref)
 	{
 		ServerActiveObject *obj = getobject(ref);
@@ -3134,10 +3134,6 @@ const luaL_reg EnvRef::methods[] = {
 	{0,0}
 };
 
-/*
-	Global functions
-*/
-
 class LuaABM : public ActiveBlockModifier
 {
 private:
@@ -3211,6 +3207,47 @@ class LuaABM : public ActiveBlockModifier
 	}
 };
 
+/*
+	ServerSoundParams
+*/
+
+static void read_server_sound_params(lua_State *L, int index,
+		ServerSoundParams &params)
+{
+	if(index < 0)
+		index = lua_gettop(L) + 1 + index;
+	// Clear
+	params = ServerSoundParams();
+	if(lua_istable(L, index)){
+		getfloatfield(L, index, "gain", params.gain);
+		getstringfield(L, index, "to_player", params.to_player);
+		lua_getfield(L, index, "pos");
+		if(!lua_isnil(L, -1)){
+			v3f p = read_v3f(L, -1)*BS;
+			params.pos = p;
+			params.type = ServerSoundParams::SSP_POSITIONAL;
+		}
+		lua_pop(L, 1);
+		lua_getfield(L, index, "object");
+		if(!lua_isnil(L, -1)){
+			ObjectRef *ref = ObjectRef::checkobject(L, -1);
+			ServerActiveObject *sao = ObjectRef::getobject(ref);
+			if(sao){
+				params.object = sao->getId();
+				params.type = ServerSoundParams::SSP_OBJECT;
+			}
+		}
+		lua_pop(L, 1);
+		params.max_hear_distance = BS*getfloatfield_default(L, index,
+				"max_hear_distance", params.max_hear_distance/BS);
+		getboolfield(L, index, "loop", params.loop);
+	}
+}
+
+/*
+	Global functions
+*/
+
 // debug(text)
 // Writes a line to dstream
 static int l_debug(lua_State *L)
@@ -3674,6 +3711,26 @@ static int l_get_worldpath(lua_State *L)
 	return 1;
 }
 
+// sound_play(spec, parameters)
+static int l_sound_play(lua_State *L)
+{
+	SimpleSoundSpec spec;
+	read_soundspec(L, 1, spec);
+	ServerSoundParams params;
+	read_server_sound_params(L, 2, params);
+	s32 handle = get_server(L)->playSound(spec, params);
+	lua_pushinteger(L, handle);
+	return 1;
+}
+
+// sound_stop(handle)
+static int l_sound_stop(lua_State *L)
+{
+	int handle = luaL_checkinteger(L, 1);
+	get_server(L)->stopSound(handle);
+	return 0;
+}
+
 static const struct luaL_Reg minetest_f [] = {
 	{"debug", l_debug},
 	{"log", l_log},
@@ -3691,6 +3748,8 @@ static const struct luaL_Reg minetest_f [] = {
 	{"get_current_modname", l_get_current_modname},
 	{"get_modpath", l_get_modpath},
 	{"get_worldpath", l_get_worldpath},
+	{"sound_play", l_sound_play},
+	{"sound_stop", l_sound_stop},
 	{NULL, NULL}
 };
 
diff --git a/src/server.cpp b/src/server.cpp
index e781f1284..745e55f83 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -3126,6 +3126,24 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id)
 					<<action<<std::endl;
 		}
 	}
+	else if(command == TOSERVER_REMOVED_SOUNDS)
+	{
+		std::string datastring((char*)&data[2], datasize-2);
+		std::istringstream is(datastring, std::ios_base::binary);
+
+		int num = readU16(is);
+		for(int k=0; k<num; k++){
+			s32 id = readS32(is);
+			std::map<s32, ServerPlayingSound>::iterator i =
+					m_playing_sounds.find(id);
+			if(i == m_playing_sounds.end())
+				continue;
+			ServerPlayingSound &psound = i->second;
+			psound.clients.erase(peer_id);
+			if(psound.clients.size() == 0)
+				m_playing_sounds.erase(i++);
+		}
+	}
 	else
 	{
 		infostream<<"Server::ProcessData(): Ignoring "
@@ -3575,6 +3593,107 @@ void Server::SendMovePlayer(Player *player)
 	m_con.Send(player->peer_id, 0, data, true);
 }
 
+s32 Server::playSound(const SimpleSoundSpec &spec,
+		const ServerSoundParams &params)
+{
+	// Find out initial position of sound
+	bool pos_exists = false;
+	v3f pos = params.getPos(m_env, &pos_exists);
+	// If position is not found while it should be, cancel sound
+	if(pos_exists != (params.type != ServerSoundParams::SSP_LOCAL))
+		return -1;
+	// Filter destination clients
+	std::set<RemoteClient*> dst_clients;
+	if(params.to_player != "")
+	{
+		Player *player = m_env->getPlayer(params.to_player.c_str());
+		if(!player){
+			infostream<<"Server::playSound: Player \""<<params.to_player
+					<<"\" not found"<<std::endl;
+			return -1;
+		}
+		if(player->peer_id == PEER_ID_INEXISTENT){
+			infostream<<"Server::playSound: Player \""<<params.to_player
+					<<"\" not connected"<<std::endl;
+			return -1;
+		}
+		RemoteClient *client = getClient(player->peer_id);
+		dst_clients.insert(client);
+	}
+	else
+	{
+		for(core::map<u16, RemoteClient*>::Iterator
+				i = m_clients.getIterator(); i.atEnd() == false; i++)
+		{
+			RemoteClient *client = i.getNode()->getValue();
+			Player *player = m_env->getPlayer(client->peer_id);
+			if(!player)
+				continue;
+			if(pos_exists){
+				if(player->getPosition().getDistanceFrom(pos) >
+						params.max_hear_distance)
+					continue;
+			}
+			dst_clients.insert(client);
+		}
+	}
+	if(dst_clients.size() == 0)
+		return -1;
+	// Create the sound
+	s32 id = m_next_sound_id++;
+	// The sound will exist as a reference in m_playing_sounds
+	m_playing_sounds[id] = ServerPlayingSound();
+	ServerPlayingSound &psound = m_playing_sounds[id];
+	psound.params = params;
+	for(std::set<RemoteClient*>::iterator i = dst_clients.begin();
+			i != dst_clients.end(); i++)
+		psound.clients.insert((*i)->peer_id);
+	// Create packet
+	std::ostringstream os(std::ios_base::binary);
+	writeU16(os, TOCLIENT_PLAY_SOUND);
+	writeS32(os, id);
+	os<<serializeString(spec.name);
+	writeF1000(os, spec.gain * params.gain);
+	writeU8(os, params.type);
+	writeV3F1000(os, pos);
+	writeU16(os, params.object);
+	writeU8(os, params.loop);
+	// Make data buffer
+	std::string s = os.str();
+	SharedBuffer<u8> data((u8*)s.c_str(), s.size());
+	// Send
+	for(std::set<RemoteClient*>::iterator i = dst_clients.begin();
+			i != dst_clients.end(); i++){
+		// Send as reliable
+		m_con.Send((*i)->peer_id, 0, data, true);
+	}
+	return id;
+}
+void Server::stopSound(s32 handle)
+{
+	// Get sound reference
+	std::map<s32, ServerPlayingSound>::iterator i =
+			m_playing_sounds.find(handle);
+	if(i == m_playing_sounds.end())
+		return;
+	ServerPlayingSound &psound = i->second;
+	// Create packet
+	std::ostringstream os(std::ios_base::binary);
+	writeU16(os, TOCLIENT_STOP_SOUND);
+	writeS32(os, handle);
+	// Make data buffer
+	std::string s = os.str();
+	SharedBuffer<u8> data((u8*)s.c_str(), s.size());
+	// Send
+	for(std::set<u16>::iterator i = psound.clients.begin();
+			i != psound.clients.end(); i++){
+		// Send as reliable
+		m_con.Send(*i, 0, data, true);
+	}
+	// Remove sound reference
+	m_playing_sounds.erase(i);
+}
+
 void Server::sendRemoveNode(v3s16 p, u16 ignore_id,
 	core::list<u16> *far_players, float far_d_nodes)
 {
@@ -4511,6 +4630,21 @@ void Server::handlePeerChange(PeerChange &c)
 				obj->m_known_by_count--;
 		}
 
+		/*
+			Clear references to playing sounds
+		*/
+		for(std::map<s32, ServerPlayingSound>::iterator
+				i = m_playing_sounds.begin();
+				i != m_playing_sounds.end();)
+		{
+			ServerPlayingSound &psound = i->second;
+			psound.clients.erase(c.peer_id);
+			if(psound.clients.size() == 0)
+				m_playing_sounds.erase(i++);
+			else
+				i++;
+		}
+
 		ServerRemotePlayer* player =
 				static_cast<ServerRemotePlayer*>(m_env->getPlayer(c.peer_id));
 
diff --git a/src/server.h b/src/server.h
index 3baeb433d..ae50af15b 100644
--- a/src/server.h
+++ b/src/server.h
@@ -35,6 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "mods.h"
 #include "inventorymanager.h"
 #include "subgame.h"
+#include "sound.h"
 struct LuaState;
 typedef struct lua_State lua_State;
 class IWritableItemDefManager;
@@ -274,6 +275,58 @@ struct TextureInformation
 	}
 };
 
+struct ServerSoundParams
+{
+	float gain;
+	std::string to_player;
+	enum Type{
+		SSP_LOCAL=0,
+		SSP_POSITIONAL=1,
+		SSP_OBJECT=2
+	} type;
+	v3f pos;
+	u16 object;
+	float max_hear_distance;
+	bool loop;
+
+	ServerSoundParams():
+		gain(1.0),
+		to_player(""),
+		type(SSP_LOCAL),
+		pos(0,0,0),
+		object(0),
+		max_hear_distance(32*BS),
+		loop(false)
+	{}
+	
+	v3f getPos(ServerEnvironment *env, bool *pos_exists) const
+	{
+		if(pos_exists) *pos_exists = false;
+		switch(type){
+		case SSP_LOCAL:
+			return v3f(0,0,0);
+		case SSP_POSITIONAL:
+			if(pos_exists) *pos_exists = true;
+			return pos;
+		case SSP_OBJECT: {
+			if(object == 0)
+				return v3f(0,0,0);
+			ServerActiveObject *sao = env->getActiveObject(object);
+			if(!sao)
+				return v3f(0,0,0);
+			if(pos_exists) *pos_exists = true;
+			return sao->getBasePosition(); }
+		}
+		return v3f(0,0,0);
+	}
+};
+
+struct ServerPlayingSound
+{
+	ServerSoundParams params;
+	std::set<u16> clients; // peer ids
+};
+
 class RemoteClient
 {
 public:
@@ -464,6 +517,11 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 	// Envlock and conlock should be locked when calling this
 	void SendMovePlayer(Player *player);
 	
+	// Returns -1 if failed, sound handle on success
+	// Envlock + conlock
+	s32 playSound(const SimpleSoundSpec &spec, const ServerSoundParams &params);
+	void stopSound(s32 handle);
+	
 	// Thread-safe
 	u64 getPlayerAuthPrivs(const std::string &name);
 	void setPlayerAuthPrivs(const std::string &name, u64 privs);
@@ -775,6 +833,12 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 	friend class RemoteClient;
 
 	std::map<std::string,TextureInformation> m_Textures;
+
+	/*
+		Sounds
+	*/
+	std::map<s32, ServerPlayingSound> m_playing_sounds;
+	s32 m_next_sound_id;
 };
 
 /*
diff --git a/src/sound.h b/src/sound.h
index 7f6e4141e..f9c1ea0ce 100644
--- a/src/sound.h
+++ b/src/sound.h
@@ -67,6 +67,8 @@ class ISoundManager
 	virtual int playSoundAt(const std::string &name, bool loop,
 			float volume, v3f pos) = 0;
 	virtual void stopSound(int sound) = 0;
+	virtual bool soundExists(int sound) = 0;
+	virtual void updateSoundPosition(int sound, v3f pos) = 0;
 
 	int playSound(const SimpleSoundSpec &spec, bool loop)
 		{ return playSound(spec.name, loop, spec.gain); }
@@ -87,6 +89,8 @@ class DummySoundManager: public ISoundManager
 	int playSoundAt(const std::string &name, bool loop,
 			float volume, v3f pos) {return 0;}
 	void stopSound(int sound) {}
+	bool soundExists(int sound) {return false;}
+	void updateSoundPosition(int sound, v3f pos) {}
 };
 
 // Global DummySoundManager singleton
diff --git a/src/sound_openal.cpp b/src/sound_openal.cpp
index edcb9e8d4..26ad6fa4c 100644
--- a/src/sound_openal.cpp
+++ b/src/sound_openal.cpp
@@ -482,6 +482,24 @@ class OpenALSoundManager: public ISoundManager
 		maintain();
 		deleteSound(sound);
 	}
+	bool soundExists(int sound)
+	{
+		maintain();
+		return (m_sounds_playing.count(sound) != 0);
+	}
+	void updateSoundPosition(int id, v3f pos)
+	{
+		std::map<int, PlayingSound*>::iterator i =
+				m_sounds_playing.find(id);
+		if(i == m_sounds_playing.end())
+			return;
+		PlayingSound *sound = i->second;
+
+		alSourcei(sound->source_id, AL_SOURCE_RELATIVE, false);
+		alSource3f(sound->source_id, AL_POSITION, pos.X, pos.Y, pos.Z);
+		alSource3f(sound->source_id, AL_VELOCITY, 0, 0, 0);
+		alSourcef(sound->source_id, AL_REFERENCE_DISTANCE, 30.0);
+	}
 };
 
 ISoundManager *createOpenALSoundManager(OnDemandSoundFetcher *fetcher)
-- 
GitLab