← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands/cleanup-soundhandler into lp:widelands

 

GunChleoc has proposed merging lp:~widelands-dev/widelands/cleanup-soundhandler into lp:widelands with lp:~widelands-dev/widelands/cleanup-rendertarget as a prerequisite.

Commit message:
Complete overhaul of the sound handler

- Extensive refactoring of the SoundHandler, FXSet and SongSet classes.
  Make it Widelands-agnostic.
- Categorize sound effects and overhaul the sound options to have
  separate sliders for music, UI sounds, chat messages, game messages
  and ambient sounds. Available both in-game and in the Fullscreen menu
  options.
- Memory saver: Only load sound effects when they are played for the
  first time and then keep them in memory until their category is
  cleared. Clear the ambient sounds when EditorGameBase gets destroyed
- Implement stereo position and distance for sounds emitted by map
  objects. This includes in-game messaages to the player that have been
  triggered by map objects.
- Remove the now obsolete commandline options - we only support
  --nosound now.
- Fix slider style for FSMenu sliders
- Website binaries no longer instantiate the global sound handler.
- Switch to ingame music set in netsetup and internet lobby, because
  just 1 song can get annoying while waiting for other players.
- The register_fx function now only uses the file path for identifying
  the files.
- Randomize the time last played on creation of fxset, to prevent
  ambient sound barrage when starting a new game.
- Rename g_soundhandler to g_sh for consistency with other global
  objects.

Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1590153 in widelands: "Add "play_sound" to critter programs"
  https://bugs.launchpad.net/widelands/+bug/1590153
  Bug #1660544 in widelands: "Add separate sound level for messages"
  https://bugs.launchpad.net/widelands/+bug/1660544
  Bug #1818494 in widelands: "Crash on "Reset zoom" bzr9005-201903031251 (Release)"
  https://bugs.launchpad.net/widelands/+bug/1818494

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands/cleanup-soundhandler/+merge/365001

This is for Build 21.

Enjoy your games with full stereo sound :)

Unfortunately, panning sound effects that are already playing while the map view is being moved does not seem to work with SDL_Mixer. We'll probably have to switch to a different backend for that (e.g. OpenAL), which is out of scope for this branch.
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands/cleanup-soundhandler into lp:widelands.
=== modified file 'data/campaigns/emp04.wmf/scripting/tribes/brewery1.lua'
--- data/campaigns/emp04.wmf/scripting/tribes/brewery1.lua	2018-08-22 06:48:57 +0000
+++ data/campaigns/emp04.wmf/scripting/tribes/brewery1.lua	2019-03-23 14:09:21 +0000
@@ -54,7 +54,7 @@
             "sleep=30000",
             "return=skipped unless economy needs beer",
             "consume=water:3 wheat",
-            "playsound=sound/empire beerbubble 180",
+            "playsound=sound/empire/beerbubble 180",
             "animate=working 30000",
             "produce=beer"
          }

=== modified file 'data/campaigns/emp04.wmf/scripting/tribes/brewery2.lua'
--- data/campaigns/emp04.wmf/scripting/tribes/brewery2.lua	2018-11-21 09:47:14 +0000
+++ data/campaigns/emp04.wmf/scripting/tribes/brewery2.lua	2019-03-23 14:09:21 +0000
@@ -53,7 +53,7 @@
             "sleep=30000",
             "return=skipped unless economy needs beer",
             "consume=water wheat",
-            "playsound=sound/empire beerbubble 180",
+            "playsound=sound/empire/beerbubble 180",
             "animate=working 30000",
             "produce=beer"
          }

=== modified file 'data/campaigns/emp04.wmf/scripting/tribes/mill1.lua'
--- data/campaigns/emp04.wmf/scripting/tribes/mill1.lua	2018-08-22 06:48:57 +0000
+++ data/campaigns/emp04.wmf/scripting/tribes/mill1.lua	2019-03-23 14:09:21 +0000
@@ -53,7 +53,7 @@
             "sleep=5000",
             "return=skipped unless economy needs flour",
             "consume=wheat:2",
-            "playsound=sound/mill mill_turning 240",
+            "playsound=sound/mill/mill_turning 240",
             "animate=working 10000",
             "produce=flour"
          }

=== modified file 'data/campaigns/emp04.wmf/scripting/tribes/mill2.lua'
--- data/campaigns/emp04.wmf/scripting/tribes/mill2.lua	2018-11-21 09:47:14 +0000
+++ data/campaigns/emp04.wmf/scripting/tribes/mill2.lua	2019-03-23 14:09:21 +0000
@@ -51,7 +51,7 @@
             "sleep=5000",
             "return=skipped unless economy needs flour",
             "consume=wheat",
-            "playsound=sound/mill mill_turning 240",
+            "playsound=sound/mill/mill_turning 240",
             "animate=working 10000",
             "produce=flour"
          }

=== modified file 'data/tribes/buildings/productionsites/atlanteans/gold_spinning_mill/init.lua'
--- data/tribes/buildings/productionsites/atlanteans/gold_spinning_mill/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/atlanteans/gold_spinning_mill/init.lua	2019-03-23 14:09:21 +0000
@@ -53,7 +53,7 @@
             "sleep=15000",
             "return=skipped unless economy needs gold_thread",
             "consume=gold",
-            "playsound=sound/atlanteans goldspin 192",
+            "playsound=sound/atlanteans/goldspin 192",
             "animate=working 25000",
             "produce=gold_thread"
          }

=== modified file 'data/tribes/buildings/productionsites/atlanteans/horsefarm/init.lua'
--- data/tribes/buildings/productionsites/atlanteans/horsefarm/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/atlanteans/horsefarm/init.lua	2019-03-23 14:09:21 +0000
@@ -55,7 +55,7 @@
             "sleep=15000",
             "return=skipped unless economy needs atlanteans_horse",
             "consume=corn water",
-            "playsound=sound/farm horse 192",
+            "playsound=sound/farm/horse 192",
             "animate=working 15000", -- Feeding cute little foals ;)
             "recruit=atlanteans_horse"
          }

=== modified file 'data/tribes/buildings/productionsites/atlanteans/mill/init.lua'
--- data/tribes/buildings/productionsites/atlanteans/mill/init.lua	2019-01-15 21:23:35 +0000
+++ data/tribes/buildings/productionsites/atlanteans/mill/init.lua	2019-03-23 14:09:21 +0000
@@ -68,7 +68,7 @@
             "return=skipped unless economy needs cornmeal",
             "sleep=3500",
             "consume=corn",
-            "playsound=sound/mill mill_turning 240",
+            "playsound=sound/mill/mill_turning 240",
             "animate=working 15000",
             "produce=cornmeal"
          }
@@ -81,7 +81,7 @@
             "return=skipped when site has corn and economy needs cornmeal and not economy needs blackroot_flour",
             "sleep=3500",
             "consume=blackroot",
-            "playsound=sound/mill mill_turning 240",
+            "playsound=sound/mill/mill_turning 240",
             "animate=working 15000",
             "produce=blackroot_flour"
          }

=== modified file 'data/tribes/buildings/productionsites/atlanteans/sawmill/init.lua'
--- data/tribes/buildings/productionsites/atlanteans/sawmill/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/atlanteans/sawmill/init.lua	2019-03-23 14:09:21 +0000
@@ -55,7 +55,7 @@
             "sleep=16500", -- Much faster than barbarians' wood hardener
             "return=skipped unless economy needs planks",
             "consume=log:2",
-            "playsound=sound/atlanteans/saw benchsaw 192",
+            "playsound=sound/atlanteans/saw/benchsaw 192",
             "animate=working 20000", -- Much faster than barbarians' wood hardener
             "produce=planks"
          }

=== modified file 'data/tribes/buildings/productionsites/atlanteans/smelting_works/init.lua'
--- data/tribes/buildings/productionsites/atlanteans/smelting_works/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/atlanteans/smelting_works/init.lua	2019-03-23 14:09:21 +0000
@@ -68,9 +68,9 @@
             "return=skipped unless economy needs iron",
             "consume=iron_ore coal",
             "sleep=25000",
-            "playsound=sound/metal fizzle 150",
+            "playsound=sound/metal/fizzle 150",
             "animate=working 35000",
-            "playsound=sound/metal ironping 80",
+            "playsound=sound/metal/ironping 80",
             "produce=iron"
          }
       },
@@ -81,9 +81,9 @@
             "return=skipped unless economy needs gold",
             "consume=gold_ore coal",
             "sleep=25000",
-            "playsound=sound/metal fizzle 150",
+            "playsound=sound/metal/fizzle 150",
             "animate=working 35000",
-            "playsound=sound/metal goldping 80",
+            "playsound=sound/metal/goldping 80",
             "produce=gold"
          }
       },

=== modified file 'data/tribes/buildings/productionsites/atlanteans/toolsmithy/init.lua'
--- data/tribes/buildings/productionsites/atlanteans/toolsmithy/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/atlanteans/toolsmithy/init.lua	2019-03-23 14:09:21 +0000
@@ -86,7 +86,7 @@
             "return=skipped unless economy needs bread_paddle",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=bread_paddle"
          }
@@ -98,7 +98,7 @@
             "return=skipped unless economy needs buckets",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=buckets"
          }
@@ -110,7 +110,7 @@
             "return=skipped unless economy needs fire_tongs",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=fire_tongs"
          }
@@ -122,7 +122,7 @@
             "return=skipped unless economy needs fishing_net",
             "consume=spidercloth:2",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=fishing_net"
          }
@@ -134,7 +134,7 @@
             "return=skipped unless economy needs hammer",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=hammer"
          }
@@ -146,7 +146,7 @@
             "return=skipped unless economy needs hook_pole",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=hook_pole"
          }
@@ -158,7 +158,7 @@
             "return=skipped unless economy needs hunting_bow",
             "consume=log spidercloth",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=hunting_bow"
          }
@@ -170,7 +170,7 @@
             "return=skipped unless economy needs milking_tongs",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=milking_tongs"
          }
@@ -182,7 +182,7 @@
             "return=skipped unless economy needs pick",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=pick"
          }
@@ -194,7 +194,7 @@
             "return=skipped unless economy needs saw",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=saw"
          }
@@ -206,7 +206,7 @@
             "return=skipped unless economy needs scythe",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=scythe"
          }
@@ -218,7 +218,7 @@
             "return=skipped unless economy needs shovel",
             "consume=iron log",
             "sleep=32000",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=shovel"
          }

=== modified file 'data/tribes/buildings/productionsites/atlanteans/weaponsmithy/init.lua'
--- data/tribes/buildings/productionsites/atlanteans/weaponsmithy/init.lua	2018-09-12 03:04:08 +0000
+++ data/tribes/buildings/productionsites/atlanteans/weaponsmithy/init.lua	2019-03-23 14:09:21 +0000
@@ -76,9 +76,9 @@
             "return=skipped unless economy needs trident_light",
             "consume=iron planks",
             "sleep=20000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 21000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=trident_light"
          }
@@ -91,9 +91,9 @@
             "return=skipped unless economy needs trident_long",
             "consume=iron coal planks",
             "sleep=32000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 36000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=trident_long"
          }
@@ -106,9 +106,9 @@
             "return=skipped unless economy needs trident_steel",
             "consume=iron:2 coal planks",
             "sleep=32000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 36000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=trident_steel"
          }
@@ -121,9 +121,9 @@
             "return=skipped unless economy needs trident_double",
             "consume=iron coal:2 planks gold",
             "sleep=32000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 36000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=trident_double"
          }
@@ -136,9 +136,9 @@
            "return=skipped unless economy needs trident_heavy_double",
             "consume=iron:2 coal:2 planks gold",
             "sleep=32000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 36000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=trident_heavy_double"
          }

=== modified file 'data/tribes/buildings/productionsites/atlanteans/weaving_mill/init.lua'
--- data/tribes/buildings/productionsites/atlanteans/weaving_mill/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/atlanteans/weaving_mill/init.lua	2019-03-23 14:09:21 +0000
@@ -71,7 +71,7 @@
             "return=skipped unless economy needs spidercloth",
             "consume=spider_silk",
             "sleep=20000",
-            "playsound=sound/mill weaving 120",
+            "playsound=sound/mill/weaving 120",
             "animate=working 20000",
             "produce=spidercloth"
          }
@@ -84,7 +84,7 @@
             "return=skipped unless economy needs tabard",
             "consume=spider_silk",
             "sleep=20000",
-            "playsound=sound/mill weaving 120",
+            "playsound=sound/mill/weaving 120",
             "animate=working 20000",
             "produce=tabard"
          }
@@ -97,7 +97,7 @@
             "return=skipped unless economy needs tabard_golden",
             "consume=spider_silk gold_thread",
             "sleep=20000",
-            "playsound=sound/mill weaving 120",
+            "playsound=sound/mill/weaving 120",
             "animate=working 20000",
             "produce=tabard_golden"
          }

=== modified file 'data/tribes/buildings/productionsites/barbarians/ax_workshop/init.lua'
--- data/tribes/buildings/productionsites/barbarians/ax_workshop/init.lua	2018-09-12 03:04:08 +0000
+++ data/tribes/buildings/productionsites/barbarians/ax_workshop/init.lua	2019-03-23 14:09:21 +0000
@@ -80,9 +80,9 @@
             "return=skipped unless economy needs ax",
             "consume=coal iron",
             "sleep=26000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 22000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=ax"
          }
@@ -95,9 +95,9 @@
             "return=skipped unless economy needs ax_sharp",
             "consume=coal iron:2",
             "sleep=26000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 22000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=ax_sharp"
          }
@@ -110,9 +110,9 @@
             "return=skipped unless economy needs ax_broad",
             "consume=coal:2 iron:2",
             "sleep=26000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 22000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=ax_broad"
          }

=== modified file 'data/tribes/buildings/productionsites/barbarians/big_inn/init.lua'
--- data/tribes/buildings/productionsites/barbarians/big_inn/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/barbarians/big_inn/init.lua	2019-03-23 14:09:21 +0000
@@ -73,7 +73,7 @@
             "return=skipped unless economy needs ration",
             "sleep=5000",
             "consume=barbarians_bread,fish,meat",
-            "playsound=sound/barbarians/taverns tavern 100",
+            "playsound=sound/barbarians/taverns/tavern 100",
             "animate=working 18000",
             "sleep=10000",
             "produce=ration"
@@ -86,7 +86,7 @@
             -- time total: 37
             "return=skipped unless economy needs snack",
             "consume=barbarians_bread fish,meat beer",
-            "playsound=sound/barbarians/taverns biginn 100",
+            "playsound=sound/barbarians/taverns/biginn 100",
             "animate=working 27000",
             "sleep=10000",
             "produce=snack"
@@ -99,7 +99,7 @@
             -- time total: 40
             "return=skipped unless economy needs meal",
             "consume=barbarians_bread fish,meat beer_strong",
-            "playsound=sound/barbarians/taverns biginn 100",
+            "playsound=sound/barbarians/taverns/biginn 100",
             "animate=working 30000",
             "sleep=10000",
             "produce=meal"

=== modified file 'data/tribes/buildings/productionsites/barbarians/cattlefarm/init.lua'
--- data/tribes/buildings/productionsites/barbarians/cattlefarm/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/barbarians/cattlefarm/init.lua	2019-03-23 14:09:21 +0000
@@ -55,7 +55,7 @@
             "sleep=15000",
             "return=skipped unless economy needs barbarians_ox",
             "consume=wheat water",
-            "playsound=sound/farm ox 192",
+            "playsound=sound/farm/ox 192",
             "animate=working 15000", -- Animation of feeding the cattle
             "recruit=barbarians_ox"
          }

=== modified file 'data/tribes/buildings/productionsites/barbarians/inn/init.lua'
--- data/tribes/buildings/productionsites/barbarians/inn/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/barbarians/inn/init.lua	2019-03-23 14:09:21 +0000
@@ -70,7 +70,7 @@
             "return=skipped unless economy needs ration",
             "sleep=5000",
             "consume=barbarians_bread,fish,meat",
-            "playsound=sound/barbarians/taverns inn 100",
+            "playsound=sound/barbarians/taverns/inn 100",
             "animate=working 18000",
             "sleep=10000",
             "produce=ration"
@@ -83,7 +83,7 @@
             -- time total: 37
             "return=skipped unless economy needs snack",
             "consume=barbarians_bread fish,meat beer",
-            "playsound=sound/barbarians/taverns inn 100",
+            "playsound=sound/barbarians/taverns/inn 100",
             "animate=working 27000",
             "sleep=10000",
             "produce=snack"

=== modified file 'data/tribes/buildings/productionsites/barbarians/lime_kiln/init.lua'
--- data/tribes/buildings/productionsites/barbarians/lime_kiln/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/barbarians/lime_kiln/init.lua	2019-03-23 14:09:21 +0000
@@ -57,9 +57,9 @@
             "sleep=50000",
             "return=skipped unless economy needs grout",
             "consume=coal granite:2 water:2",
-            "playsound=sound/barbarians stonegrind 100",
+            "playsound=sound/barbarians/stonegrind 100",
             "animate=working 29000",
-            "playsound=sound/barbarians mortar 80",
+            "playsound=sound/barbarians/mortar 80",
             "sleep=3000",
             "produce=grout:2"
          }

=== modified file 'data/tribes/buildings/productionsites/barbarians/metal_workshop/init.lua'
--- data/tribes/buildings/productionsites/barbarians/metal_workshop/init.lua	2018-09-06 08:21:35 +0000
+++ data/tribes/buildings/productionsites/barbarians/metal_workshop/init.lua	2019-03-23 14:09:21 +0000
@@ -96,7 +96,7 @@
             "return=skipped unless economy needs bread_paddle",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=bread_paddle"
          }
@@ -108,7 +108,7 @@
             "return=skipped unless economy needs felling_ax",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=felling_ax"
          }
@@ -120,7 +120,7 @@
             "return=skipped unless economy needs fire_tongs",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=fire_tongs"
          }
@@ -132,7 +132,7 @@
             "return=skipped unless economy needs fishing_rod",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=fishing_rod"
          }
@@ -144,7 +144,7 @@
             "return=skipped unless economy needs hammer",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=hammer"
          }
@@ -156,7 +156,7 @@
             "return=skipped unless economy needs hunting_spear",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=hunting_spear"
          }
@@ -168,7 +168,7 @@
             "return=skipped unless economy needs kitchen_tools",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=kitchen_tools"
          }
@@ -180,7 +180,7 @@
             "return=skipped unless economy needs pick",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=pick"
          }
@@ -192,7 +192,7 @@
             "return=skipped unless economy needs scythe",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=scythe"
          }
@@ -204,7 +204,7 @@
             "return=skipped unless economy needs shovel",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=shovel"
          }

=== modified file 'data/tribes/buildings/productionsites/barbarians/smelting_works/init.lua'
--- data/tribes/buildings/productionsites/barbarians/smelting_works/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/barbarians/smelting_works/init.lua	2019-03-23 14:09:21 +0000
@@ -72,9 +72,9 @@
             "return=skipped unless economy needs iron",
             "consume=coal iron_ore",
             "sleep=32000",
-            "playsound=sound/metal furnace 192",
+            "playsound=sound/metal/furnace 192",
             "animate=working 35000",
-            "playsound=sound/metal ironping 80",
+            "playsound=sound/metal/ironping 80",
             "produce=iron"
          }
       },
@@ -85,9 +85,9 @@
             "return=skipped unless economy needs gold",
             "consume=coal gold_ore",
             "sleep=32000",
-            "playsound=sound/metal furnace 192",
+            "playsound=sound/metal/furnace 192",
             "animate=working 35000",
-            "playsound=sound/metal goldping 80",
+            "playsound=sound/metal/goldping 80",
             "produce=gold"
          }
       },

=== modified file 'data/tribes/buildings/productionsites/barbarians/tavern/init.lua'
--- data/tribes/buildings/productionsites/barbarians/tavern/init.lua	2018-08-11 14:26:39 +0000
+++ data/tribes/buildings/productionsites/barbarians/tavern/init.lua	2019-03-23 14:09:21 +0000
@@ -64,7 +64,7 @@
             "return=skipped unless economy needs ration",
             "sleep=5000",
             "consume=barbarians_bread,fish,meat",
-            "playsound=sound/barbarians/taverns tavern 100",
+            "playsound=sound/barbarians/taverns/tavern 100",
             "animate=working 18000",
             "sleep=10000",
             "produce=ration"

=== modified file 'data/tribes/buildings/productionsites/barbarians/warmill/init.lua'
--- data/tribes/buildings/productionsites/barbarians/warmill/init.lua	2018-09-12 03:04:08 +0000
+++ data/tribes/buildings/productionsites/barbarians/warmill/init.lua	2019-03-23 14:09:21 +0000
@@ -87,9 +87,9 @@
             "return=skipped unless economy needs ax",
             "consume=coal iron",
             "sleep=26000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 22000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=ax"
          }
@@ -102,9 +102,9 @@
             "return=skipped unless economy needs ax_sharp",
             "consume=coal iron:2",
             "sleep=26000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 22000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=ax_sharp"
          }
@@ -117,9 +117,9 @@
             "return=skipped unless economy needs ax_broad",
             "consume=coal:2 iron:2",
             "sleep=26000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 22000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=ax_broad"
          }
@@ -132,9 +132,9 @@
             "return=skipped unless economy needs ax_bronze",
             "consume=coal:2 iron:2",
             "sleep=26000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 22000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=ax_bronze"
          }
@@ -147,9 +147,9 @@
             "return=skipped unless economy needs ax_battle",
             "consume=coal gold iron:2",
             "sleep=26000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 22000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=ax_battle"
          }
@@ -162,9 +162,9 @@
             "return=skipped unless economy needs ax_warriors",
             "consume=coal:2 gold:2 iron:2",
             "sleep=26000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 22000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=ax_warriors"
          }

=== modified file 'data/tribes/buildings/productionsites/barbarians/weaving_mill/init.lua'
--- data/tribes/buildings/productionsites/barbarians/weaving_mill/init.lua	2017-11-18 21:23:09 +0000
+++ data/tribes/buildings/productionsites/barbarians/weaving_mill/init.lua	2019-03-23 14:09:21 +0000
@@ -59,7 +59,7 @@
             "checkmap=seafaring",
             "return=skipped unless economy needs cloth",
             "consume=thatch_reed",
-            "playsound=sound/barbarians weaver 120",
+            "playsound=sound/barbarians/weaver 120",
             "animate=working 20000",
             "produce=cloth"
          }

=== modified file 'data/tribes/buildings/productionsites/barbarians/wood_hardener/init.lua'
--- data/tribes/buildings/productionsites/barbarians/wood_hardener/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/barbarians/wood_hardener/init.lua	2019-03-23 14:09:21 +0000
@@ -62,7 +62,7 @@
             "sleep=43000",
             "return=skipped unless economy needs blackwood",
             "consume=log:2",
-            "playsound=sound/barbarians blackwood 80",
+            "playsound=sound/barbarians/blackwood 80",
             "animate=working 24000",
             "produce=blackwood"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/brewery/init.lua'
--- data/tribes/buildings/productionsites/empire/brewery/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/empire/brewery/init.lua	2019-03-23 14:09:21 +0000
@@ -56,7 +56,7 @@
             "sleep=30000",
             "return=skipped unless economy needs beer",
             "consume=water wheat",
-            "playsound=sound/empire beerbubble 180",
+            "playsound=sound/empire/beerbubble 180",
             "animate=working 30000",
             "produce=beer"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/donkeyfarm/init.lua'
--- data/tribes/buildings/productionsites/empire/donkeyfarm/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/empire/donkeyfarm/init.lua	2019-03-23 14:09:21 +0000
@@ -55,7 +55,7 @@
             "sleep=15000",
             "return=skipped unless economy needs empire_donkey",
             "consume=wheat water",
-            "playsound=sound/farm donkey 192",
+            "playsound=sound/farm/donkey 192",
             "animate=working 15000", -- Feeding cute little baby donkeys ;)
             "recruit=empire_donkey"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/inn/init.lua'
--- data/tribes/buildings/productionsites/empire/inn/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/empire/inn/init.lua	2019-03-23 14:09:21 +0000
@@ -66,7 +66,7 @@
             "return=skipped unless economy needs ration",
             "sleep=5000",
             "consume=empire_bread,fish,meat",
-            "playsound=sound/empire/taverns ration 100",
+            "playsound=sound/empire/taverns/ration 100",
             "animate=working 18000",
             "sleep=10000",
             "produce=ration"
@@ -79,7 +79,7 @@
             -- time total: 40
             "return=skipped unless economy needs meal",
             "consume=empire_bread fish,meat",
-            "playsound=sound/empire/taverns meal 100",
+            "playsound=sound/empire/taverns/meal 100",
             "animate=working 30000",
             "sleep=10000",
             "produce=meal"

=== modified file 'data/tribes/buildings/productionsites/empire/mill/init.lua'
--- data/tribes/buildings/productionsites/empire/mill/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/empire/mill/init.lua	2019-03-23 14:09:21 +0000
@@ -55,7 +55,7 @@
             "sleep=5000",
             "return=skipped unless economy needs flour",
             "consume=wheat",
-            "playsound=sound/mill mill_turning 240",
+            "playsound=sound/mill/mill_turning 240",
             "animate=working 10000",
             "produce=flour"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/piggery/init.lua'
--- data/tribes/buildings/productionsites/empire/piggery/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/empire/piggery/init.lua	2019-03-23 14:09:21 +0000
@@ -56,7 +56,7 @@
             "sleep=25000",
             "return=skipped unless economy needs meat",
             "consume=water wheat",
-            "playsound=sound/farm farm_animal 180",
+            "playsound=sound/farm/farm_animal 180",
             "animate=working 30000",
             "produce=meat"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/sawmill/init.lua'
--- data/tribes/buildings/productionsites/empire/sawmill/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/empire/sawmill/init.lua	2019-03-23 14:09:21 +0000
@@ -55,7 +55,7 @@
             "sleep=16500", -- Much faster than barbarians' wood hardener
             "return=skipped unless economy needs planks",
             "consume=log:2",
-            "playsound=sound/sawmill sawmill 180",
+            "playsound=sound/sawmill/sawmill 180",
             "animate=working 20000", -- Much faster than barbarians' wood hardener
             "produce=planks"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/sheepfarm/init.lua'
--- data/tribes/buildings/productionsites/empire/sheepfarm/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/empire/sheepfarm/init.lua	2019-03-23 14:09:21 +0000
@@ -56,7 +56,7 @@
             "sleep=25000",
             "return=skipped unless economy needs wool",
             "consume=water wheat",
-            "playsound=sound/farm sheep 192",
+            "playsound=sound/farm/sheep 192",
             "animate=working 30000",
             "produce=wool"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/smelting_works/init.lua'
--- data/tribes/buildings/productionsites/empire/smelting_works/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/empire/smelting_works/init.lua	2019-03-23 14:09:21 +0000
@@ -73,9 +73,9 @@
             "return=skipped unless economy needs iron",
             "consume=iron_ore coal",
             "sleep=25000",
-            "playsound=sound/metal fizzle 150",
+            "playsound=sound/metal/fizzle 150",
             "animate=working 35000",
-            "playsound=sound/metal ironping 80",
+            "playsound=sound/metal/ironping 80",
             "produce=iron"
          }
       },
@@ -86,9 +86,9 @@
             "return=skipped unless economy needs gold",
             "consume=gold_ore coal",
             "sleep=25000",
-            "playsound=sound/metal fizzle 150",
+            "playsound=sound/metal/fizzle 150",
             "animate=working 35000",
-            "playsound=sound/metal goldping 80",
+            "playsound=sound/metal/goldping 80",
             "produce=gold"
          }
       },

=== modified file 'data/tribes/buildings/productionsites/empire/stonemasons_house/init.lua'
--- data/tribes/buildings/productionsites/empire/stonemasons_house/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/empire/stonemasons_house/init.lua	2019-03-23 14:09:21 +0000
@@ -56,7 +56,7 @@
             "sleep=50000",
             "return=skipped unless economy needs marble_column",
             "consume=marble:2",
-            "playsound=sound/stonecutting stonemason 192",
+            "playsound=sound/stonecutting/stonemason 192",
             "animate=working 32000",
             "produce=marble_column"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/tavern/init.lua'
--- data/tribes/buildings/productionsites/empire/tavern/init.lua	2018-08-11 14:26:39 +0000
+++ data/tribes/buildings/productionsites/empire/tavern/init.lua	2019-03-23 14:09:21 +0000
@@ -59,7 +59,7 @@
             "return=skipped unless economy needs ration",
             "sleep=5000",
             "consume=empire_bread,fish,meat",
-            "playsound=sound/empire/taverns ration 100",
+            "playsound=sound/empire/taverns/ration 100",
             "animate=working 18000",
             "sleep=10000",
             "produce=ration"

=== modified file 'data/tribes/buildings/productionsites/empire/toolsmithy/init.lua'
--- data/tribes/buildings/productionsites/empire/toolsmithy/init.lua	2018-09-06 08:21:35 +0000
+++ data/tribes/buildings/productionsites/empire/toolsmithy/init.lua	2019-03-23 14:09:21 +0000
@@ -85,7 +85,7 @@
             "return=skipped unless economy needs felling_ax",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=felling_ax"
          }
@@ -97,7 +97,7 @@
             "return=skipped unless economy needs basket",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=basket"
          }
@@ -109,7 +109,7 @@
             "return=skipped unless economy needs bread_paddle",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=bread_paddle"
          }
@@ -121,7 +121,7 @@
             "return=skipped unless economy needs fire_tongs",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=fire_tongs"
          }
@@ -133,7 +133,7 @@
             "return=skipped unless economy needs fishing_rod",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=fishing_rod"
          }
@@ -145,7 +145,7 @@
             "return=skipped unless economy needs hammer",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=hammer"
          }
@@ -157,7 +157,7 @@
             "return=skipped unless economy needs hunting_spear",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=hunting_spear"
          }
@@ -169,7 +169,7 @@
             "return=skipped unless economy needs kitchen_tools",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=kitchen_tools"
          }
@@ -181,7 +181,7 @@
             "return=skipped unless economy needs pick",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=pick"
          }
@@ -193,7 +193,7 @@
             "return=skipped unless economy needs saw",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=saw"
          }
@@ -205,7 +205,7 @@
             "return=skipped unless economy needs scythe",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=scythe"
          }
@@ -217,7 +217,7 @@
             "return=skipped unless economy needs shovel",
             "sleep=32000",
             "consume=iron log",
-            "playsound=sound/smiths toolsmith 192",
+            "playsound=sound/smiths/toolsmith 192",
             "animate=working 35000",
             "produce=shovel"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/weaponsmithy/init.lua'
--- data/tribes/buildings/productionsites/empire/weaponsmithy/init.lua	2018-09-12 03:04:08 +0000
+++ data/tribes/buildings/productionsites/empire/weaponsmithy/init.lua	2019-03-23 14:09:21 +0000
@@ -81,9 +81,9 @@
             "return=skipped unless economy needs spear_wooden",
             "consume=planks",
             "sleep=20000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 21000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=spear_wooden"
          }
@@ -96,9 +96,9 @@
             "return=skipped unless economy needs spear",
             "consume=coal iron planks",
             "sleep=32000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 36000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=spear"
          }
@@ -111,9 +111,9 @@
             "return=skipped unless economy needs spear_advanced",
             "consume=coal iron:2 planks",
             "sleep=32000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 36000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=spear_advanced"
          }
@@ -126,9 +126,9 @@
             "return=skipped unless economy needs spear_heavy",
             "consume=coal:2 gold iron planks",
             "sleep=32000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 36000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=spear_heavy"
          }
@@ -141,9 +141,9 @@
             "return=skipped unless economy needs spear_war",
             "consume=coal:2 gold iron:2 planks",
             "sleep=32000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 36000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=spear_war"
          }

=== modified file 'data/tribes/buildings/productionsites/empire/weaving_mill/init.lua'
--- data/tribes/buildings/productionsites/empire/weaving_mill/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/empire/weaving_mill/init.lua	2019-03-23 14:09:21 +0000
@@ -63,7 +63,7 @@
             "return=skipped unless economy needs cloth",
             "consume=wool",
             "sleep=15000",
-            "playsound=sound/mill weaving 120",
+            "playsound=sound/mill/weaving 120",
             "animate=working 15000", -- Unsure of balancing CW
             "sleep=5000",
             "produce=cloth"

=== modified file 'data/tribes/buildings/productionsites/empire/winery/init.lua'
--- data/tribes/buildings/productionsites/empire/winery/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/buildings/productionsites/empire/winery/init.lua	2019-03-23 14:09:21 +0000
@@ -58,7 +58,7 @@
             -- Grapes are only needed for wine, so no need to check if wine is needed
             "sleep=30000",
             "consume=grape:2",
-            "playsound=sound/empire winebubble 180",
+            "playsound=sound/empire/winebubble 180",
             "animate=working 30000",
             "produce=wine"
          }

=== modified file 'data/tribes/buildings/productionsites/frisians/armor_smithy_large/init.lua'
--- data/tribes/buildings/productionsites/frisians/armor_smithy_large/init.lua	2018-09-10 12:32:56 +0000
+++ data/tribes/buildings/productionsites/frisians/armor_smithy_large/init.lua	2019-03-23 14:09:21 +0000
@@ -78,9 +78,9 @@
             "return=skipped unless economy needs sword_broad",
             "consume=coal iron:2 gold",
             "sleep=24000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 24000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=sword_broad"
          },
@@ -93,9 +93,9 @@
             "return=skipped unless economy needs sword_double",
             "consume=coal:2 iron:2 gold",
             "sleep=24000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 24000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=sword_double"
          },

=== modified file 'data/tribes/buildings/productionsites/frisians/armor_smithy_small/init.lua'
--- data/tribes/buildings/productionsites/frisians/armor_smithy_small/init.lua	2018-09-16 13:19:53 +0000
+++ data/tribes/buildings/productionsites/frisians/armor_smithy_small/init.lua	2019-03-23 14:09:21 +0000
@@ -77,9 +77,9 @@
             "return=skipped unless economy needs sword_short",
             "consume=coal iron",
             "sleep=24000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 24000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=sword_short"
          },
@@ -92,9 +92,9 @@
             "return=skipped unless economy needs sword_long",
             "consume=coal iron:2",
             "sleep=24000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 24000",
-            "playsound=sound/smiths sharpening 120",
+            "playsound=sound/smiths/sharpening 120",
             "sleep=9000",
             "produce=sword_long"
          },
@@ -107,7 +107,7 @@
             "return=skipped unless economy needs helmet",
             "consume=coal iron",
             "sleep=30000",
-            "playsound=sound/smiths smith 192",
+            "playsound=sound/smiths/smith 192",
             "animate=working 37000",
             "produce=helmet"
          },

=== modified file 'data/tribes/workers/atlanteans/builder/init.lua'
--- data/tribes/workers/atlanteans/builder/init.lua	2017-02-12 09:10:57 +0000
+++ data/tribes/workers/atlanteans/builder/init.lua	2019-03-23 14:09:21 +0000
@@ -9,8 +9,8 @@
    work = {
       pictures = path.list_files(dirname .. "work_??.png"),
       sound_effect = {
-            directory = "sound/hammering",
-            name = "hammering",
+         path = "sound/hammering/hammering",
+         priority = 64
       },
       hotspot = { 6, 22 },
       fps=10,

=== modified file 'data/tribes/workers/atlanteans/farmer/init.lua'
--- data/tribes/workers/atlanteans/farmer/init.lua	2018-02-28 09:38:13 +0000
+++ data/tribes/workers/atlanteans/farmer/init.lua	2019-03-23 14:09:21 +0000
@@ -51,7 +51,7 @@
       harvest = {
          "findobject=attrib:ripe_corn radius:2",
          "walk=object",
-         "playsound=sound/farm scythe 220",
+         "playsound=sound/farm/scythe 220",
          "animate=harvesting 10000",
          "callobject=harvest",
          "animate=gathering 4000",

=== modified file 'data/tribes/workers/atlanteans/fisher/init.lua'
--- data/tribes/workers/atlanteans/fisher/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/workers/atlanteans/fisher/init.lua	2019-03-23 14:09:21 +0000
@@ -33,10 +33,10 @@
       fish = {
          "findspace=size:any radius:7 resource:fish",
          "walk=coords",
-         "playsound=sound/fisher fisher_throw_net 192",
+         "playsound=sound/fisher/fisher_throw_net 192",
          "mine=fish 1",
          "animate=fishing 3000",
-         "playsound=sound/fisher fisher_pull_net 192",
+         "playsound=sound/fisher/fisher_pull_net 192",
          "createware=fish",
          "return"
       }

=== modified file 'data/tribes/workers/atlanteans/geologist/init.lua'
--- data/tribes/workers/atlanteans/geologist/init.lua	2018-07-08 18:32:58 +0000
+++ data/tribes/workers/atlanteans/geologist/init.lua	2019-03-23 14:09:21 +0000
@@ -41,7 +41,7 @@
       search = {
          "animate=hacking 5000",
          "animate=idle 2000",
-         "playsound=sound/hammering geologist_hammer 192",
+         "playsound=sound/hammering/geologist_hammer 192",
          "animate=hacking 3000",
          "findresources"
       }

=== modified file 'data/tribes/workers/atlanteans/shipwright/init.lua'
--- data/tribes/workers/atlanteans/shipwright/init.lua	2018-02-28 09:38:13 +0000
+++ data/tribes/workers/atlanteans/shipwright/init.lua	2019-03-23 14:09:21 +0000
@@ -4,8 +4,8 @@
    idle = {
       pictures = path.list_files(dirname .. "idle_??.png"),
       sound_effect = {
-            directory = "sound/hammering",
-            name = "hammering",
+         path = "sound/hammering/hammering",
+         priority = 64
       },
       hotspot = { 12, 28 },
       fps = 10
@@ -33,7 +33,7 @@
       buildship = {
          "walk=object-or-coords",
          "plant=attrib:shipconstruction unless object",
-         "playsound=sound/sawmill sawmill 230",
+         "playsound=sound/sawmill/sawmill 230",
          "animate=idle 500",
          "construct",
          "animate=idle 5000",

=== modified file 'data/tribes/workers/atlanteans/stonecutter/init.lua'
--- data/tribes/workers/atlanteans/stonecutter/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/workers/atlanteans/stonecutter/init.lua	2019-03-23 14:09:21 +0000
@@ -33,7 +33,7 @@
       cut_granite = {
          "findobject=attrib:rocks radius:6",
          "walk=object",
-         "playsound=sound/atlanteans/cutting stonecutter 192",
+         "playsound=sound/atlanteans/cutting/stonecutter 192",
          "animate=hacking 12000",
          "callobject=shrink",
          "createware=granite",

=== modified file 'data/tribes/workers/atlanteans/woodcutter/init.lua'
--- data/tribes/workers/atlanteans/woodcutter/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/workers/atlanteans/woodcutter/init.lua	2019-03-23 14:09:21 +0000
@@ -33,9 +33,9 @@
       harvest = {
          "findobject=attrib:tree radius:10",
          "walk=object",
-         "playsound=sound/atlanteans/saw sawing 230",
+         "playsound=sound/atlanteans/saw/sawing 230",
          "animate=sawing 10000",
-         "playsound=sound/woodcutting tree-falling 130",
+         "playsound=sound/woodcutting/tree-falling 130",
          "callobject=fall",
          "animate=idle 2000",
          "createware=log",

=== modified file 'data/tribes/workers/barbarians/builder/init.lua'
--- data/tribes/workers/barbarians/builder/init.lua	2017-02-12 09:10:57 +0000
+++ data/tribes/workers/barbarians/builder/init.lua	2019-03-23 14:09:21 +0000
@@ -9,8 +9,8 @@
    work = {
       pictures = path.list_files(dirname .. "work_??.png"),
       sound_effect = {
-            directory = "sound/hammering",
-            name = "hammering",
+         path = "sound/hammering/hammering",
+         priority = 64
       },
       hotspot = { 10, 22 },
       fps = 10

=== modified file 'data/tribes/workers/barbarians/farmer/init.lua'
--- data/tribes/workers/barbarians/farmer/init.lua	2018-02-28 09:38:13 +0000
+++ data/tribes/workers/barbarians/farmer/init.lua	2019-03-23 14:09:21 +0000
@@ -51,7 +51,7 @@
       harvest = {
          "findobject=attrib:ripe_wheat radius:2",
          "walk=object",
-         "playsound=sound/farm scythe 220",
+         "playsound=sound/farm/scythe 220",
          "animate=harvesting 10000",
          "callobject=harvest",
          "animate=gathering 4000",

=== modified file 'data/tribes/workers/barbarians/fisher/init.lua'
--- data/tribes/workers/barbarians/fisher/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/workers/barbarians/fisher/init.lua	2019-03-23 14:09:21 +0000
@@ -33,10 +33,10 @@
       fish = {
          "findspace=size:any radius:7 resource:fish",
          "walk=coords",
-         "playsound=sound/fisher fisher_throw_net 192",
+         "playsound=sound/fisher/fisher_throw_net 192",
          "mine=fish 1",
          "animate=fishing 3000",
-         "playsound=sound/fisher fisher_pull_net 192",
+         "playsound=sound/fisher/fisher_pull_net 192",
          "createware=fish",
          "return"
       }

=== modified file 'data/tribes/workers/barbarians/geologist/init.lua'
--- data/tribes/workers/barbarians/geologist/init.lua	2017-11-18 21:08:26 +0000
+++ data/tribes/workers/barbarians/geologist/init.lua	2019-03-23 14:09:21 +0000
@@ -41,7 +41,7 @@
       search = {
          "animate=hacking 5000",
          "animate=idle 2000",
-         "playsound=sound/hammering geologist_hammer 192",
+         "playsound=sound/hammering/geologist_hammer 192",
          "animate=hacking 3000",
          "findresources"
       }

=== modified file 'data/tribes/workers/barbarians/lumberjack/init.lua'
--- data/tribes/workers/barbarians/lumberjack/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/workers/barbarians/lumberjack/init.lua	2019-03-23 14:09:21 +0000
@@ -34,10 +34,10 @@
       harvest = {
          "findobject=attrib:tree radius:10",
          "walk=object",
-         "playsound=sound/woodcutting woodcutting 255",
+         "playsound=sound/woodcutting/woodcutting 255",
          "animate=hacking 10000",
          -- "playsound=sound/spoken timber 192",
-         "playsound=sound/woodcutting tree-falling 130",
+         "playsound=sound/woodcutting/tree-falling 130",
          "callobject=fall",
          "animate=idle 2000",
          "createware=log",

=== modified file 'data/tribes/workers/barbarians/shipwright/init.lua'
--- data/tribes/workers/barbarians/shipwright/init.lua	2018-02-28 09:38:13 +0000
+++ data/tribes/workers/barbarians/shipwright/init.lua	2019-03-23 14:09:21 +0000
@@ -8,8 +8,8 @@
    work = {
       pictures = path.list_files(dirname .. "work_??.png"),
       sound_effect = {
-            directory = "sound/hammering",
-            name = "hammering",
+         path = "sound/hammering/hammering",
+         priority = 64
       },
       hotspot = { 11, 26 },
       fps = 10
@@ -37,7 +37,7 @@
       buildship = {
          "walk=object-or-coords",
          "plant=attrib:shipconstruction unless object",
-         "playsound=sound/sawmill sawmill 230",
+         "playsound=sound/sawmill/sawmill 230",
          "animate=work 500",
          "construct",
          "animate=work 5000",

=== modified file 'data/tribes/workers/barbarians/stonemason/init.lua'
--- data/tribes/workers/barbarians/stonemason/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/workers/barbarians/stonemason/init.lua	2019-03-23 14:09:21 +0000
@@ -34,7 +34,7 @@
       cut_granite = {
          "findobject=attrib:rocks radius:6",
          "walk=object",
-         "playsound=sound/stonecutting stonecutter 192",
+         "playsound=sound/stonecutting/stonecutter 192",
          "animate=hacking 10000",
          "callobject=shrink",
          "createware=granite",

=== modified file 'data/tribes/workers/empire/builder/init.lua'
--- data/tribes/workers/empire/builder/init.lua	2017-02-12 09:10:57 +0000
+++ data/tribes/workers/empire/builder/init.lua	2019-03-23 14:09:21 +0000
@@ -9,8 +9,8 @@
    work = {
       pictures = path.list_files(dirname .. "work_??.png"),
       sound_effect = {
-            directory = "sound/hammering",
-            name = "hammering",
+         path = "sound/hammering/hammering",
+         priority = 64
       },
       hotspot = { 11, 21 },
       fps = 10

=== modified file 'data/tribes/workers/empire/farmer/init.lua'
--- data/tribes/workers/empire/farmer/init.lua	2018-02-28 09:38:13 +0000
+++ data/tribes/workers/empire/farmer/init.lua	2019-03-23 14:09:21 +0000
@@ -51,7 +51,7 @@
       harvest = {
          "findobject=attrib:ripe_wheat radius:2",
          "walk=object",
-         "playsound=sound/farm scythe 220",
+         "playsound=sound/farm/scythe 220",
          "animate=harvesting 10000",
          "callobject=harvest",
          "animate=gathering 4000",

=== modified file 'data/tribes/workers/empire/fisher/init.lua'
--- data/tribes/workers/empire/fisher/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/workers/empire/fisher/init.lua	2019-03-23 14:09:21 +0000
@@ -33,10 +33,10 @@
       fish = {
          "findspace=size:any radius:7 resource:fish",
          "walk=coords",
-         "playsound=sound/fisher fisher_throw_net 192",
+         "playsound=sound/fisher/fisher_throw_net 192",
          "mine=fish 1",
          "animate=fishing 3000", -- Play a fishing animation
-         "playsound=sound/fisher fisher_pull_net 192",
+         "playsound=sound/fisher/fisher_pull_net 192",
          "createware=fish",
          "return"
       }

=== modified file 'data/tribes/workers/empire/geologist/init.lua'
--- data/tribes/workers/empire/geologist/init.lua	2017-11-18 21:08:26 +0000
+++ data/tribes/workers/empire/geologist/init.lua	2019-03-23 14:09:21 +0000
@@ -41,7 +41,7 @@
       search = {
          "animate=hacking 5000",
          "animate=idle 2000",
-         "playsound=sound/hammering geologist_hammer 192",
+         "playsound=sound/hammering/geologist_hammer 192",
          "animate=hacking 3000",
          "findresources"
       }

=== modified file 'data/tribes/workers/empire/lumberjack/init.lua'
--- data/tribes/workers/empire/lumberjack/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/workers/empire/lumberjack/init.lua	2019-03-23 14:09:21 +0000
@@ -34,10 +34,10 @@
       harvest = {
          "findobject=attrib:tree radius:10",
          "walk=object",
-         "playsound=sound/woodcutting fast_woodcutting 250",
+         "playsound=sound/woodcutting/fast_woodcutting 250",
          "animate=hacking 10000",
-         --  "playsound=sound/spoken timber 156",
-         "playsound=sound/woodcutting tree-falling 130",
+         --  "playsound=sound/spoken/timber 156",
+         "playsound=sound/woodcutting/tree-falling 130",
          "callobject=fall",
          "animate=idle 2000",
          "createware=log",

=== modified file 'data/tribes/workers/empire/shipwright/init.lua'
--- data/tribes/workers/empire/shipwright/init.lua	2018-02-28 09:38:13 +0000
+++ data/tribes/workers/empire/shipwright/init.lua	2019-03-23 14:09:21 +0000
@@ -8,8 +8,8 @@
    work = {
       pictures = path.list_files(dirname .. "work_??.png"),
       sound_effect = {
-            directory = "sound/hammering",
-            name = "hammering",
+         path = "sound/hammering/hammering",
+         priority = 64
       },
       hotspot = { 12, 27 },
       fps = 10
@@ -37,7 +37,7 @@
       buildship = {
          "walk=object-or-coords",
          "plant=attrib:shipconstruction unless object",
-         "playsound=sound/sawmill sawmill 230",
+         "playsound=sound/sawmill/sawmill 230",
          "animate=work 500",
          "construct",
          "animate=work 5000",

=== modified file 'data/tribes/workers/empire/stonemason/init.lua'
--- data/tribes/workers/empire/stonemason/init.lua	2017-11-18 17:57:00 +0000
+++ data/tribes/workers/empire/stonemason/init.lua	2019-03-23 14:09:21 +0000
@@ -34,7 +34,7 @@
       cut_granite = {
          "findobject=attrib:rocks radius:6",
          "walk=object",
-         "playsound=sound/stonecutting stonecutter 220",
+         "playsound=sound/stonecutting/stonecutter 220",
          "animate=hacking 10000",
          "callobject=shrink",
          "createware=granite",
@@ -43,7 +43,7 @@
       cut_marble = {
          "findobject= attrib:rocks radius:6",
          "walk=object",
-         "playsound=sound/stonecutting stonecutter 220",
+         "playsound=sound/stonecutting/stonecutter 220",
          "animate=hacking 10000",
          "callobject=shrink",
          "createware=marble",

=== modified file 'data/tribes/workers/frisians/builder/init.lua'
--- data/tribes/workers/frisians/builder/init.lua	2018-11-30 10:27:21 +0000
+++ data/tribes/workers/frisians/builder/init.lua	2019-03-23 14:09:21 +0000
@@ -9,8 +9,8 @@
    work = {
       pictures = path.list_files (dirname .. "work_??.png"),
       sound_effect = {
-            directory = "sound/hammering",
-            name = "hammering",
+         path = "sound/hammering/hammering",
+         priority = 64
       },
       hotspot = {9, 24},
       fps = 10

=== modified file 'data/tribes/workers/frisians/shipwright/init.lua'
--- data/tribes/workers/frisians/shipwright/init.lua	2018-11-30 10:27:21 +0000
+++ data/tribes/workers/frisians/shipwright/init.lua	2019-03-23 14:09:21 +0000
@@ -8,8 +8,8 @@
    work = {
       pictures = path.list_files (dirname .. "work_??.png"),
       sound_effect = {
-            directory = "sound/hammering",
-            name = "hammering",
+         path = "sound/hammering/hammering",
+         priority = 64
       },
       hotspot = {9, 24},
       fps = 10

=== modified file 'data/world/critters/duck/init.lua'
--- data/world/critters/duck/init.lua	2017-02-12 09:10:57 +0000
+++ data/world/critters/duck/init.lua	2019-03-23 14:09:21 +0000
@@ -4,8 +4,7 @@
    idle = {
       pictures = path.list_files(dirname .. "idle_??.png"),
       sound_effect = {
-         directory = dirname,
-         name = "duck",
+         path = dirname .. "duck",
       },
       hotspot = { 5, 7 },
       fps = 4,

=== modified file 'data/world/critters/elk/init.lua'
--- data/world/critters/elk/init.lua	2017-02-12 09:10:57 +0000
+++ data/world/critters/elk/init.lua	2019-03-23 14:09:21 +0000
@@ -7,8 +7,7 @@
       fps = 20,
       sound_effect = {
          -- Sound files with numbers starting for 10 are generating silence. Remove when we move the sound triggering to programs
-         directory = "sound/animals",
-         name = "elk",
+         path = "sound/animals/elk",
       },
    },
 }

=== modified file 'data/world/critters/fox/init.lua'
--- data/world/critters/fox/init.lua	2017-02-12 09:10:57 +0000
+++ data/world/critters/fox/init.lua	2019-03-23 14:09:21 +0000
@@ -5,8 +5,7 @@
       pictures = path.list_files(dirname .. "idle_??.png"),
       sound_effect = {
          -- Sound files with numbers starting for 10 are generating silence. Remove when we move the sound triggering to programs
-         directory = "sound/animals",
-         name = "coyote",
+         path = "sound/animals/coyote",
       },
       hotspot = { 10, 13 },
       fps = 10,

=== modified file 'data/world/critters/sheep/init.lua'
--- data/world/critters/sheep/init.lua	2017-02-12 09:10:57 +0000
+++ data/world/critters/sheep/init.lua	2019-03-23 14:09:21 +0000
@@ -4,8 +4,7 @@
    idle = {
       pictures = path.list_files(dirname .. "idle_??.png"),
       sound_effect = {
-         directory = "sound/farm",
-         name = "sheep",
+         path = "sound/farm/sheep",
       },
       hotspot = { 8, 16 },
       fps = 20,

=== modified file 'data/world/critters/stag/init.lua'
--- data/world/critters/stag/init.lua	2017-02-12 09:10:57 +0000
+++ data/world/critters/stag/init.lua	2019-03-23 14:09:21 +0000
@@ -5,8 +5,7 @@
       pictures = path.list_files(dirname .. "idle_??.png"),
       sound_effect = {
          -- Sound files with numbers starting for 10 are generating silence. Remove when we move the sound triggering to programs
-         directory = "sound/animals",
-         name = "stag",
+         path = "sound/animals/stag",
       },
       hotspot = { 12, 26 },
       fps = 20,

=== modified file 'data/world/critters/wildboar/init.lua'
--- data/world/critters/wildboar/init.lua	2017-02-12 09:10:57 +0000
+++ data/world/critters/wildboar/init.lua	2019-03-23 14:09:21 +0000
@@ -6,8 +6,7 @@
       hotspot = { 10, 18 },
       fps = 20,
       sound_effect = {
-         directory = "sound/animals",
-         name = "boar",
+         path = "sound/animals/boar",
       },
    },
 }

=== modified file 'data/world/critters/wolf/init.lua'
--- data/world/critters/wolf/init.lua	2017-02-12 09:10:57 +0000
+++ data/world/critters/wolf/init.lua	2019-03-23 14:09:21 +0000
@@ -6,9 +6,8 @@
       hotspot = { 8, 15 },
       fps = 10,
       sound_effect = {
-         -- Sound files with numbers starting for 10 are generating silence. Remove when we move the sound triggering to programs
-         directory = "sound/animals",
-         name = "wolf",
+         -- Sound files with numbers starting from 10 are generating silence.
+         path = "sound/animals/wolf",
       },
    },
 }

=== modified file 'data/world/immovables/grass1/init.lua'
--- data/world/immovables/grass1/init.lua	2016-01-21 10:39:57 +0000
+++ data/world/immovables/grass1/init.lua	2019-03-23 14:09:21 +0000
@@ -12,8 +12,7 @@
          pictures = path.list_files(dirname .. "idle.png"),
          hotspot = { 10, 20 },
          sound_effect = {
-            directory = "sound/animals",
-            name = "frog1",
+            path = "sound/animals/frog1",
          },
       },
    }

=== modified file 'data/world/immovables/grass2/init.lua'
--- data/world/immovables/grass2/init.lua	2016-01-21 10:39:57 +0000
+++ data/world/immovables/grass2/init.lua	2019-03-23 14:09:21 +0000
@@ -12,8 +12,7 @@
          pictures = path.list_files(dirname .. "idle.png"),
          hotspot = { 10, 16 },
          sound_effect = {
-            directory = "sound/animals",
-            name = "frog1",
+            path = "sound/animals/frog1",
          },
       },
    }

=== modified file 'data/world/immovables/grass3/init.lua'
--- data/world/immovables/grass3/init.lua	2016-01-21 10:39:57 +0000
+++ data/world/immovables/grass3/init.lua	2019-03-23 14:09:21 +0000
@@ -12,8 +12,7 @@
          pictures = path.list_files(dirname .. "idle.png"),
          hotspot = { 10, 11 },
          sound_effect = {
-            directory = "sound/animals",
-            name = "frog1",
+            path = "sound/animals/frog1",
          },
       },
    }

=== modified file 'data/world/immovables/trees/alder/init.lua'
--- data/world/immovables/trees/alder/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/alder/init.lua	2019-03-23 14:09:21 +0000
@@ -108,8 +108,7 @@
          hotspot = { 23, 59 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird4",
+            path = "sound/animals/bird4",
          },
       },
    },

=== modified file 'data/world/immovables/trees/aspen/init.lua'
--- data/world/immovables/trees/aspen/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/aspen/init.lua	2019-03-23 14:09:21 +0000
@@ -104,8 +104,7 @@
          hotspot = { 23, 58 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird1",
+            path = "sound/animals/bird1",
          },
       },
       falling = {

=== modified file 'data/world/immovables/trees/beech/init.lua'
--- data/world/immovables/trees/beech/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/beech/init.lua	2019-03-23 14:09:21 +0000
@@ -100,8 +100,7 @@
          hotspot = { 24, 60 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird6",
+            path = "sound/animals/bird6",
          },
       },
    },

=== modified file 'data/world/immovables/trees/birch/init.lua'
--- data/world/immovables/trees/birch/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/birch/init.lua	2019-03-23 14:09:21 +0000
@@ -103,8 +103,7 @@
          hotspot = { 23, 58 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird5",
+            path = "sound/animals/bird5",
          },
       },
    },

=== modified file 'data/world/immovables/trees/larch/init.lua'
--- data/world/immovables/trees/larch/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/larch/init.lua	2019-03-23 14:09:21 +0000
@@ -100,8 +100,7 @@
          hotspot = { 15, 59 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird6",
+            path = "sound/animals/bird6",
          },
       },
    },

=== modified file 'data/world/immovables/trees/maple/init.lua'
--- data/world/immovables/trees/maple/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/maple/init.lua	2019-03-23 14:09:21 +0000
@@ -100,8 +100,7 @@
          hotspot = { 23, 59 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird4",
+            path = "sound/animals/bird4",
          },
       },
    },

=== modified file 'data/world/immovables/trees/oak/init.lua'
--- data/world/immovables/trees/oak/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/oak/init.lua	2019-03-23 14:09:21 +0000
@@ -101,8 +101,7 @@
          hotspot = { 24, 60 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird2",
+            path = "sound/animals/bird2",
          },
       },
       falling = {

=== modified file 'data/world/immovables/trees/palm_borassus/init.lua'
--- data/world/immovables/trees/palm_borassus/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/palm_borassus/init.lua	2019-03-23 14:09:21 +0000
@@ -100,8 +100,7 @@
          hotspot = { 24, 60 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "crickets1",
+            path = "sound/animals/crickets1",
          },
       },
    },

=== modified file 'data/world/immovables/trees/palm_coconut/init.lua'
--- data/world/immovables/trees/palm_coconut/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/palm_coconut/init.lua	2019-03-23 14:09:21 +0000
@@ -100,8 +100,7 @@
          hotspot = { 24, 59 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird3",
+            path = "sound/animals/bird3",
          },
       },
    },

=== modified file 'data/world/immovables/trees/palm_date/init.lua'
--- data/world/immovables/trees/palm_date/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/palm_date/init.lua	2019-03-23 14:09:21 +0000
@@ -103,8 +103,7 @@
          hotspot = { 24, 60 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "crickets1",
+            path = "sound/animals/crickets1",
          },
       },
    },

=== modified file 'data/world/immovables/trees/palm_oil/init.lua'
--- data/world/immovables/trees/palm_oil/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/palm_oil/init.lua	2019-03-23 14:09:21 +0000
@@ -104,8 +104,7 @@
          hotspot = { 24, 60 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "crickets2",
+            path = "sound/animals/crickets2",
          },
       },
    },

=== modified file 'data/world/immovables/trees/palm_roystonea/init.lua'
--- data/world/immovables/trees/palm_roystonea/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/palm_roystonea/init.lua	2019-03-23 14:09:21 +0000
@@ -100,8 +100,7 @@
          hotspot = { 24, 60 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "crickets2",
+            path = "sound/animals/crickets2",
          },
       },
    },

=== modified file 'data/world/immovables/trees/rowan/init.lua'
--- data/world/immovables/trees/rowan/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/rowan/init.lua	2019-03-23 14:09:21 +0000
@@ -103,8 +103,7 @@
          hotspot = { 23, 59 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird6",
+            path = "sound/animals/bird6",
          },
       },
    },

=== modified file 'data/world/immovables/trees/spruce/init.lua'
--- data/world/immovables/trees/spruce/init.lua	2018-11-03 11:27:18 +0000
+++ data/world/immovables/trees/spruce/init.lua	2019-03-23 14:09:21 +0000
@@ -100,8 +100,7 @@
          hotspot = { 15, 59 },
          fps = 10,
          sound_effect = {
-            directory = "sound/animals",
-            name = "bird3",
+            path = "sound/animals/bird3",
          },
       },
    },

=== modified file 'doc/sphinx/source/animations.rst'
--- doc/sphinx/source/animations.rst	2018-04-09 08:01:28 +0000
+++ doc/sphinx/source/animations.rst	2019-03-23 14:09:21 +0000
@@ -12,8 +12,8 @@
          scale = 2.5,
          fps = 4,
          sound_effect = {
-            directory = "sound/foo",
-            name = "bar",
+            path = "sound/foo/bar",
+            priority = 128
          },
       },
       working = ...
@@ -38,7 +38,11 @@
    specify the float value here. For example, if the animation images are 2.5 times the size of what should be blitted at default zoom, use ``scale = 2.5``.
 
 **sound_effect**
-   *Optional*. Our example will look for the sound files ``bar_00.ogg`` through ``bar_99.ogg`` in the directory ``data/sound/foo`` and play them in sequence.
+   *Optional*. Our example will look for the sound files ``bar_00.ogg`` through ``bar_99.ogg`` in the directory ``data/sound/foo`` and play them in sequence. The priority is optional with the default being ``1``, and its range is:
+
+   * **0-127:** Probability between ``0.0`` and ``1.0``, only one instance can be playing at any time
+   * **128-254:** Probability between ``0.0`` and ``1.0``, many instances can be playing at any time
+   * **255:** Always play
 
 
 Directional Animations

=== modified file 'doc/sphinx/source/productionsite_program.rst'
--- doc/sphinx/source/productionsite_program.rst	2017-11-18 21:23:09 +0000
+++ doc/sphinx/source/productionsite_program.rst	2019-03-23 14:09:21 +0000
@@ -66,7 +66,7 @@
          "return=skipped unless economy needs snack",
          "sleep=5000",
          "consume=barbarians_bread fish,meat beer",
-         "playsound=sound/barbarians/taverns inn 100",
+         "playsound=sound/barbarians/taverns/inn 100",
          "animate=working 22000",
          "sleep=10000",
          "produce=snack"
@@ -298,8 +298,8 @@
 
 Parameter semantics:
 
-``soundFX``
-    The filename of a soundFX (relative to the productionsite's directory).
+``filepath``
+    The path/base_filename of a soundFX (relative to the data directory).
 ``priority``
     An integer. If omitted, 127 is used.
 

=== modified file 'regression_test.py'
--- regression_test.py	2017-09-15 13:23:09 +0000
+++ regression_test.py	2019-03-23 14:09:21 +0000
@@ -89,8 +89,7 @@
                     '--datadir={}'.format(datadir()),
                     '--datadir_for_testing={}'.format(datadir_for_testing()),
                     '--homedir={}'.format(self.run_dir),
-                    '--disable_fx=true',
-                    '--disable_music=true',
+                    '--nosound',
                     '--language=en_US' ]
             args += [ "--{}={}".format(key, value) for key, value in iteritems(wlargs) ]
 

=== modified file 'src/economy/flag.h'
--- src/economy/flag.h	2019-02-27 17:19:00 +0000
+++ src/economy/flag.h	2019-03-23 14:09:21 +0000
@@ -156,6 +156,7 @@
 	void draw(uint32_t gametime,
 	          TextToDraw draw_text,
 	          const Vector2f& point_on_dst,
+			  const Coords& coords,
 	          float scale,
 	          RenderTarget* dst) override;
 

=== modified file 'src/economy/portdock.cc'
--- src/economy/portdock.cc	2019-02-27 17:19:00 +0000
+++ src/economy/portdock.cc	2019-03-23 14:09:21 +0000
@@ -140,7 +140,7 @@
 		expedition_bootstrap_->set_economy(e);
 }
 
-void PortDock::draw(uint32_t, const TextToDraw, const Vector2f&, float, RenderTarget*) {
+void PortDock::draw(uint32_t, const TextToDraw, const Vector2f&, const Widelands::Coords&, float, RenderTarget*) {
 	// do nothing
 }
 

=== modified file 'src/economy/portdock.h'
--- src/economy/portdock.h	2019-02-27 17:19:00 +0000
+++ src/economy/portdock.h	2019-03-23 14:09:21 +0000
@@ -98,7 +98,7 @@
 	PositionList get_positions(const EditorGameBase&) const override;
 	void draw(uint32_t gametime,
 	          TextToDraw draw_text,
-	          const Vector2f& point_on_dst,
+	          const Vector2f& point_on_dst, const Coords&,
 	          float scale,
 	          RenderTarget* dst) override;
 

=== modified file 'src/economy/road.h'
--- src/economy/road.h	2019-02-27 17:19:00 +0000
+++ src/economy/road.h	2019-03-23 14:09:21 +0000
@@ -131,8 +131,7 @@
 	void cleanup(EditorGameBase&) override;
 
 	void draw(uint32_t gametime,
-	          TextToDraw draw_text,
-	          const Vector2f& point_on_dst,
+	          TextToDraw draw_text, const Vector2f&, const Coords&,
 	          float scale,
 	          RenderTarget* dst) override;
 

=== modified file 'src/editor/editorinteractive.cc'
--- src/editor/editorinteractive.cc	2019-02-27 17:19:00 +0000
+++ src/editor/editorinteractive.cc	2019-03-23 14:09:21 +0000
@@ -300,14 +300,14 @@
 		if (draw_immovables_) {
 			Widelands::BaseImmovable* const imm = field.fcoords.field->get_immovable();
 			if (imm != nullptr && imm->get_positions(ebase).front() == field.fcoords) {
-				imm->draw(gametime, TextToDraw::kNone, field.rendertarget_pixel, scale, &dst);
+				imm->draw(gametime, TextToDraw::kNone, field.rendertarget_pixel, field.fcoords, scale, &dst);
 			}
 		}
 
 		if (draw_bobs_) {
 			for (Widelands::Bob* bob = field.fcoords.field->get_first_bob(); bob;
 			     bob = bob->get_next_bob()) {
-				bob->draw(ebase, TextToDraw::kNone, field.rendertarget_pixel, scale, &dst);
+				bob->draw(ebase, TextToDraw::kNone, field.rendertarget_pixel, field.fcoords, scale, &dst);
 			}
 		}
 
@@ -405,6 +405,10 @@
 	is_painting_ = false;
 }
 
+bool EditorInteractive::player_hears_field(const Widelands::Coords&) const {
+	return true;
+}
+
 void EditorInteractive::on_buildhelp_changed(const bool value) {
 	toggle_buildhelp_->set_perm_pressed(value);
 }

=== modified file 'src/editor/editorinteractive.h'
--- src/editor/editorinteractive.h	2019-02-23 11:00:49 +0000
+++ src/editor/editorinteractive.h	2019-03-23 14:09:21 +0000
@@ -140,6 +140,7 @@
 private:
 	friend struct EditorToolMenu;
 
+	bool player_hears_field(const Widelands::Coords& coords) const override;
 	void on_buildhelp_changed(const bool value) override;
 
 	void toggle_resources();

=== modified file 'src/graphic/animation.cc'
--- src/graphic/animation.cc	2019-02-23 11:00:49 +0000
+++ src/graphic/animation.cc	2019-03-23 14:09:21 +0000
@@ -37,11 +37,13 @@
 #include "graphic/playercolor.h"
 #include "graphic/texture.h"
 #include "io/filesystem/layered_filesystem.h"
+#include "logic/game_data_error.h"
 #include "scripting/lua_table.h"
 #include "sound/note_sound.h"
 #include "sound/sound_handler.h"
 
 namespace {
+
 /**
  * Implements the Animation interface for an animation that is unpacked on disk, that
  * is every frame and every pc color frame is an singular file on disk.
@@ -62,14 +64,16 @@
 	uint32_t frametime() const override;
 	const Image* representative_image(const RGBColor* clr) const override;
 	const std::string& representative_image_filename() const override;
-	virtual void blit(uint32_t time,
+	virtual void blit(uint32_t time, const Widelands::Coords& coords,
 	                  const Rectf& source_rect,
 	                  const Rectf& destination_rect,
 	                  const RGBColor* clr,
 	                  Surface* target) const override;
-	void trigger_sound(uint32_t framenumber, uint32_t stereo_position) const override;
-
 private:
+	// TODO(unknown): The chosen semantics of animation sound effects is problematic:
+	// What if the game runs very slowly or very quickly?
+	void trigger_sound(uint32_t framenumber, const Widelands::Coords& coords) const override;
+
 	// Loads the graphics if they are not yet loaded.
 	void ensure_graphics_are_loaded() const;
 
@@ -88,10 +92,9 @@
 	std::vector<const Image*> frames_;
 	std::vector<const Image*> pcmasks_;
 
-	// name of sound effect that will be played at frame 0.
-	// TODO(sirver): this should be done using playsound in a program instead of
-	// binding it to the animation.
-	std::string sound_effect_;
+	// ID of sound effect that will be played at frame 0.
+	FxId sound_effect_;
+	int32_t sound_priority_;
 	bool play_once_;
 };
 
@@ -100,15 +103,22 @@
      hotspot_(table.get_vector<std::string, int>("hotspot")),
      hasplrclrs_(false),
      scale_(1),
+	 sound_effect_(kNoSoundEffect),
+	 sound_priority_(kFxPriorityLowest),
      play_once_(false) {
 	try {
 		if (table.has_key("sound_effect")) {
 			std::unique_ptr<LuaTable> sound_effects = table.get_table("sound_effect");
-
-			const std::string name = sound_effects->get_string("name");
-			const std::string directory = sound_effects->get_string("directory");
-			sound_effect_ = directory + g_fs->file_separator() + name;
-			g_sound_handler.load_fx_if_needed(directory, name, sound_effect_);
+			sound_effect_ = SoundHandler::register_fx(SoundType::kAmbient, sound_effects->get_string("path"));
+
+			if (sound_effects->has_key<std::string>("priority")) {
+				sound_priority_ = sound_effects->get_int("priority");
+			}
+
+			if (sound_priority_ < kFxPriorityLowest) {
+				throw Widelands::GameDataError("Minmum priority for sounds is %d, but only %d was specified for %s",
+									kFxPriorityLowest, sound_priority_, sound_effects->get_string("path").c_str());
+			}
 		}
 
 		if (table.has_key("play_once")) {
@@ -236,15 +246,15 @@
 	return 0;
 }
 
-void NonPackedAnimation::trigger_sound(uint32_t time, uint32_t stereo_position) const {
-	if (sound_effect_.empty()) {
+void NonPackedAnimation::trigger_sound(uint32_t time, const Widelands::Coords& coords) const {
+	if (sound_effect_ == kNoSoundEffect || coords == Widelands::Coords::null()) {
 		return;
 	}
 
 	const uint32_t framenumber = current_frame(time);
 
 	if (framenumber == 0) {
-		Notifications::publish(NoteSound(sound_effect_, stereo_position, 1));
+		Notifications::publish(NoteSound(SoundType::kAmbient, sound_effect_, coords, sound_priority_));
 	}
 }
 
@@ -264,6 +274,7 @@
 }
 
 void NonPackedAnimation::blit(uint32_t time,
+							  const Widelands::Coords& coords,
                               const Rectf& source_rect,
                               const Rectf& destination_rect,
                               const RGBColor* clr,
@@ -278,8 +289,7 @@
 		target->blit_blended(
 		   destination_rect, *frames_.at(idx), *pcmasks_.at(idx), source_rect, *clr);
 	}
-	// TODO(GunChleoc): Stereo position would be nice.
-	trigger_sound(time, 128);
+	trigger_sound(time, coords);
 }
 
 }  // namespace

=== modified file 'src/graphic/animation.h'
--- src/graphic/animation.h	2019-02-23 11:00:49 +0000
+++ src/graphic/animation.h	2019-03-23 14:09:21 +0000
@@ -32,6 +32,7 @@
 #include "base/rect.h"
 #include "base/vector.h"
 #include "graphic/surface.h"
+#include "logic/widelands_geometry.h"
 
 class Image;
 class LuaTable;
@@ -91,13 +92,18 @@
 	/// in which case the neutral image will be blitted. The Surface is the 'target'
 	/// for the blit operation and must be non-null.
 	virtual void blit(uint32_t time,
+	                  const Widelands::Coords& coords,
 	                  const Rectf& source_rect,
 	                  const Rectf& destination_rect,
 	                  const RGBColor* clr,
 	                  Surface* target) const = 0;
 
+protected:
 	/// Play the sound effect associated with this animation at the given time.
-	virtual void trigger_sound(uint32_t time, uint32_t stereo_position) const = 0;
+	/// Any sound effects are played with stereo position according to 'coords'.
+	/// If 'coords' == Widelands::Coords::null(), skip playing any sound effects.
+	virtual void trigger_sound(uint32_t time,
+							   const Widelands::Coords& coords) const = 0;
 
 private:
 	DISALLOW_COPY_AND_ASSIGN(Animation);

=== modified file 'src/graphic/game_renderer.cc'
--- src/graphic/game_renderer.cc	2019-03-23 14:09:20 +0000
+++ src/graphic/game_renderer.cc	2019-03-23 14:09:21 +0000
@@ -44,13 +44,13 @@
 	uint32_t const anim_idx = field.owner->tribe().frontier_animation();
 	if (field.vision) {
 		dst->blit_animation(
-		   field.rendertarget_pixel, scale, anim_idx, 0, &field.owner->get_playercolor());
+		   field.rendertarget_pixel, field.fcoords, scale, anim_idx, 0, &field.owner->get_playercolor());
 	}
 	for (const auto& nf : {fields_to_draw.at(field.rn_index), fields_to_draw.at(field.bln_index),
 	                       fields_to_draw.at(field.brn_index)}) {
 		if ((field.vision || nf.vision) && nf.is_border &&
 		    (field.owner == nf.owner || nf.owner == nullptr)) {
-			dst->blit_animation(middle(field.rendertarget_pixel, nf.rendertarget_pixel), scale,
+			dst->blit_animation(middle(field.rendertarget_pixel, nf.rendertarget_pixel), Widelands::Coords::null(), scale,
 			                    anim_idx, 0, &field.owner->get_playercolor());
 		}
 	}

=== modified file 'src/graphic/rendertarget.cc'
--- src/graphic/rendertarget.cc	2019-03-23 14:09:20 +0000
+++ src/graphic/rendertarget.cc	2019-03-23 14:09:21 +0000
@@ -286,22 +286,20 @@
 }
 
 void RenderTarget::blit_animation(const Vector2f& dst,
+								  const Widelands::Coords& coords,
                                      const float scale,
                                      uint32_t animation_id,
                                      uint32_t time,
                                      const RGBColor* player_color,
                                      const int percent_from_bottom) {
 	const Animation& animation = g_gr->animations().get_animation(animation_id);
-	// TODO(unknown): Correctly calculate the stereo position for sound effects
-	// TODO(unknown): The chosen semantics of animation sound effects is problematic:
-	// What if the game runs very slowly or very quickly?
 	assert(percent_from_bottom <= 100);
 	if (percent_from_bottom > 0) {
 		// Scaling for zoom and animation image size, then fit screen edges.
 		Rectf srcrc = animation.source_rectangle(percent_from_bottom);
 		Rectf dstrc = animation.destination_rectangle(dst, srcrc, scale);
 		if (to_surface_geometry(&dstrc, &srcrc)) {
-			animation.blit(time, srcrc, dstrc, player_color, surface_);
+			animation.blit(time, coords, srcrc, dstrc, player_color, surface_);
 		}
 	}
 }

=== modified file 'src/graphic/rendertarget.h'
--- src/graphic/rendertarget.h	2019-03-23 14:09:20 +0000
+++ src/graphic/rendertarget.h	2019-03-23 14:09:21 +0000
@@ -27,6 +27,7 @@
 #include "graphic/blend_mode.h"
 #include "graphic/color.h"
 #include "graphic/image.h"
+#include "logic/widelands_geometry.h"
 
 class Animation;
 class Surface;
@@ -105,7 +106,10 @@
 	// Draw the 'animation' as it should appear at 'time' in this target at
 	// 'dst'. Optionally, the animation is tinted with 'player_color' and
 	// cropped to 'source_rect'.
+	// Any sound effects are played with stereo position according to 'coords'.
+	// If 'coords' == Widelands::Coords::null(), skip playing any sound effects.
 	void blit_animation(const Vector2f& dst,
+						const Widelands::Coords& coords,
 	                       const float scale,
 	                       uint32_t animation_id,
 	                       uint32_t time,

=== modified file 'src/logic/editor_game_base.cc'
--- src/logic/editor_game_base.cc	2019-03-19 21:33:01 +0000
+++ src/logic/editor_game_base.cc	2019-03-23 14:09:21 +0000
@@ -53,6 +53,7 @@
 #include "map_io/map_saver.h"
 #include "scripting/logic.h"
 #include "scripting/lua_table.h"
+#include "sound/sound_handler.h"
 #include "ui_basic/progresswindow.h"
 #include "wui/interactive_base.h"
 #include "wui/interactive_gamebase.h"
@@ -78,6 +79,9 @@
 
 EditorGameBase::~EditorGameBase() {
 	delete_tempfile();
+	if (g_sh != nullptr) {
+		g_sh->remove_fx_set(SoundType::kAmbient);
+	}
 }
 
 /**

=== modified file 'src/logic/game.cc'
--- src/logic/game.cc	2019-03-09 10:55:24 +0000
+++ src/logic/game.cc	2019-03-23 14:09:21 +0000
@@ -64,6 +64,7 @@
 #include "network/network.h"
 #include "scripting/logic.h"
 #include "scripting/lua_table.h"
+#include "sound/sound_handler.h"
 #include "ui_basic/progresswindow.h"
 #include "wui/game_tips.h"
 #include "wui/interactive_player.h"
@@ -541,7 +542,7 @@
 		;
 #endif
 
-	g_sound_handler.change_music("ingame", 1000, 0);
+	g_sh->change_music("ingame", 1000);
 
 	state_ = gs_running;
 
@@ -549,7 +550,7 @@
 
 	state_ = gs_ending;
 
-	g_sound_handler.change_music("menu", 1000, 0);
+	g_sh->change_music("menu", 1000);
 
 	cleanup_objects();
 	set_ibase(nullptr);

=== modified file 'src/logic/map_objects/bob.cc'
--- src/logic/map_objects/bob.cc	2019-03-23 14:09:20 +0000
+++ src/logic/map_objects/bob.cc	2019-03-23 14:09:21 +0000
@@ -759,6 +759,7 @@
 void Bob::draw(const EditorGameBase& egbase,
                const TextToDraw&,
                const Vector2f& field_on_dst,
+			   const Widelands::Coords& coords,
                const float scale,
                RenderTarget* dst) const {
 	if (!anim_) {
@@ -767,7 +768,7 @@
 
 	auto* const bob_owner = get_owner();
 	const Vector2f point_on_dst = calc_drawpos(egbase, field_on_dst, scale);
-	dst->blit_animation(point_on_dst, scale, anim_, egbase.get_gametime() - animstart_,
+	dst->blit_animation(point_on_dst, coords, scale, anim_, egbase.get_gametime() - animstart_,
 						(bob_owner == nullptr) ? nullptr : &bob_owner->get_playercolor());
 }
 

=== modified file 'src/logic/map_objects/bob.h'
--- src/logic/map_objects/bob.h	2019-02-27 17:19:00 +0000
+++ src/logic/map_objects/bob.h	2019-03-23 14:09:21 +0000
@@ -265,7 +265,7 @@
 	// required to draw the bob in the right size.
 	virtual void draw(const EditorGameBase&,
 	                  const TextToDraw& draw_text,
-	                  const Vector2f& field_on_dst,
+	                  const Vector2f& field_on_dst, const Coords& coords,
 	                  float scale,
 	                  RenderTarget* dst) const;
 

=== modified file 'src/logic/map_objects/immovable.cc'
--- src/logic/map_objects/immovable.cc	2019-03-23 14:09:20 +0000
+++ src/logic/map_objects/immovable.cc	2019-03-23 14:09:21 +0000
@@ -170,8 +170,6 @@
 			action = new ActRemove(arguments.get(), *immovable);
 		} else if (parts[0] == "seed") {
 			action = new ActSeed(arguments.get(), *immovable);
-		} else if (parts[0] == "playsound") {
-			action = new ActPlaySound(arguments.get(), *immovable);
 		} else if (parts[0] == "construct") {
 			action = new ActConstruct(arguments.get(), *immovable);
 		} else {
@@ -456,24 +454,26 @@
 void Immovable::draw(uint32_t gametime,
                      const TextToDraw draw_text,
                      const Vector2f& point_on_dst,
+					 const Widelands::Coords& coords,
                      float scale,
                      RenderTarget* dst) {
 	if (!anim_) {
 		return;
 	}
 	if (!anim_construction_total_) {
-		dst->blit_animation(point_on_dst, scale, anim_, gametime - animstart_);
+		dst->blit_animation(point_on_dst, coords, scale, anim_, gametime - animstart_);
 		if (former_building_descr_) {
 			do_draw_info(draw_text, former_building_descr_->descname(), "", point_on_dst, scale, dst);
 		}
 	} else {
-		draw_construction(gametime, draw_text, point_on_dst, scale, dst);
+		draw_construction(gametime, draw_text, point_on_dst, coords, scale, dst);
 	}
 }
 
 void Immovable::draw_construction(const uint32_t gametime,
                                   const TextToDraw draw_text,
                                   const Vector2f& point_on_dst,
+								  const Widelands::Coords& coords,
                                   const float scale,
                                   RenderTarget* dst) {
 	const ImmovableProgram::ActConstruct* constructionact = nullptr;
@@ -504,13 +504,13 @@
 	if (current_frame > 0) {
 		// Not the first pic, so draw the previous one in the back
 		dst->blit_animation(
-		   point_on_dst, scale, anim_, (current_frame - 1) * frametime, &player_color);
+		   point_on_dst, Widelands::Coords::null(), scale, anim_, (current_frame - 1) * frametime, &player_color);
 	}
 
 	const int percent = ((done % units_per_frame) * 100) / units_per_frame;
 
 	dst->blit_animation(
-	   point_on_dst, scale, anim_, current_frame * frametime, &player_color, percent);
+	   point_on_dst, coords, scale, anim_, current_frame * frametime, &player_color, percent);
 
 	// Additionally, if statistics are enabled, draw a progression string
 	do_draw_info(draw_text, descr().descname(),
@@ -788,35 +788,6 @@
 	   game, duration_ ? 1 + game.logic_rand() % duration_ + game.logic_rand() % duration_ : 0);
 }
 
-ImmovableProgram::ActPlaySound::ActPlaySound(char* parameters, const ImmovableDescr&) {
-	try {
-		bool reached_end;
-		name = next_word(parameters, reached_end);
-
-		if (!reached_end) {
-			char* endp;
-			unsigned long long int const value = strtoull(parameters, &endp, 0);
-			priority = value;
-			if (*endp || priority != value)
-				throw GameDataError("expected %s but found \"%s\"", "priority", parameters);
-		} else
-			priority = 127;
-
-		g_sound_handler.load_fx_if_needed(
-		   FileSystem::fs_dirname(name), FileSystem::fs_filename(name.c_str()), name);
-	} catch (const WException& e) {
-		throw GameDataError("playsound: %s", e.what());
-	}
-}
-
-/** Demand from the g_sound_handler to play a certain sound effect.
- * Whether the effect actually gets played
- * is decided only by the sound server*/
-void ImmovableProgram::ActPlaySound::execute(Game& game, Immovable& immovable) const {
-	Notifications::publish(NoteSound(name, immovable.get_position(), priority));
-	immovable.program_step(game);
-}
-
 ImmovableProgram::ActTransform::ActTransform(char* parameters, ImmovableDescr& descr) {
 	try {
 		tribe = true;

=== modified file 'src/logic/map_objects/immovable.h'
--- src/logic/map_objects/immovable.h	2019-02-27 17:19:00 +0000
+++ src/logic/map_objects/immovable.h	2019-03-23 14:09:21 +0000
@@ -105,6 +105,7 @@
 	virtual void draw(uint32_t gametime,
 	                  TextToDraw draw_text,
 	                  const Vector2f& point_on_dst,
+					  const Coords& coords,
 	                  float scale,
 	                  RenderTarget* dst) = 0;
 
@@ -229,7 +230,7 @@
 	void act(Game&, uint32_t data) override;
 	void draw(uint32_t gametime,
 	          TextToDraw draw_text,
-	          const Vector2f& point_on_dst,
+	          const Vector2f& point_on_dst, const Coords& coords,
 	          float scale,
 	          RenderTarget* dst) override;
 
@@ -316,6 +317,7 @@
 	void draw_construction(uint32_t gametime,
 	                       TextToDraw draw_text,
 	                       const Vector2f& point_on_dst,
+						   const Widelands::Coords& coords,
 	                       float scale,
 	                       RenderTarget* dst);
 };

=== modified file 'src/logic/map_objects/immovable_program.h'
--- src/logic/map_objects/immovable_program.h	2019-02-23 11:00:49 +0000
+++ src/logic/map_objects/immovable_program.h	2019-03-23 14:09:21 +0000
@@ -128,29 +128,6 @@
 		uint8_t probability;
 	};
 
-	/// Plays a sound effect.
-	///
-	/// Parameter syntax:
-	///    parameters ::= directory sound [priority]
-	/// Parameter semantics:
-	///    directory:
-	///       The directory of the sound files, relative to the datadir.
-	///    sound:
-	///       The base filename of a sound effect (relative to the directory).
-	///    priority:
-	///       An integer. If omitted, 127 is used.
-	///
-	/// Plays the specified sound effect with the specified priority. Whether the
-	/// sound effect is actually played is determined by the sound handler.
-	struct ActPlaySound : public Action {
-		ActPlaySound(char* parameters, const ImmovableDescr&);
-		void execute(Game&, Immovable&) const override;
-
-	private:
-		std::string name;
-		uint8_t priority;
-	};
-
 	/**
 	 * Puts the immovable into construction mode.
 	 *

=== modified file 'src/logic/map_objects/tribes/building.cc'
--- src/logic/map_objects/tribes/building.cc	2019-03-23 14:09:20 +0000
+++ src/logic/map_objects/tribes/building.cc	2019-03-23 14:09:21 +0000
@@ -608,10 +608,11 @@
 void Building::draw(uint32_t gametime,
                     const TextToDraw draw_text,
                     const Vector2f& point_on_dst,
+					const Widelands::Coords& coords,
                     const float scale,
                     RenderTarget* dst) {
 	dst->blit_animation(
-	   point_on_dst, scale, anim_, gametime - animstart_, &get_owner()->get_playercolor());
+	   point_on_dst, coords, scale, anim_, gametime - animstart_, &get_owner()->get_playercolor());
 
 	//  door animation?
 

=== modified file 'src/logic/map_objects/tribes/building.h'
--- src/logic/map_objects/tribes/building.h	2019-02-27 17:19:00 +0000
+++ src/logic/map_objects/tribes/building.h	2019-03-23 14:09:21 +0000
@@ -341,7 +341,7 @@
 
 	void draw(uint32_t gametime,
 	          TextToDraw draw_text,
-	          const Vector2f& point_on_dst,
+	          const Vector2f& point_on_dst, const Coords& coords,
 	          float scale,
 	          RenderTarget* dst) override;
 	void

=== modified file 'src/logic/map_objects/tribes/constructionsite.cc'
--- src/logic/map_objects/tribes/constructionsite.cc	2019-03-23 14:09:20 +0000
+++ src/logic/map_objects/tribes/constructionsite.cc	2019-03-23 14:09:21 +0000
@@ -35,11 +35,14 @@
 #include "logic/game.h"
 #include "logic/map_objects/tribes/tribe_descr.h"
 #include "logic/map_objects/tribes/worker.h"
+#include "sound/note_sound.h"
+#include "sound/sound_handler.h"
 #include "ui_basic/window.h"
 
 namespace Widelands {
 
 void ConstructionsiteInformation::draw(const Vector2f& point_on_dst,
+									   const Widelands::Coords& coords,
                                        float scale,
                                        const RGBColor& player_color,
                                        RenderTarget* dst) const {
@@ -55,11 +58,11 @@
 
 	if (cur_frame) {  //  not the first pic
 		// Draw the complete prev pic , so we won't run into trouble if images have different sizes
-		dst->blit_animation(point_on_dst, scale, anim_idx, anim_time - FRAME_LENGTH, &player_color);
+		dst->blit_animation(point_on_dst, Widelands::Coords::null(), scale, anim_idx, anim_time - FRAME_LENGTH, &player_color);
 	} else if (was) {
 		//  Is the first picture but there was another building here before,
 		//  get its most fitting picture and draw it instead.
-		dst->blit_animation(point_on_dst, scale, was->get_unoccupied_animation(),
+		dst->blit_animation(point_on_dst, Widelands::Coords::null(), scale, was->get_unoccupied_animation(),
 		                    anim_time - FRAME_LENGTH, &player_color);
 	}
 	// Now blit a segment of the current construction phase from the bottom.
@@ -68,7 +71,7 @@
 		percent /= totaltime;
 	}
 	percent -= 100 * cur_frame;
-	dst->blit_animation(point_on_dst, scale, anim_idx, anim_time, &player_color, percent);
+	dst->blit_animation(point_on_dst, coords, scale, anim_idx, anim_time, &player_color, percent);
 }
 
 /**
@@ -78,7 +81,8 @@
 ConstructionSiteDescr::ConstructionSiteDescr(const std::string& init_descname,
                                              const LuaTable& table,
                                              const EditorGameBase& egbase)
-   : BuildingDescr(init_descname, MapObjectType::CONSTRUCTIONSITE, table, egbase) {
+   : BuildingDescr(init_descname, MapObjectType::CONSTRUCTIONSITE, table, egbase),
+	 creation_fx_(SoundHandler::register_fx(SoundType::kAmbient, "sound/create_construction_site")) {
 	add_attribute(MapObject::CONSTRUCTIONSITE);
 }
 
@@ -86,6 +90,10 @@
 	return *new ConstructionSite(*this);
 }
 
+FxId ConstructionSiteDescr::creation_fx() const {
+	return creation_fx_;
+}
+
 /*
 ==============================
 
@@ -143,6 +151,7 @@
 ===============
 */
 bool ConstructionSite::init(EditorGameBase& egbase) {
+	Notifications::publish(NoteSound(SoundType::kAmbient, descr().creation_fx(), position_, kFxPriorityAlwaysPlay));
 	PartiallyFinishedBuilding::init(egbase);
 
 	const std::map<DescriptionIndex, uint8_t>* buildcost;
@@ -339,12 +348,13 @@
 void ConstructionSite::draw(uint32_t gametime,
                             TextToDraw draw_text,
                             const Vector2f& point_on_dst,
+							const Widelands::Coords& coords,
                             float scale,
                             RenderTarget* dst) {
 	uint32_t tanim = gametime - animstart_;
 	// Draw the construction site marker
 	const RGBColor& player_color = get_owner()->get_playercolor();
-	dst->blit_animation(point_on_dst, scale, anim_, tanim, &player_color);
+	dst->blit_animation(point_on_dst, Widelands::Coords::null(), scale, anim_, tanim, &player_color);
 
 	// Draw the partially finished building
 
@@ -358,7 +368,7 @@
 		info_.completedtime += CONSTRUCTIONSITE_STEP_TIME + gametime - work_steptime_;
 	}
 
-	info_.draw(point_on_dst, scale, player_color, dst);
+	info_.draw(point_on_dst, coords, scale, player_color, dst);
 
 	// Draw help strings
 	draw_info(draw_text, point_on_dst, scale, dst);

=== modified file 'src/logic/map_objects/tribes/constructionsite.h'
--- src/logic/map_objects/tribes/constructionsite.h	2019-02-27 17:19:00 +0000
+++ src/logic/map_objects/tribes/constructionsite.h	2019-03-23 14:09:21 +0000
@@ -38,7 +38,7 @@
 	}
 
 	/// Draw the partly finished constructionsite
-	void draw(const Vector2f& point_on_dst,
+	void draw(const Vector2f& point_on_dst, const Coords& coords,
 	          float scale,
 	          const RGBColor& player_color,
 	          RenderTarget* dst) const;
@@ -78,7 +78,11 @@
 
 	Building& create_object() const override;
 
+	FxId creation_fx() const;
+
 private:
+	const FxId creation_fx_;
+
 	DISALLOW_COPY_AND_ASSIGN(ConstructionSiteDescr);
 };
 
@@ -122,7 +126,7 @@
 
 	void draw(uint32_t gametime,
 	          TextToDraw draw_text,
-	          const Vector2f& point_on_dst,
+	          const Vector2f& point_on_dst, const Coords& coords,
 	          float scale,
 	          RenderTarget* dst) override;
 

=== modified file 'src/logic/map_objects/tribes/dismantlesite.cc'
--- src/logic/map_objects/tribes/dismantlesite.cc	2019-03-23 14:09:20 +0000
+++ src/logic/map_objects/tribes/dismantlesite.cc	2019-03-23 14:09:21 +0000
@@ -34,6 +34,8 @@
 #include "logic/game.h"
 #include "logic/map_objects/tribes/tribe_descr.h"
 #include "logic/map_objects/tribes/worker.h"
+#include "sound/note_sound.h"
+#include "sound/sound_handler.h"
 
 namespace Widelands {
 
@@ -45,7 +47,8 @@
 DismantleSiteDescr::DismantleSiteDescr(const std::string& init_descname,
                                        const LuaTable& table,
                                        const EditorGameBase& egbase)
-   : BuildingDescr(init_descname, MapObjectType::DISMANTLESITE, table, egbase) {
+   : BuildingDescr(init_descname, MapObjectType::DISMANTLESITE, table, egbase),
+	 creation_fx_(SoundHandler::register_fx(SoundType::kAmbient, "sound/create_construction_site")) {
 	add_attribute(MapObject::Attribute::CONSTRUCTIONSITE);  // Yep, this is correct.
 }
 
@@ -53,6 +56,10 @@
 	return *new DismantleSite(*this);
 }
 
+FxId DismantleSiteDescr::creation_fx() const {
+	return creation_fx_;
+}
+
 /*
 ==============================
 
@@ -106,6 +113,8 @@
 ===============
 */
 bool DismantleSite::init(EditorGameBase& egbase) {
+	Notifications::publish(NoteSound(SoundType::kAmbient, descr().creation_fx(), position_, kFxPriorityAlwaysPlay));
+
 	PartiallyFinishedBuilding::init(egbase);
 
 	for (const auto& ware : count_returned_wares(this)) {
@@ -219,16 +228,17 @@
 void DismantleSite::draw(uint32_t gametime,
                          const TextToDraw draw_text,
                          const Vector2f& point_on_dst,
+						 const Widelands::Coords& coords,
                          float scale,
                          RenderTarget* dst) {
 	uint32_t tanim = gametime - animstart_;
 	const RGBColor& player_color = get_owner()->get_playercolor();
 
 	// Draw the construction site marker
-	dst->blit_animation(point_on_dst, scale, anim_, tanim, &player_color);
+	dst->blit_animation(point_on_dst, Widelands::Coords::null(), scale, anim_, tanim, &player_color);
 
 	// Blit bottom part of the animation according to dismantle progress
-	dst->blit_animation(point_on_dst, scale, building_->get_unoccupied_animation(), tanim,
+	dst->blit_animation(point_on_dst, coords, scale, building_->get_unoccupied_animation(), tanim,
 	                    &player_color, 100 - ((get_built_per64k() * 100) >> 16));
 
 	// Draw help strings

=== modified file 'src/logic/map_objects/tribes/dismantlesite.h'
--- src/logic/map_objects/tribes/dismantlesite.h	2019-02-27 17:19:00 +0000
+++ src/logic/map_objects/tribes/dismantlesite.h	2019-03-23 14:09:21 +0000
@@ -53,7 +53,11 @@
 
 	Building& create_object() const override;
 
+	FxId creation_fx() const;
+
 private:
+	const FxId creation_fx_;
+
 	DISALLOW_COPY_AND_ASSIGN(DismantleSiteDescr);
 };
 
@@ -90,6 +94,7 @@
 	void draw(uint32_t gametime,
 	          TextToDraw draw_text,
 	          const Vector2f& point_on_dst,
+			  const Widelands::Coords& coords,
 	          float scale,
 	          RenderTarget* dst) override;
 };

=== modified file 'src/logic/map_objects/tribes/partially_finished_building.cc'
--- src/logic/map_objects/tribes/partially_finished_building.cc	2019-02-23 11:00:49 +0000
+++ src/logic/map_objects/tribes/partially_finished_building.cc	2019-03-23 14:09:21 +0000
@@ -26,7 +26,6 @@
 #include "logic/map_objects/tribes/tribe_descr.h"
 #include "logic/map_objects/tribes/worker.h"
 #include "logic/player.h"
-#include "sound/note_sound.h"
 
 namespace Widelands {
 
@@ -69,10 +68,10 @@
 bool PartiallyFinishedBuilding::init(EditorGameBase& egbase) {
 	Building::init(egbase);
 
-	if (upcast(Game, game, &egbase))
+	if (upcast(Game, game, &egbase)) {
 		request_builder(*game);
+	}
 
-	Notifications::publish(NoteSound("create_construction_site", position_, 255));
 	return true;
 }
 

=== modified file 'src/logic/map_objects/tribes/production_program.cc'
--- src/logic/map_objects/tribes/production_program.cc	2019-02-23 11:00:49 +0000
+++ src/logic/map_objects/tribes/production_program.cc	2019-03-23 14:09:21 +0000
@@ -1409,9 +1409,9 @@
 ProductionProgram::ActPlaySound::ActPlaySound(char* parameters) {
 	try {
 		bool reached_end;
-		const std::string& filepath = next_word(parameters, reached_end);
-		const std::string& filename = next_word(parameters, reached_end);
-		name = filepath + g_fs->file_separator() + filename;
+
+		const char* const name = next_word(parameters, reached_end);
+		fx = SoundHandler::register_fx(SoundType::kAmbient, name);
 
 		if (!reached_end) {
 			char* endp;
@@ -1419,17 +1419,20 @@
 			priority = value;
 			if (*endp || priority != value)
 				throw GameDataError("expected %s but found \"%s\"", "priority", parameters);
-		} else
-			priority = 127;
-
-		g_sound_handler.load_fx_if_needed(filepath, filename, name);
+		} else {
+			priority = kFxPriorityAllowMultiple - 1;
+		}
+		if (priority < kFxPriorityLowest) {
+			throw GameDataError("Minmum priority for sounds is %d, but only %d was specified for %s",
+								kFxPriorityLowest, priority, name);
+		}
 	} catch (const WException& e) {
 		throw GameDataError("playsound: %s", e.what());
 	}
 }
 
 void ProductionProgram::ActPlaySound::execute(Game& game, ProductionSite& ps) const {
-	Notifications::publish(NoteSound(name, ps.position_, priority));
+	Notifications::publish(NoteSound(SoundType::kAmbient, fx, ps.position_, priority));
 	return ps.program_step(game);
 }
 

=== modified file 'src/logic/map_objects/tribes/production_program.h'
--- src/logic/map_objects/tribes/production_program.h	2019-02-23 11:00:49 +0000
+++ src/logic/map_objects/tribes/production_program.h	2019-03-23 14:09:21 +0000
@@ -503,7 +503,7 @@
 		void execute(Game&, ProductionSite&) const override;
 
 	private:
-		std::string name;
+		FxId fx;
 		uint8_t priority;
 	};
 

=== modified file 'src/logic/map_objects/tribes/ship.cc'
--- src/logic/map_objects/tribes/ship.cc	2019-02-27 17:19:00 +0000
+++ src/logic/map_objects/tribes/ship.cc	2019-03-23 14:09:21 +0000
@@ -979,10 +979,11 @@
 
 void Ship::draw(const EditorGameBase& egbase,
                 const TextToDraw& draw_text,
-                const Vector2f& field_on_dst,
+                const Vector2f& point_on_dst,
+				const Widelands::Coords& coords,
                 const float scale,
                 RenderTarget* dst) const {
-	Bob::draw(egbase, draw_text, field_on_dst, scale, dst);
+	Bob::draw(egbase, draw_text, point_on_dst, coords, scale, dst);
 
 	// Show ship name and current activity
 	std::string statistics_string;
@@ -1024,7 +1025,7 @@
 		                       .str();
 	}
 
-	do_draw_info(draw_text, shipname_, statistics_string, calc_drawpos(egbase, field_on_dst, scale),
+	do_draw_info(draw_text, shipname_, statistics_string, calc_drawpos(egbase, point_on_dst, scale),
 	             scale, dst);
 }
 

=== modified file 'src/logic/map_objects/tribes/ship.h'
--- src/logic/map_objects/tribes/ship.h	2019-02-27 17:19:00 +0000
+++ src/logic/map_objects/tribes/ship.h	2019-03-23 14:09:21 +0000
@@ -234,7 +234,7 @@
 protected:
 	void draw(const EditorGameBase&,
 	          const TextToDraw& draw_text,
-	          const Vector2f& field_on_dst,
+	          const Vector2f& point_on_dst, const Coords& coords,
 	          float scale,
 	          RenderTarget* dst) const override;
 

=== modified file 'src/logic/map_objects/tribes/soldier.cc'
--- src/logic/map_objects/tribes/soldier.cc	2019-02-28 12:22:36 +0000
+++ src/logic/map_objects/tribes/soldier.cc	2019-03-23 14:09:21 +0000
@@ -440,8 +440,8 @@
  */
 void Soldier::draw(const EditorGameBase& game,
                    const TextToDraw&,
-                   const Vector2f& field_on_dst,
-                   const float scale,
+                   const Vector2f& field_on_dst, const Coords& coords,
+                   float scale,
                    RenderTarget* dst) const {
 	const uint32_t anim = get_current_anim();
 	if (!anim) {
@@ -453,7 +453,7 @@
 	   point_on_dst.cast<int>() -
 	      Vector2i(0, (g_gr->animations().get_animation(get_current_anim()).height() - 7) * scale),
 	   scale, true, dst);
-	draw_inner(game, point_on_dst, scale, dst);
+	draw_inner(game, point_on_dst, coords, scale, dst);
 }
 
 /**

=== modified file 'src/logic/map_objects/tribes/soldier.h'
--- src/logic/map_objects/tribes/soldier.h	2019-02-27 17:19:00 +0000
+++ src/logic/map_objects/tribes/soldier.h	2019-03-23 14:09:21 +0000
@@ -211,6 +211,7 @@
 	void draw(const EditorGameBase&,
 	          const TextToDraw& draw_text,
 	          const Vector2f& point_on_dst,
+			  const Widelands::Coords& coords,
 	          float scale,
 	          RenderTarget* dst) const override;
 

=== modified file 'src/logic/map_objects/tribes/worker.cc'
--- src/logic/map_objects/tribes/worker.cc	2019-03-23 14:09:20 +0000
+++ src/logic/map_objects/tribes/worker.cc	2019-03-23 14:09:21 +0000
@@ -995,11 +995,11 @@
 }
 
 /**
- * Demand from the g_sound_handler to play a certain sound effect.
+ * Demand from the g_sh to play a certain sound effect.
  * Whether the effect actually gets played is decided only by the sound server.
  */
 bool Worker::run_playsound(Game& game, State& state, const Action& action) {
-	Notifications::publish(NoteSound(action.sparam1, get_position(), action.iparam1));
+	Notifications::publish(NoteSound(SoundType::kAmbient, action.iparam2, get_position(), action.iparam1));
 
 	++state.ivar1;
 	schedule_act(game, 10);
@@ -2984,21 +2984,21 @@
 }
 
 void Worker::draw_inner(const EditorGameBase& game,
-                        const Vector2f& point_on_dst,
+                        const Vector2f& point_on_dst, const Coords& coords,
                         const float scale,
                         RenderTarget* dst) const {
 	assert(get_owner() != nullptr);
 	const RGBColor& player_color = get_owner()->get_playercolor();
 
 	dst->blit_animation(
-	   point_on_dst, scale, get_current_anim(), game.get_gametime() - get_animstart(), &player_color);
+	   point_on_dst, coords, scale, get_current_anim(), game.get_gametime() - get_animstart(), &player_color);
 
 	if (WareInstance const* const carried_ware = get_carried_ware(game)) {
 		const Vector2f hotspot = descr().ware_hotspot().cast<float>();
 		const Vector2f location(
 		   point_on_dst.x - hotspot.x * scale, point_on_dst.y - hotspot.y * scale);
 		dst->blit_animation(
-		   location, scale, carried_ware->descr().get_animation("idle"), 0, &player_color);
+		   location, Widelands::Coords::null(), scale, carried_ware->descr().get_animation("idle"), 0, &player_color);
 	}
 }
 
@@ -3008,12 +3008,13 @@
 void Worker::draw(const EditorGameBase& egbase,
                   const TextToDraw&,
                   const Vector2f& field_on_dst,
+				  const Widelands::Coords& coords,
                   const float scale,
                   RenderTarget* dst) const {
 	if (!get_current_anim()) {
 		return;
 	}
-	draw_inner(egbase, calc_drawpos(egbase, field_on_dst, scale), scale, dst);
+	draw_inner(egbase, calc_drawpos(egbase, field_on_dst, scale), coords, scale, dst);
 }
 
 /*

=== modified file 'src/logic/map_objects/tribes/worker.h'
--- src/logic/map_objects/tribes/worker.h	2019-02-27 17:19:00 +0000
+++ src/logic/map_objects/tribes/worker.h	2019-03-23 14:09:21 +0000
@@ -179,11 +179,13 @@
 	virtual bool is_evict_allowed();
 	virtual void draw_inner(const EditorGameBase& game,
 	                        const Vector2f& point_on_dst,
+							const Widelands::Coords& coords,
 	                        const float scale,
 	                        RenderTarget* dst) const;
 	void draw(const EditorGameBase&,
 	          const TextToDraw& draw_text,
 	          const Vector2f& field_on_dst,
+			  const Widelands::Coords& coords,
 	          float scale,
 	          RenderTarget* dst) const override;
 	void init_auto_task(Game&) override;

=== modified file 'src/logic/map_objects/tribes/worker_program.cc'
--- src/logic/map_objects/tribes/worker_program.cc	2019-02-23 11:00:49 +0000
+++ src/logic/map_objects/tribes/worker_program.cc	2019-03-23 14:09:21 +0000
@@ -160,7 +160,7 @@
       harvest = {
          "findobject=attrib:ripe_wheat radius:2",
          "walk=object",
-         "playsound=sound/farm scythe 220",
+         "playsound=sound/farm/scythe 220",
          "animate=harvesting 10000",
          "callobject=harvest",
          "animate=gathering 4000",
@@ -194,10 +194,10 @@
       fish = {
          "findspace=size:any radius:7 resource:fish",
          "walk=coords",
-         "playsound=sound/fisher fisher_throw_net 192",
+         "playsound=sound/fisher/fisher_throw_net 192",
          "mine=fish 1", -- Remove a fish in an area of 1
          "animate=fishing 3000",
-         "playsound=sound/fisher fisher_pull_net 192",
+         "playsound=sound/fisher/fisher_pull_net 192",
          "createware=fish",
          "return"
       },
@@ -273,7 +273,7 @@
          "findobject=attrib:rocks radius:6", -- Find rocks on the map within a radius of 6 from your
          building
          "walk=object", -- Now walk to those rocks
-         "playsound=sound/atlanteans/cutting stonecutter 192",
+         "playsound=sound/atlanteans/cutting/stonecutter 192",
          "animate=hacking 12000",
          "callobject=shrink",
          "createware=granite",
@@ -515,7 +515,7 @@
          "walk=object-or-coords", -- Walk to coordinates from 1. or to object from 2.
          -- 2. This will create an object for us if we don't have one yet
          "plant=attrib:shipconstruction unless object",
-         "playsound=sound/sawmill sawmill 230",
+         "playsound=sound/sawmill/sawmill 230",
          "animate=work 500",
          "construct", -- 1. This will find a space for us if no object has been planted yet
          "animate=work 5000",
@@ -618,9 +618,9 @@
       harvest = {
          "findobject=attrib:tree radius:10",
          "walk=object",
-         "playsound=sound/woodcutting fast_woodcutting 250",
+         "playsound=sound/woodcutting/fast_woodcutting 250",
          "animate=hacking 10000",
-         "playsound=sound/woodcutting tree-falling 130",
+         "playsound=sound/woodcutting/tree-falling 130",
          "callobject=fall", -- Cause the tree to fall
          "animate=idle 2000",
          "createware=log",
@@ -676,7 +676,7 @@
          "walk=object-or-coords",
          -- Only create a shipconstruction if we don't already have one
          "plant=attrib:shipconstruction unless object",
-         "playsound=sound/sawmill sawmill 230",
+         "playsound=sound/sawmill/sawmill 230",
          "animate=work 500",
          "construct",
          "animate=work 5000",
@@ -830,7 +830,7 @@
       search = {
          "animate=hacking 5000",
          "animate=idle 2000",
-         "playsound=sound/hammering geologist_hammer 192",
+         "playsound=sound/hammering/geologist_hammer 192",
          "animate=hacking 3000",
          -- Plant a resource marker at the current location, according to what has been found.
          "findresources"
@@ -875,13 +875,12 @@
 /* RST
 playsound
 ^^^^^^^^^^
-.. function:: playsound=\<sound_dir\> \<sound_name\> [priority]
+.. function:: playsound=\<sound_dir/sound_name\> [priority]
 
-   :arg string sound_dir: The directory (folder) that the sound files are in,
-      relative to the data directory.
-   :arg string sound_name: The name of the particular sound to play.
+   :arg string sound_dir/sound_name: The directory (folder) that the sound files are in,
+      relative to the data directory, followed by the name of the particular sound to play.
       There can be multiple sound files to select from at random, e.g.
-      for `scythe`, we can have `scythe_00.ogg`, `scythe_01.ogg` ...
+      for `sound/farm/scythe`, we can have `sound/farm/scythe_00.ogg`, `sound/farm/scythe_01.ogg` ...
 
    :arg int priority: The priority to give this sound. Maximum priority is 255.
 
@@ -890,7 +889,7 @@
       harvest = {
          "findobject=attrib:ripe_wheat radius:2",
          "walk=object",
-         "playsound=sound/farm scythe 220", -- Almost certainly play a swishy harvesting sound
+         "playsound=sound/farm/scythe 220", -- Almost certainly play a swishy harvesting sound
          "animate=harvesting 10000",
          "callobject=harvest",
          "animate=gathering 4000",
@@ -902,13 +901,14 @@
 	if (cmd.size() < 3 || cmd.size() > 4)
 		throw wexception("Usage: playsound <sound_dir> <sound_name> [priority]");
 
-	act->sparam1 = cmd[1] + "/" + cmd[2];
-
-	g_sound_handler.load_fx_if_needed(cmd[1], cmd[2], act->sparam1);
+	act->iparam2 = SoundHandler::register_fx(SoundType::kAmbient, cmd[1]);
 
 	act->function = &Worker::run_playsound;
-	act->iparam1 = cmd.size() == 3 ? 64 :  //  50% chance to play, only one instance at a time
-	                  atoi(cmd[3].c_str());
+	act->iparam1 = cmd.size() == 2 ? kFxPriorityMedium : atoi(cmd[2].c_str());
+	if (act->iparam1 < kFxPriorityLowest) {
+		throw GameDataError("Minmum priority for sounds is %d, but only %d was specified for %s",
+							kFxPriorityLowest, act->iparam1, cmd[1].c_str());
+	}
 }
 
 /* RST
@@ -923,7 +923,7 @@
          "walk=object-or-coords", -- Walk to coordinates from 1. or to object from 2.
          -- 2. This will create an object for us if we don't have one yet
          "plant=attrib:shipconstruction unless object",
-         "playsound=sound/sawmill sawmill 230",
+         "playsound=sound/sawmill/sawmill 230",
          "animate=work 500",
          -- 1. Add the current ware to the shipconstruction. This will find a space for us if no
          -- shipconstruction object has been planted yet

=== modified file 'src/logic/message.h'
--- src/logic/message.h	2019-02-23 11:00:49 +0000
+++ src/logic/message.h	2019-03-23 14:09:21 +0000
@@ -108,7 +108,7 @@
 	const std::string& body() const {
 		return body_;
 	}
-	Widelands::Coords position() const {
+	const Widelands::Coords& position() const {
 		return position_;
 	}
 	Widelands::Serial serial() const {

=== modified file 'src/logic/player.cc'
--- src/logic/player.cc	2019-03-09 08:58:52 +0000
+++ src/logic/player.cc	2019-03-23 14:09:21 +0000
@@ -54,6 +54,7 @@
 #include "logic/playercommand.h"
 #include "scripting/lua_table.h"
 #include "sound/note_sound.h"
+#include "sound/sound_handler.h"
 #include "wui/interactive_player.h"
 
 namespace {
@@ -143,7 +144,10 @@
      current_consumed_statistics_(the_egbase.tribes().nrwares()),
      ware_productions_(the_egbase.tribes().nrwares()),
      ware_consumptions_(the_egbase.tribes().nrwares()),
-     ware_stocks_(the_egbase.tribes().nrwares()) {
+     ware_stocks_(the_egbase.tribes().nrwares()),
+     message_fx_(SoundHandler::register_fx(SoundType::kMessage, "sound/message")),
+     attack_fx_(SoundHandler::register_fx(SoundType::kMessage, "sound/military/under_attack")),
+     occupied_fx_(SoundHandler::register_fx(SoundType::kMessage, "sound/military/site_occupied")) {
 	set_name(name);
 
 	// Disallow workers that the player's tribe doesn't have.
@@ -321,17 +325,20 @@
  * Plays the corresponding sound when a message is received and if sound is
  * enabled.
  */
-void Player::play_message_sound(const Message::Type& msgtype) {
-#define MAYBE_PLAY(type, file)                                                                     \
-	if (msgtype == type) {                                                                          \
-		Notifications::publish(NoteSound(file, 200, PRIO_ALWAYS_PLAY));                              \
-		return;                                                                                      \
-	}
-
-	if (g_options.pull_section("global").get_bool("sound_at_message", true)) {
-		MAYBE_PLAY(Message::Type::kEconomySiteOccupied, "military/site_occupied");
-		MAYBE_PLAY(Message::Type::kWarfareUnderAttack, "military/under_attack");
-		Notifications::publish(NoteSound("message", 200, PRIO_ALWAYS_PLAY));
+void Player::play_message_sound(const Message* message) {
+	if (g_sh->is_sound_enabled(SoundType::kMessage)) {
+		FxId fx;
+		switch (message->type()) {
+			case Message::Type::kEconomySiteOccupied:
+			fx = occupied_fx_;
+			break;
+		case Message::Type::kWarfareUnderAttack:
+			fx = attack_fx_;
+			break;
+		default:
+			fx = message_fx_;
+		}
+		Notifications::publish(NoteSound(SoundType::kMessage, fx, message->position(), kFxPriorityAlwaysPlay));
 	}
 }
 
@@ -348,7 +355,7 @@
 	// Sound & popup
 	if (InteractivePlayer* const iplayer = game.get_ipl()) {
 		if (&iplayer->player() == this) {
-			play_message_sound(message->type());
+			play_message_sound(message);
 			if (popup)
 				iplayer->popup_message(id, *message);
 		}

=== modified file 'src/logic/player.h'
--- src/logic/player.h	2019-03-09 08:58:52 +0000
+++ src/logic/player.h	2019-03-23 14:09:21 +0000
@@ -37,6 +37,7 @@
 #include "logic/message_queue.h"
 #include "logic/see_unsee_node.h"
 #include "logic/widelands.h"
+#include "sound/constants.h"
 
 class Node;
 namespace Widelands {
@@ -609,7 +610,7 @@
 	BuildingStatsVector* get_mutable_building_statistics(const DescriptionIndex& i);
 	void update_building_statistics(Building&, NoteImmovable::Ownership ownership);
 	void update_team_players();
-	void play_message_sound(const Message::Type& msgtype);
+	void play_message_sound(const Message* message);
 	void enhance_or_dismantle(Building*, DescriptionIndex index_of_new_building);
 
 	// Called when a node becomes seen or has changed.  Discovers the node and
@@ -682,6 +683,10 @@
 
 	PlayerBuildingStats building_stats_;
 
+	FxId message_fx_;
+	FxId attack_fx_;
+	FxId occupied_fx_;
+
 	DISALLOW_COPY_AND_ASSIGN(Player);
 };
 

=== modified file 'src/sound/CMakeLists.txt'
--- src/sound/CMakeLists.txt	2017-06-11 08:27:02 +0000
+++ src/sound/CMakeLists.txt	2019-03-23 14:09:21 +0000
@@ -8,6 +8,13 @@
 )
 
 
+wl_library(sound_constants
+  SRCS
+    constants.cc
+    constants.h
+)
+
+
 wl_library(sound
   SRCS
     fxset.cc
@@ -27,4 +34,5 @@
     io_filesystem
     profile
     random
+    sound_constants
 )

=== added file 'src/sound/constants.cc'
--- src/sound/constants.cc	1970-01-01 00:00:00 +0000
+++ src/sound/constants.cc	2019-03-23 14:09:21 +0000
@@ -0,0 +1,1 @@
+// CMake cannot deal with header only libraries, therefore we need an empty cc file :(.

=== added file 'src/sound/constants.h'
--- src/sound/constants.h	1970-01-01 00:00:00 +0000
+++ src/sound/constants.h	2019-03-23 14:09:21 +0000
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2006-2019 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ */
+
+#ifndef WL_SOUND_CONSTANTS_H
+#define WL_SOUND_CONSTANTS_H
+
+#include <limits>
+#include <stdint.h>
+
+/* How important is it to play the effect even when others are running
+ * already?
+ *
+ * \warning DO NOT CHANGE !! The values have meaning beyond just being numbers
+ *
+ * Value 0-127: probability between 0.0 and 1.0, only one instance can
+ * be playing at any time
+ *
+ * Value 128-254: probability between 0.0 and 1.0, many instances can
+ * be playing at any time
+ *
+ * Value 255: always play; unconditional
+ */
+
+/// Priorities lower than this one are illegal
+constexpr uint8_t kFxPriorityLowest = 1;
+/// 50% chance to play
+constexpr uint8_t kFxPriorityMedium = 64;
+/// Sounds with priority lower than this one are only allowed to place one instance at a time
+constexpr uint8_t kFxPriorityAllowMultiple = 128;
+/// Sound will always play
+constexpr uint8_t kFxPriorityAlwaysPlay = 255;
+
+constexpr int32_t kStereoLeft = 0;
+constexpr int32_t kStereoCenter = 128;
+constexpr int32_t kStereoRight = 254;
+
+using FxId = uint16_t;
+constexpr FxId kNoSoundEffect = std::numeric_limits<uint16_t>::max();
+
+/// Categorize sound effects and music to control their volume etc.
+enum class SoundType {
+	kUI,
+	kMessage,
+	kChat,
+	kAmbient,
+	kMusic
+};
+
+#endif  // end of include guard: WL_SOUND_CONSTANTS_H

=== modified file 'src/sound/fxset.cc'
--- src/sound/fxset.cc	2019-02-23 11:00:49 +0000
+++ src/sound/fxset.cc	2019-03-23 14:09:21 +0000
@@ -22,13 +22,45 @@
 #include <cassert>
 
 #include <SDL.h>
-
-#include "sound/sound_handler.h"
-
-/** Create an FXset and set it's \ref priority_
- * \param[in] prio  The desired priority (optional)
+#include <boost/regex.hpp>
+
+#include "base/log.h"
+#include "helper.h"
+#include "io/fileread.h"
+#include "io/filesystem/layered_filesystem.h"
+#include "logic/game_data_error.h"
+
+/**
+ * Create an FXset
+ * \param path The directory the sound files are in, followed by the filename base
+ * \param random: Randomize the time last played a bit to prevent sound onslaught at game start
  */
-FXset::FXset(uint8_t const priority) : last_used_(0), priority_(priority) {
+FXset::FXset(const std::string& path, uint32_t random) : last_used_(random % 2000) {
+	// Check directory
+	std::string directory = FileSystem::fs_dirname(path);
+	if (!g_fs->is_directory(directory)) {
+		throw Widelands::GameDataError("SoundHandler: Can't load files from %s, not a directory!", directory.c_str());
+	}
+
+	// Find files
+	std::string base_filename = FileSystem::fs_filename(path.c_str());
+	boost::regex re(base_filename + "_\\d+\\.ogg");
+	paths_ = filter(g_fs->list_directory(directory), [&re](const std::string& fn) {
+		return boost::regex_match(FileSystem::fs_filename(fn.c_str()), re);
+	});
+
+	// Ensure that we have at least 1 file
+	if (paths_.empty()) {
+		throw Widelands::GameDataError("FXset: No files matching the pattern '%s_<numbers>.ogg' found in directory %s\n",
+							base_filename.c_str(), directory.c_str());
+	}
+
+#ifndef NDEBUG
+	// Ensure that we haven't found any directories by mistake
+	for (const std::string& p : paths_) {
+		assert(!g_fs->is_directory(p));
+	}
+#endif
 }
 
 /// Delete all fxs to avoid memory leaks. This also frees the audio data.
@@ -43,27 +75,49 @@
 	fxs_.clear();
 }
 
-/** Append a sound effect to the end of the fxset
- * \param[in] fx    The sound fx to append
- * \param[in] prio  Set previous \ref priority_ to new value (optional)
- */
-void FXset::add_fx(Mix_Chunk* const fx, uint8_t const prio) {
-	assert(fx);
-
-	priority_ = prio;
-	fxs_.push_back(fx);
+uint32_t FXset::ticks_since_last_play() const {
+	return SDL_GetTicks() - last_used_;
 }
 
-/** Get a sound effect from the fxset. \e Which variant of the fx is actually
- * given out is determined at random
- * \return  a pointer to the chosen effect; 0 if sound effects are
- * disabled or no fx is registered
- */
-Mix_Chunk* FXset::get_fx() {
-	if (g_sound_handler.get_disable_fx() || fxs_.empty())
+Mix_Chunk* FXset::get_fx(uint32_t random) {
+	if (!paths_.empty()) {
+		// Load sounds from paths if this FX hasn't been played yet
+		for (const std::string& path : paths_) {
+			load_sound_file(path);
+		}
+		assert(fxs_.size() == paths_.size());
+		// We don't need the paths any more
+		paths_.clear();
+	}
+
+	assert(paths_.empty());
+
+	if (fxs_.empty()) {
 		return nullptr;
+	}
+
+	assert(!fxs_.empty());
 
 	last_used_ = SDL_GetTicks();
 
-	return fxs_.at(g_sound_handler.rng_.rand() % fxs_.size());
+	return fxs_.at(random % fxs_.size());
+}
+
+
+void FXset::load_sound_file(const std::string& path) {
+	FileRead fr;
+	if (!fr.try_open(*g_fs, path)) {
+		log("WARNING: Could not open %s for reading!\n", path.c_str());
+		return;
+	}
+
+	if (Mix_Chunk* const m =
+	       Mix_LoadWAV_RW(SDL_RWFromMem(fr.data(fr.get_size(), 0), fr.get_size()), 1)) {
+		// Append a sound effect to the end of the fxset
+		assert(m);
+		fxs_.push_back(m);
+	} else {
+		log("FXset: loading sound effect file \"%s\" failed: %s\n",
+		    path.c_str(), Mix_GetError());
+	}
 }

=== modified file 'src/sound/fxset.h'
--- src/sound/fxset.h	2019-02-23 11:00:49 +0000
+++ src/sound/fxset.h	2019-03-23 14:09:21 +0000
@@ -20,61 +20,58 @@
 #ifndef WL_SOUND_FXSET_H
 #define WL_SOUND_FXSET_H
 
+#include <set>
+#include <string>
 #include <vector>
 
 #include <SDL_mixer.h>
 
-class SoundHandler;
-
-/// Predefined priorities for easy reading
-/// \warning DO NOT CHANGE !! The values have meaning beyond just being numbers
-
-// TODO(unknown): These values should not have any meaning beyond just being numbers.
-
-#define PRIO_ALWAYS_PLAY 255
-#define PRIO_ALLOW_MULTIPLE 128
-#define PRIO_MEDIUM 63
-
 /** A collection of several sound effects meant for the same event.
  *
  * An FXset encapsulates a number of interchangeable sound effects, e.g.
  * all effects that might be played when a blacksmith is happily hammering away.
- * It is possible to select the effects one after another or in random order.
  * The fact that an FXset really contains several different effects is hidden
  * from the outside
  */
 struct FXset {
-	friend class SoundHandler;
-	explicit FXset(uint8_t priority = PRIO_MEDIUM);
+	explicit FXset(const std::string& path, uint32_t random);
 	~FXset();
 
-	void add_fx(Mix_Chunk* fx, uint8_t prio = PRIO_MEDIUM);
-	Mix_Chunk* get_fx();
-	bool empty() {
-		return fxs_.empty();
-	}
-
-protected:
-	/// The collection of sound effects
-	std::vector<Mix_Chunk*> fxs_;
+	/**
+	 * Number of ticks since this FXSet was last played
+	 */
+	uint32_t ticks_since_last_play() const;
+
+	/** Get a sound effect from the fxset. Load the audio on demand.
+	 * \param random A random number for picking a variant
+	 * \return  a pointer to the chosen effect; 0 if sound effects are
+	 * disabled or no fx is registered
+	 */
+	Mix_Chunk* get_fx(uint32_t random);
+
+private:
+	/** Load an audio file into memory.
+	 * \param path      the effect to be loaded
+	 * The file format must be ogg. Otherwise this call will complain and
+	 * not load the file.
+	 * \note The complete audio file will be loaded into memory and stays there
+	 * until the game is finished.
+	 */
+	void load_sound_file(const std::string& path);
 
 	/** When the effect was played the last time (milliseconds since SDL
 	 * initialization). Set via SDL_GetTicks()
 	 */
 	uint32_t last_used_;
 
-	/** How important is it to play the effect even when others are running
-	 * already?
-	 *
-	 * Value 0-127: probability between 0.0 and 1.0, only one instance can
-	 * be playing at any time
-	 *
-	 * Value 128-254: probability between 0.0 and 1.0, many instances can
-	 * be playing at any time
-	 *
-	 * Value 255: always play; unconditional
-	 */
-	uint8_t priority_;
+	/**
+	* Filename paths for the physical sound files
+	* This will be cleared when the effects have been loaded into memory by \ref get_fx on first play.
+	*/
+	std::set<std::string> paths_;
+
+	/// The collection of sound effects, to be loaded on demand
+	std::vector<Mix_Chunk*> fxs_;
 };
 
 #endif  // end of include guard: WL_SOUND_FXSET_H

=== modified file 'src/sound/note_sound.h'
--- src/sound/note_sound.h	2019-02-23 11:00:49 +0000
+++ src/sound/note_sound.h	2019-03-23 14:09:21 +0000
@@ -25,25 +25,20 @@
 #include "logic/widelands_geometry.h"
 #include "notifications/note_ids.h"
 #include "notifications/notifications.h"
+#include "sound/constants.h"
 
 struct NoteSound {
 	CAN_BE_SENT_AS_NOTE(NoteId::Sound)
-	const std::string fx;
+	const SoundType type;
+	const FxId fx;
 	const Widelands::Coords coords;
 	const uint8_t priority;
-	const uint32_t stereo_position;
 
-	NoteSound(const std::string& init_fx, Widelands::Coords init_coords, uint8_t init_priority)
-	   : fx(init_fx),
+	NoteSound(SoundType init_type, FxId init_fx, Widelands::Coords init_coords, uint8_t init_priority)
+	   : type(init_type),
+		 fx(init_fx),
 	     coords(init_coords),
-	     priority(init_priority),
-	     stereo_position(std::numeric_limits<uint32_t>::max()) {
-	}
-	NoteSound(const std::string& init_fx, uint32_t init_stereo_position, uint8_t init_priority)
-	   : fx(init_fx),
-	     coords(Widelands::Coords::null()),
-	     priority(init_priority),
-	     stereo_position(init_stereo_position) {
+	     priority(init_priority) {
 	}
 };
 

=== modified file 'src/sound/songset.cc'
--- src/sound/songset.cc	2019-02-23 11:00:49 +0000
+++ src/sound/songset.cc	2019-03-23 14:09:21 +0000
@@ -21,13 +21,26 @@
 
 #include <utility>
 
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/regex.hpp>
+
 #include "base/log.h"
+#include "helper.h"
 #include "io/fileread.h"
 #include "io/filesystem/layered_filesystem.h"
-#include "sound/sound_handler.h"
-
-/// Prepare infrastructure for reading song files from disk
-Songset::Songset() : m_(nullptr), rwops_(nullptr) {
+
+/// Prepare infrastructure for reading song files from disk and register the matching files
+Songset::Songset(const std::string& dir, const std::string& basename) : m_(nullptr), rwops_(nullptr) {
+	assert(g_fs);
+	FilenameSet files = filter(g_fs->list_directory(dir), [&basename](const std::string& fn) {
+		const std::string only_filename = FileSystem::fs_filename(fn.c_str());
+		return boost::starts_with(only_filename, basename) && boost::ends_with(only_filename, ".ogg");
+	});
+
+	for (const std::string& filename : files) {
+		assert(!g_fs->is_directory(filename));
+		add_song(filename);
+	}
 }
 
 /// Close and delete all songs to avoid memory leaks.
@@ -54,27 +67,25 @@
 	current_song_ = 0;
 }
 
-/** Get a song from the songset. Depending on
- * \ref SoundHandler::sound_random_order, the selection will either be random
- * or linear (after last song, will start again with first).
- * \return  a pointer to the chosen song; 0 if none was found, music is disabled
+/**
+ * Uses a 'random' number to select a song and return its audio data.
+ * \param random A random number for picking the song
+ * \return  a pointer to the chosen song; nullptr if none was found
  *          or an error occurred
  */
-Mix_Music* Songset::get_song() {
+Mix_Music* Songset::get_song(uint32_t random) {
 	std::string filename;
 
-	if (g_sound_handler.get_disable_music() || songs_.empty())
+	if (songs_.empty()) {
 		return nullptr;
+	}
 
 	if (songs_.size() > 1) {
-		if (g_sound_handler.random_order_) {
-			// exclude current_song from playing two times in a row
-			current_song_ += 1 + g_sound_handler.rng_.rand() % (songs_.size() - 1);
-		} else {
-			++current_song_;
-		}
+		// Exclude current_song from playing two times in a row
+		current_song_ += 1 + random % (songs_.size() - 1);
 		current_song_ = current_song_ % songs_.size();
 	}
+
 	filename = songs_.at(current_song_);
 
 	// First, close the previous song and remove it from memory
@@ -102,10 +113,10 @@
 		m_ = Mix_LoadMUS_RW(rwops_, 0);
 
 	if (m_)
-		log("SoundHandler: loaded song \"%s\"\n", filename.c_str());
+		log("Songset: Loaded song \"%s\"\n", filename.c_str());
 	else {
-		log("SoundHandler: loading song \"%s\" failed!\n", filename.c_str());
-		log("SoundHandler: %s\n", Mix_GetError());
+		log("Songset: Loading song \"%s\" failed!\n", filename.c_str());
+		log("Songset: %s\n", Mix_GetError());
 	}
 
 	return m_;

=== modified file 'src/sound/songset.h'
--- src/sound/songset.h	2019-02-23 11:00:49 +0000
+++ src/sound/songset.h	2019-03-23 14:09:21 +0000
@@ -32,24 +32,20 @@
  *
  * A Songset encapsulates a number of interchangeable pieces of (background)
  * music, e.g. all songs that might be played while the main menu is being
- * shown. It is possible to access those songs one after another or in
- * random order. The fact that a Songset really contains several different
- * songs is hidden from the outside.
+ * shown.
  * A songset does not contain the audio data itself, to not use huge amounts of
  * memory. Instead, each song is loaded on request and the data is free()d
  * afterwards
  */
 struct Songset {
-	Songset();
+	explicit Songset(const std::string& dir, const std::string& basename);
 	~Songset();
 
+	Mix_Music* get_song(uint32_t random);
+
+private:
 	void add_song(const std::string& filename);
-	Mix_Music* get_song();
-	bool empty() {
-		return songs_.empty();
-	}
 
-protected:
 	/// The filenames of all configured songs
 	std::vector<std::string> songs_;
 

=== modified file 'src/sound/sound_handler.cc'
--- src/sound/sound_handler.cc	2019-02-23 11:00:49 +0000
+++ src/sound/sound_handler.cc	2019-03-23 14:09:21 +0000
@@ -24,71 +24,48 @@
 
 #include <SDL.h>
 #include <SDL_mixer.h>
-#include <boost/algorithm/string/predicate.hpp>
-#include <boost/regex.hpp>
 #ifdef _WIN32
 #include <windows.h>
 #endif
 
 #include "base/i18n.h"
 #include "base/log.h"
-#include "helper.h"
-#include "io/fileread.h"
-#include "io/filesystem/layered_filesystem.h"
 #include "profile/profile.h"
-#include "sound/songset.h"
 
 namespace {
-
 constexpr int kDefaultMusicVolume = 64;
 constexpr int kDefaultFxVolume = 128;
 constexpr int kNumMixingChannels = 32;
-
-void report_initalization_error(const char* msg) {
-	log("WARNING: Failed to initialize sound system: %s\n", msg);
-	return;
-}
-
 }  // namespace
 
-/** The global \ref SoundHandler object
- * The sound handler is a static object because otherwise it'd be quite
- * difficult to pass the --nosound command line option
- */
-SoundHandler g_sound_handler;
-
-/** This is just a basic constructor. The \ref SoundHandler must already exist
- * during command line parsing because --nosound needs to be known. At this
- * time, however, all other information is still unknown, so a real
- * initialization cannot take place.
- * \sa SoundHandler::init()
+
+/// The global \ref SoundHandler object
+SoundHandler* g_sh;
+
+bool SoundHandler::backend_is_disabled_ = false;
+
+/**
+ * Initialize our data structures, and if SoundHandler::is_backend_disabled() is false, initialize the SDL sound system and configure everything.
  */
 SoundHandler::SoundHandler()
-   : nosound_(false),
-     is_backend_disabled_(false),
-     disable_music_(false),
-     disable_fx_(false),
-     music_volume_(MIX_MAX_VOLUME),
-     fx_volume_(MIX_MAX_VOLUME),
-     random_order_(true),
+   : sound_options_{
+{SoundType::kUI, SoundOptions(kDefaultFxVolume, "ui")},
+{SoundType::kMessage, SoundOptions(kDefaultFxVolume, "message")},
+{SoundType::kChat, SoundOptions(kDefaultFxVolume, "chat")},
+{SoundType::kAmbient, SoundOptions(kDefaultFxVolume, "ambient")},
+{SoundType::kMusic, SoundOptions(kDefaultMusicVolume, "music")}},
      fx_lock_(nullptr) {
-}
-
-/// Housekeeping: unset hooks. Audio data will be freed automagically by the
-/// \ref Songset and \ref FXset destructors, but not the {song|fx}sets
-/// themselves.
-SoundHandler::~SoundHandler() {
-}
-
-/** The real initialization for SoundHandler.
- *
- * \see SoundHandler::SoundHandler()
- */
-void SoundHandler::init() {
+	// Ensure that we don't lose our config for when we start with sound the next time
 	read_config();
+
+	// No sound wanted, let's not do anything.
+	if (SoundHandler::is_backend_disabled()) {
+		return;
+	}
+
+	// This RNG will still be somewhat predictable, but it's just to avoid
+	// identical playback patterns
 	rng_.seed(SDL_GetTicks());
-// This RNG will still be somewhat predictable, but it's just to avoid
-// identical playback patterns
 
 // Windows Music has crickling inside if the buffer has another size
 // than 4k, but other systems work fine with less, some crash
@@ -105,11 +82,11 @@
 	log("SDL version: %d.%d.%d\n", static_cast<unsigned int>(sdl_version.major),
 	    static_cast<unsigned int>(sdl_version.minor), static_cast<unsigned int>(sdl_version.patch));
 
-	/// SDL 2.0.6 will crash due to upstream bug:
-	/// https://bugs.launchpad.net/ubuntu/+source/libsdl2/+bug/1722060
+	// SDL 2.0.6 will crash due to an upstream bug:
+	// https://bugs.launchpad.net/ubuntu/+source/libsdl2/+bug/1722060
 	if (sdl_version.major == 2 && sdl_version.minor == 0 && sdl_version.patch == 6) {
 		log("Disabled sound due to a bug in SDL 2.0.6\n");
-		nosound_ = true;
+		SoundHandler::disable_backend();
 	}
 
 	SDL_MIXER_VERSION(&sdl_version);
@@ -118,77 +95,79 @@
 
 	log("**** END SOUND REPORT ****\n");
 
-	if (nosound_) {
-		set_disable_music(true);
-		set_disable_fx(true);
-		is_backend_disabled_ = true;
+	if (SoundHandler::is_backend_disabled()) {
 		return;
 	}
 
 	if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
-		report_initalization_error(SDL_GetError());
+		initialization_error(SDL_GetError(), false);
 		return;
 	}
 
 	if (Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT, 2, bufsize) != 0) {
-		initialization_error(Mix_GetError());
+		initialization_error(Mix_GetError(), true);
 		return;
 	}
 
 	constexpr int kMixInitFlags = MIX_INIT_OGG;
-	int initted = Mix_Init(kMixInitFlags);
-	if ((initted & kMixInitFlags) != kMixInitFlags) {
-		initialization_error("No Ogg support in SDL_Mixer.");
+	int init_flags = Mix_Init(kMixInitFlags);
+	if ((init_flags & kMixInitFlags) != kMixInitFlags) {
+		initialization_error("No Ogg support in SDL_Mixer.", true);
 		return;
 	}
 
 	if (Mix_AllocateChannels(kNumMixingChannels) != kNumMixingChannels) {
-		initialization_error(Mix_GetError());
+		initialization_error(Mix_GetError(), true);
+		return;
 	}
 
 	Mix_HookMusicFinished(SoundHandler::music_finished_callback);
 	Mix_ChannelFinished(SoundHandler::fx_finished_callback);
-	load_system_sounds();
-	Mix_VolumeMusic(music_volume_);  //  can not do this before InitSubSystem
+	Mix_VolumeMusic(sound_options_.at(SoundType::kMusic).volume);
 
-	if (fx_lock_ == nullptr)
+	if (fx_lock_ == nullptr) {
 		fx_lock_ = SDL_CreateMutex();
-}
-
-void SoundHandler::initialization_error(const std::string& msg) {
-	log("WARNING: Failed to initialize sound system: %s\n", msg.c_str());
-
-	SDL_QuitSubSystem(SDL_INIT_AUDIO);
-
-	set_disable_music(true);
-	set_disable_fx(true);
-	is_backend_disabled_ = true;
-	return;
-}
-
-void SoundHandler::shutdown() {
+	}
+}
+
+/**
+ * Housekeeping: unset hooks, clear the mutex and all data structures and shut down the sound system.
+ * Audio data will be freed automagically by the \ref Songset and \ref FXset destructors, but not the {song|fx}sets themselves.
+ */
+SoundHandler::~SoundHandler() {
+	if (SDL_WasInit(SDL_INIT_AUDIO) == 0) {
+		return;
+	}
+
 	Mix_ChannelFinished(nullptr);
 	Mix_HookMusicFinished(nullptr);
 
+	stop_music();
+
+	songs_.clear();
+	fxs_.clear();
+
 	int numtimesopened, frequency, channels;
 	uint16_t format;
 	numtimesopened = Mix_QuerySpec(&frequency, &format, &channels);
-	log("SoundHandler closing times %i, freq %i, format %i, chan %i\n", numtimesopened, frequency,
-	    format, channels);
+	log("SoundHandler: Closing %i time%s, %i Hz, format %i, %i channel%s\n",
+		numtimesopened, numtimesopened == 1 ? "" : "s",
+		frequency, format, channels, channels == 1 ? "" : "s");
 
-	if (!numtimesopened)
+	if (numtimesopened == 0) {
 		return;
+	}
 
 	Mix_HaltChannel(-1);
 
 	if (SDL_InitSubSystem(SDL_INIT_AUDIO) == -1) {
-		log("audio error %s\n", SDL_GetError());
+		log("SoundHandler: Audio error %s\n", SDL_GetError());
 	}
 
-	log("SDL_AUDIODRIVER %s\n", SDL_GetCurrentAudioDriver());
+	log("SoundHandler: SDL_AUDIODRIVER %s\n", SDL_GetCurrentAudioDriver());
 
 	if (numtimesopened != 1) {
-		log("PROBLEM: sound device opened multiple times, trying to close");
+		log("SoundHandler: PROBLEM: sound device opened multiple times, trying to close");
 	}
 	for (int i = 0; i < numtimesopened; ++i) {
 		Mix_CloseAudio();
@@ -199,476 +178,463 @@
 		fx_lock_ = nullptr;
 	}
 
-	songs_.clear();
-	fxs_.clear();
-
 	Mix_Quit();
 	SDL_QuitSubSystem(SDL_INIT_AUDIO);
 }
 
-/** Read the main config file, load background music and systemwide sound fx
- *
+/// Prints an error and disables and shuts down the sound system.
+void SoundHandler::initialization_error(const char* const msg, bool quit_sdl) {
+	log("WARNING: Failed to initialize sound system: %s\n", msg);
+	SoundHandler::disable_backend();
+	if (quit_sdl) {
+		SDL_QuitSubSystem(SDL_INIT_AUDIO);
+	}
+	return;
+}
+
+/**
+ * Load the sound options from g_options. If an option is not available, use the defaults set by the constructor.
  */
 void SoundHandler::read_config() {
-	Section& s = g_options.pull_section("global");
-
-	if (nosound_) {
-		set_disable_music(true);
-		set_disable_fx(true);
-	} else {
-		set_disable_music(s.get_bool("disable_music", false));
-		set_disable_fx(s.get_bool("disable_fx", false));
-		music_volume_ = s.get_int("music_volume", kDefaultMusicVolume);
-		fx_volume_ = s.get_int("fx_volume", kDefaultFxVolume);
-	}
-
-	random_order_ = s.get_bool("sound_random_order", true);
-
-	register_song("music", "intro");
-	register_song("music", "menu");
-	register_song("music", "ingame");
-}
-
-/** Load systemwide sound fx into memory.
- * \note This loads only systemwide fx. Worker/building fx will be loaded by
- * their respective conf-file parsers
- */
-void SoundHandler::load_system_sounds() {
-	load_fx_if_needed("sound", "click", "click");
-	load_fx_if_needed("sound", "create_construction_site", "create_construction_site");
-	load_fx_if_needed("sound", "message", "message");
-	load_fx_if_needed("sound/military", "under_attack", "military/under_attack");
-	load_fx_if_needed("sound/military", "site_occupied", "military/site_occupied");
-	load_fx_if_needed("sound", "lobby_chat", "lobby_chat");
-	load_fx_if_needed("sound", "lobby_freshmen", "lobby_freshmen");
-}
-
-/**
- * Returns 'true' if the playing of sounds is disabled due to sound driver problems.
- */
-bool SoundHandler::is_backend_disabled() const {
-	return is_backend_disabled_;
-}
-
-/** Load a sound effect. One sound effect can consist of several audio files
+	// TODO(GunChleoc): Compatibility code to avoid getting bug reports about unread sections. Remove after Build 21.
+	if (g_options.get_section("sound") == nullptr) {
+		Section& global = g_options.pull_section("global");
+
+		for (auto& option : sound_options_) {
+			switch (option.first) {
+			case SoundType::kMusic:
+				option.second.volume = global.get_int("music_volume", option.second.volume);
+				option.second.enabled = !global.get_bool("disable_music", !option.second.enabled);
+				break;
+			case SoundType::kChat:
+				option.second.volume = global.get_int("fx_volume", option.second.volume);
+				option.second.enabled = global.get_bool("sound_at_message", option.second.enabled);
+				break;
+			default:
+				option.second.volume = global.get_int("fx_volume", option.second.volume);
+				option.second.enabled = !global.get_bool("disable_fx", !option.second.enabled);
+				break;
+			}
+		}
+		save_config();
+	}
+
+	// This is the code that we want to keep
+	Section& sound = g_options.pull_section("sound");
+	for (auto& option : sound_options_) {
+		option.second.volume = sound.get_int(("volume_" + option.second.name).c_str(), option.second.volume);
+		option.second.enabled = sound.get_bool(("enable_" + option.second.name).c_str(), option.second.enabled);
+	}
+}
+
+/// Save the current sound options to g_options
+void SoundHandler::save_config() {
+	Section& sound = g_options.pull_section("sound");
+	for (auto& option : sound_options_) {
+		const int volume = option.second.volume;
+		const std::string& name = option.second.name;
+		const bool enabled = option.second.enabled;
+
+		const std::string enable_name = "enable_" + name;
+		sound.set_bool(enable_name.c_str(), enabled);
+
+		const std::string volume_name = "volume_" + name;
+		sound.set_int(volume_name.c_str(), volume);
+	}
+}
+
+/// Read the sound options from g_options and apply them
+void SoundHandler::load_config() {
+	read_config();
+	for (auto& option : sound_options_) {
+		set_volume(option.first, option.second.volume);
+		set_enable_sound(option.first, option.second.enabled);
+	}
+}
+
+/**
+ * Returns 'true' if the playing of sounds is disabled due to sound driver problems, or because disable_backend() was used.
+ */
+bool SoundHandler::is_backend_disabled() {
+	return SoundHandler::backend_is_disabled_;
+}
+
+/**
+ * Disables all sound.
+ */
+void SoundHandler::disable_backend() {
+	SoundHandler::backend_is_disabled_ = true;
+}
+
+/** Register a sound effect. One sound effect can consist of several audio files
  * named EFFECT_XX.ogg, where XX is between 00 and 99.
  *
  * Subdirectories of and files under FILENAME_XX can be named anything you want.
  *
- * \param dir        The relative directory where the audio files reside in data/sound
- * \param filename   Name from which filenames will be formed
- *                   (BASENAME_XX.ogg);
- *                   also the name used with \ref play_fx
- */
-void SoundHandler::load_fx_if_needed(const std::string& dir,
-                                     const std::string& basename,
-                                     const std::string& fx_name) {
-	assert(g_fs);
-
-	if (!g_fs->is_directory(dir)) {
-		throw wexception("SoundHandler: Can't load files from %s, not a directory!", dir.c_str());
-	}
-
-	if (nosound_ || fxs_.count(fx_name) > 0)
-		return;
-
-	fxs_.insert(std::make_pair(fx_name, std::unique_ptr<FXset>(new FXset())));
-
-	boost::regex re(basename + "_\\d+\\.ogg");
-	FilenameSet files = filter(g_fs->list_directory(dir), [&re](const std::string& fn) {
-		return boost::regex_match(FileSystem::fs_filename(fn.c_str()), re);
-	});
-
-	for (const std::string& path : files) {
-		assert(!g_fs->is_directory(path));
-		load_one_fx(path, fx_name);
-	}
-}
-
-/** Add exactly one file to the given fxset.
- * \param path      the effect to be loaded
- * \param fx_name   the fxset to add the file to
- * The file format must be ogg. Otherwise this call will complain and
- * not load the file.
- * \note The complete audio file will be loaded into memory and stays there
- * until the game is finished.
- */
-void SoundHandler::load_one_fx(const std::string& path, const std::string& fx_name) {
-	if (nosound_ || is_backend_disabled_) {
-		return;
-	}
-
-	FileRead fr;
-	if (!fr.try_open(*g_fs, path)) {
-		log("WARNING: Could not open %s for reading!\n", path.c_str());
-		return;
-	}
-
-	if (Mix_Chunk* const m =
-	       Mix_LoadWAV_RW(SDL_RWFromMem(fr.data(fr.get_size(), 0), fr.get_size()), 1)) {
-		// Make sure that requested FXset exists
-
-		assert(fxs_.count(fx_name) > 0);
-
-		fxs_[fx_name]->add_fx(m);
-	} else
-		log("SoundHandler: loading sound effect \"%s\" for FXset \"%s\" "
-		    "failed: %s\n",
-		    path.c_str(), fx_name.c_str(), Mix_GetError());
-}
-
-/** Find out whether to actually play a certain effect right now or rather not
- * (to avoid "sonic overload").
- */
-// TODO(unknown): What is the selection algorithm? cf class documentation
-bool SoundHandler::play_or_not(const std::string& fx_name,
-                               int32_t const stereo_pos,
+ * \param type       The category of the FxSet to create
+ * \param fx_path    The relative path and base filename from which filenames will be formed
+ *                   (<datadir>/fx_path_XX.ogg). Also acts as unique string ID for the effect that will be used to identify it in \ref play_fx.
+ *                   If an effect with the same 'type' and 'fx_path' already exists, we assume that it is already registered and skip it.
+ */
+
+FxId SoundHandler::register_fx(SoundType type, const std::string& fx_path) {
+	if (SoundHandler::is_backend_disabled() || g_sh == nullptr) {
+		return kNoSoundEffect;
+	}
+	size_t result = g_sh->do_register_fx(type, fx_path);
+	return result;
+}
+
+/// Non-static implementation of register_fx
+FxId SoundHandler::do_register_fx(SoundType type, const std::string& fx_path) {
+	assert(!SoundHandler::is_backend_disabled());
+	if (fx_ids_[type].count(fx_path) == 0) {
+		const FxId new_id = fxs_[type].size();
+		fx_ids_[type].insert(std::make_pair(fx_path, new_id));
+		fxs_[type].insert(std::make_pair(new_id, std::unique_ptr<FXset>(new FXset(fx_path, rng_.rand()))));
+		return new_id;
+	} else {
+		return fx_ids_[type].at(fx_path);
+	}
+}
+
+/**
+ * Find out whether to actually play a certain effect right now or rather not
+ * (to avoid "sonic overload"). Based on priority and on when it was last played.
+ * System sounds and sounds with priority "kFxPriorityAlwaysPlay" always return 'true'.
+ */
+bool SoundHandler::play_or_not(SoundType type, const FxId fx_id,
                                uint8_t const priority) {
-	bool allow_multiple = false;  //  convenience for easier code reading
-	float evaluation;             // Temporary to calculate single influences
-	float probability;            // Weighted total of all influences
-
-	if (nosound_)
-		return false;
-
-	// Probability that this fx gets played; initially set according to priority
-
-	//  float division! not integer
-	probability = (priority % PRIO_ALLOW_MULTIPLE) / 128.0f;
-
-	// TODO(unknown): what to do with fx that happen offscreen?
-	// TODO(unknown): reduce volume? reduce priority? other?
-	if (stereo_pos == -1) {
-		return false;
-	}
-
-	// TODO(unknown): check for a free channel
-
-	if (priority == PRIO_ALWAYS_PLAY) {
-		// TODO(unknown): if there is no free channel, kill a running fx and complain
-		return true;
-	}
-
-	if (priority >= PRIO_ALLOW_MULTIPLE)
-		allow_multiple = true;
-
-	// Find out if an fx called fx_name is already running
-	bool already_running = false;
-
-	// Access to active_fx_ is protected because it can
-	// be accessed from callback
-	if (fx_lock_)
-		SDL_LockMutex(fx_lock_);
-
-	// starting a block, so I can define a local type for iterating
-	{
+	assert(!backend_is_disabled_ && is_sound_enabled(type));
+	assert(priority >= kFxPriorityLowest);
+
+	if (fxs_[type].count(fx_id) == 0) {
+		return false;
+	}
+
+	switch (type) {
+	case SoundType::kAmbient:
+		break;
+	default:
+		// We always play Ui, chat and system sounds
+		return true;
+	}
+
+	// We always play important sounds
+	if (priority == kFxPriorityAlwaysPlay) {
+		return true;
+	}
+
+	// Do not run multiple instances of the same sound effect if the priority is too low
+	bool too_many_playing = false;
+	if (priority < kFxPriorityAllowMultiple) {
+		lock_fx();
+		// Find out if an fx called 'fx_name' is already running
 		for (const auto& fx_pair : active_fx_) {
-			if (fx_pair.second == fx_name) {
-				already_running = true;
+			if (fx_pair.second == fx_id) {
+				too_many_playing = true;
 				break;
 			}
 		}
 	}
 
-	if (fx_lock_)
-		SDL_UnlockMutex(fx_lock_);
+	release_fx_lock();
 
-	if (!allow_multiple && already_running)
+	if (too_many_playing) {
 		return false;
+	}
 
 	// TODO(unknown): long time since any play increases weighted_priority
 	// TODO(unknown): high general frequency reduces weighted priority
 	// TODO(unknown): deal with "coupled" effects like throw_net and retrieve_net
 
-	uint32_t const ticks_since_last_play = SDL_GetTicks() - fxs_[fx_name]->last_used_;
-
-	//  reward an fx for being silent
-	if (ticks_since_last_play > SLIDING_WINDOW_SIZE) {
-		evaluation = 1;  //  arbitrary value; 0 -> no change, 1 -> probability = 1
+	uint32_t const ticks_since_last_play = fxs_[type][fx_id]->ticks_since_last_play();
+
+	// Weighted total probability that this fx gets played; initially set according to priority
+	//  float division! not integer
+	float probability = (priority % kFxPriorityAllowMultiple) / static_cast<float>(kFxPriorityAllowMultiple);
+
+	// How many milliseconds in the past to consider
+	constexpr uint32_t kSlidingWindowSize = 20000;
+
+	if (ticks_since_last_play > kSlidingWindowSize) { //  reward an fx for being silent
+		const float evaluation = 1.0f;  //  arbitrary value; 0 -> no change, 1 -> probability = 1
 
 		//  "decrease improbability"
-		probability = 1 - ((1 - probability) * (1 - evaluation));
+		probability = 1.0f - ((1.0f - probability) * (1.0f - evaluation));
 	} else {  // Penalize an fx for playing in short succession
-		evaluation = static_cast<float>(ticks_since_last_play) / SLIDING_WINDOW_SIZE;
+		const float evaluation = static_cast<float>(ticks_since_last_play) / kSlidingWindowSize;
 		probability *= evaluation;  //  decrease probability
 	}
 
 	// finally: the decision
 	// float division! not integer
-	return (rng_.rand() % 255) / 255.0f <= probability;
+	return (rng_.rand() % kFxPriorityAlwaysPlay) / static_cast<float>(kFxPriorityAlwaysPlay) <= probability;
 }
 
-/** \overload
- * \param fx_name  The identifying name of the sound effect, see \ref load_fx .
+/**
+ * \param type The categorization of the sound effect to be played
+ * \param fx_name  The identifying name of the sound effect, see \ref register_fx .
+ * \param priority         How important is it that this FX actually gets
+ *                         played? (see \ref FXset::priority_)
  * \param stereo_position  position in widelands' game window, see
  *                         \ref stereo_position
- * \param priority         How important is it that this FX actually gets
- *                         played? (see \ref FXset::priority_)
+ * \param distance The distance to use set in the mix
  */
-void SoundHandler::play_fx(const std::string& fx_name,
+void SoundHandler::play_fx(SoundType type, const FxId fx_id,
+						   uint8_t const priority,
                            int32_t const stereo_pos,
-                           uint8_t const priority) {
-	if (nosound_ || is_backend_disabled_)
-		return;
-
-	assert(stereo_pos >= -1);
-	assert(stereo_pos <= 254);
-
-	if (get_disable_fx())
-		return;
-
-	if (fxs_.count(fx_name) == 0) {
-		log("SoundHandler: sound effect \"%s\" does not exist!\n", fx_name.c_str());
+                           int distance) {
+	if (backend_is_disabled_ || !is_sound_enabled(type)) {
+		return;
+	}
+
+	assert(stereo_pos >= kStereoLeft);
+	assert(stereo_pos <= kStereoRight);
+
+	if (fx_id == kNoSoundEffect) {
+		throw wexception("SoundHandler: Trying to play sound effect that was never registered. Maybe you registered it before instantiating g_soundhandler?\n");
+	}
+
+	if (fxs_[type].count(fx_id) == 0) {
+		log("SoundHandler: Sound effect %d does not exist!\n", fx_id);
 		return;
 	}
 
 	// See if the FX should be played
-	if (!play_or_not(fx_name, stereo_pos, priority))
+	if (!play_or_not(type, fx_id, priority)) {
 		return;
+	}
 
 	//  retrieve the fx and play it if it's valid
-	if (Mix_Chunk* const m = fxs_[fx_name]->get_fx()) {
+	if (Mix_Chunk* const m = fxs_[type][fx_id]->get_fx(rng_.rand())) {
 		const int32_t chan = Mix_PlayChannel(-1, m, 0);
 		if (chan == -1) {
 			log("SoundHandler: Mix_PlayChannel failed: %s\n", Mix_GetError());
 		} else {
-			Mix_SetPanning(chan, 254 - stereo_pos, stereo_pos);
-			Mix_Volume(chan, get_fx_volume());
+			Mix_SetPanning(chan, kStereoRight - stereo_pos, stereo_pos);
+			Mix_SetDistance(chan, distance);
+			Mix_Volume(chan, get_volume(type));
 
-			// Access to active_fx_ is protected
-			// because it can be accessed from callback
-			if (fx_lock_)
-				SDL_LockMutex(fx_lock_);
-			active_fx_[chan] = fx_name;
-			if (fx_lock_)
-				SDL_UnlockMutex(fx_lock_);
+			lock_fx();
+			active_fx_[chan] = fx_id;
+			release_fx_lock();
 		}
-	} else
-		log("SoundHandler: sound effect \"%s\" exists but contains no files!\n", fx_name.c_str());
-}
-
-/** Load a background song. One "song" can consist of several audio files named
+	} else {
+		log("SoundHandler: Sound effect %d exists but contains no files!\n", fx_id);
+	}
+}
+
+/// Removes the given FXset from memory
+void SoundHandler::remove_fx_set(SoundType type) {
+	fxs_.erase(type);
+	fx_ids_.erase(type);
+}
+
+/**
+ * Register a background songset. A songset can consist of several audio files named
  * FILE_XX.ogg, where XX is between 00 and 99.
  * \param dir        The directory where the audio files reside.
  * \param basename   Name from which filenames will be formed
- *                   (BASENAME_XX.ogg); also the name used with \ref play_fx .
- * This just registers the song, actual loading takes place when
- * \ref Songset::get_song() is called, i.e. when the song is about to be
+ *                   (BASENAME_XX.ogg); also the name used with \ref change_music .
+ * This just registers the songs, actual loading takes place when
+ * \ref Songset::get_song() is called, i.e. when a song is about to be
  * played. The song will automatically be removed from memory when it has
  * finished playing.
  */
-void SoundHandler::register_song(const std::string& dir, const std::string& basename) {
-	if (nosound_ || is_backend_disabled_)
+void SoundHandler::register_songs(const std::string& dir, const std::string& basename) {
+	if (SoundHandler::is_backend_disabled()) {
 		return;
-	assert(g_fs);
-
-	FilenameSet files;
-
-	files = filter(g_fs->list_directory(dir), [&basename](const std::string& fn) {
-		const std::string only_filename = FileSystem::fs_filename(fn.c_str());
-		return boost::starts_with(only_filename, basename) && boost::ends_with(only_filename, ".ogg");
-	});
-
-	for (const std::string& filename : files) {
-		assert(!g_fs->is_directory(filename));
-		if (songs_.count(basename) == 0) {
-			songs_.insert(std::make_pair(basename, std::unique_ptr<Songset>(new Songset())));
-		}
-		songs_[basename]->add_song(filename);
+	}
+	if (songs_.count(basename) == 0) {
+		songs_.insert(std::make_pair(basename, std::unique_ptr<Songset>(new Songset(dir, basename))));
 	}
 }
 
-/** Start playing a songset.
+/**
+ * Start playing a songset.
  * \param songset_name  The songset to play a song from.
- * \param fadein_ms     Song will fade from 0% to 100% during fadein_ms
- *                      milliseconds starting from now.
- * \note When calling start_music() while music is still fading out from
- * \ref stop_music()
- * or \ref change_music() this function will block until the fadeout is complete
+ * \note When calling start_music() while music is still fading out from \ref stop_music() or \ref change_music(),
+ * this function will block until the fadeout is complete
  */
-void SoundHandler::start_music(const std::string& songset_name, int32_t fadein_ms) {
-	if (get_disable_music() || nosound_ || is_backend_disabled_)
+void SoundHandler::start_music(const std::string& songset_name) {
+	if (backend_is_disabled_ || !is_sound_enabled(SoundType::kMusic)) {
 		return;
-
-	if (fadein_ms == 0)
-		fadein_ms = 250;  //  avoid clicks
-
-	if (Mix_PlayingMusic())
-		change_music(songset_name, 0, fadein_ms);
-
-	if (songs_.count(songset_name) == 0)
+	}
+
+	if (Mix_PlayingMusic()) {
+		change_music(songset_name, kMinimumMusicFade);
+	}
+
+	if (songs_.count(songset_name) == 0) {
 		log("SoundHandler: songset \"%s\" does not exist!\n", songset_name.c_str());
-	else {
-		if (Mix_Music* const m = songs_[songset_name]->get_song()) {
-			Mix_FadeInMusic(m, 1, fadein_ms);
+	} else {
+		if (Mix_Music* const m = songs_[songset_name]->get_song(rng_.rand())) {
+			Mix_FadeInMusic(m, 1, kMinimumMusicFade);
 			current_songset_ = songset_name;
-		} else
+		} else {
 			log("SoundHandler: songset \"%s\" exists but contains no files!\n", songset_name.c_str());
+		}
 	}
-	Mix_VolumeMusic(music_volume_);
 }
 
-/** Stop playing a songset.
+/**
+ * Stop playing a songset.
  * \param fadeout_ms Song will fade from 100% to 0% during fadeout_ms
  *                   milliseconds starting from now.
  */
-void SoundHandler::stop_music(int32_t fadeout_ms) {
-	if (get_disable_music() || nosound_)
+void SoundHandler::stop_music(int fadeout_ms) {
+	if (backend_is_disabled_) {
 		return;
-
-	if (fadeout_ms == 0)
-		fadeout_ms = 250;  //  avoid clicks
-
-	Mix_FadeOutMusic(fadeout_ms);
+	}
+
+	if (Mix_PlayingMusic()) {
+		Mix_FadeOutMusic(std::max(fadeout_ms, kMinimumMusicFade));
+	}
 }
 
-/** Play an other piece of music.
+/**
+ * Play a new piece of music.
  * This is a member function provided for convenience. It is a wrapper around
  * \ref start_music and \ref stop_music.
  * \param fadeout_ms  Old song will fade from 100% to 0% during fadeout_ms
  *                    milliseconds starting from now.
- * \param fadein_ms   New song will fade from 0% to 100% during fadein_ms
- *                    milliseconds starting from now.
  * If songset_name is empty, another song from the currently active songset will
  * be selected
  */
 void SoundHandler::change_music(const std::string& songset_name,
-                                int32_t const fadeout_ms,
-                                int32_t const fadein_ms) {
-	if (nosound_)
+                                int const fadeout_ms) {
+	if (SoundHandler::is_backend_disabled()) {
 		return;
-
-	std::string s = songset_name;
-
-	if (s == "")
-		s = current_songset_;
-	else
-		current_songset_ = s;
-
-	if (Mix_PlayingMusic())
+	}
+
+	if (!songset_name.empty()) {
+		current_songset_ = songset_name;
+	}
+
+	if (Mix_PlayingMusic()) {
 		stop_music(fadeout_ms);
-	else
-		start_music(s, fadein_ms);
-}
-
-bool SoundHandler::get_disable_music() const {
-	return disable_music_;
-}
-bool SoundHandler::get_disable_fx() const {
-	return disable_fx_;
-}
-int32_t SoundHandler::get_music_volume() const {
-	return music_volume_;
-}
-int32_t SoundHandler::get_fx_volume() const {
-	return fx_volume_;
-}
-
-/** Normal set_* function, but the music must be started/stopped accordingly
- * Also, the new value is written back to the config file right away. It might
- * get lost otherwise.
- */
-void SoundHandler::set_disable_music(bool const disable) {
-	if (is_backend_disabled_ || disable_music_ == disable)
-		return;
-
-	if (disable) {
-		stop_music();
-		disable_music_ = true;
 	} else {
-		disable_music_ = false;
 		start_music(current_songset_);
 	}
-
-	g_options.pull_section("global").set_bool("disable_music", disable);
-}
-
-/** Normal set_* function
- * Also, the new value is written back to the config file right away. It might
- * get lost otherwise.
- */
-void SoundHandler::set_disable_fx(bool const disable) {
-	if (is_backend_disabled_)
-		return;
-
-	disable_fx_ = disable;
-
-	g_options.pull_section("global").set_bool("disable_fx", disable);
-}
-
-/**
- * Normal set_* function.
- * Set the music volume between 0 (muted) and \ref get_max_volume().
- * The new value is written back to the config file.
- *
- * \param volume The new music volume.
- */
-void SoundHandler::set_music_volume(int32_t volume) {
-	if (!is_backend_disabled_ && !nosound_) {
-		music_volume_ = volume;
+}
+
+/// Returns the currently playing songset
+const std::string SoundHandler::current_songset() const {
+	return current_songset_;
+}
+
+/// Returns whether we want to hear sounds of the given 'type'
+bool SoundHandler::is_sound_enabled(SoundType type) const {
+	assert(sound_options_.count(type) == 1);
+	return sound_options_.at(type).enabled;
+}
+
+/// Returns the volume that the given 'type' of sound is to be played at
+int32_t SoundHandler::get_volume(SoundType type) const {
+	assert(sound_options_.count(type) == 1);
+	return sound_options_.at(type).volume;
+}
+
+/**
+ * Sets that we want to / don't want to hear the given 'type' of sounds. If the type is \ref SoundType::kMusic, start/stop the music as well.
+ */
+void SoundHandler::set_enable_sound(SoundType type, bool const enable) {
+	assert(sound_options_.count(type) == 1);
+	SoundOptions& sound_options = sound_options_.at(type);
+	if (SoundHandler::is_backend_disabled()) {
+		return;
+	}
+
+	sound_options.enabled = enable;
+
+	// Special treatment for music
+	switch (type) {
+	case SoundType::kMusic:
+		if (enable) {
+			if (!Mix_PlayingMusic()) {
+				start_music(current_songset_);
+			}
+		} else {
+			stop_music();
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+/**
+ * Sets the music or sound 'volume' for the given 'type' between 0 (muted) and \ref get_max_volume().
+ */
+void SoundHandler::set_volume(SoundType type, int32_t volume) {
+	if (SoundHandler::is_backend_disabled()) {
+		return;
+	}
+
+	assert(sound_options_.count(type) == 1);
+	assert(volume >= 0 && volume <= get_max_volume());
+
+	sound_options_.at(type).volume = volume;
+
+	// Special treatment for music
+	switch (type) {
+	case SoundType::kMusic:
 		Mix_VolumeMusic(volume);
-		g_options.pull_section("global").set_int("music_volume", volume);
-	}
-}
-
-/**
- * Normal set_* function
- * Set the FX sound volume between 0 (muted) and \ref get_max_volume().
- * The new value is written back to the config file.
- *
- * \param volume The new music volume.
- */
-void SoundHandler::set_fx_volume(int32_t volume) {
-	if (!is_backend_disabled_ && !nosound_) {
-		fx_volume_ = volume;
+		break;
+	default:
 		Mix_Volume(-1, volume);
-		g_options.pull_section("global").set_int("fx_volume", volume);
+		break;
 	}
 }
 
-/** Callback to notify \ref SoundHandler that a song has finished playing.
- * Usually, another song from the same songset will be started.
- * There is a special case for the intro screen's music: only one song will be
- * played. If the user has not clicked the mouse or pressed escape when the song
- * finishes, Widelands will automatically go on to the main menu.
+/**
+ * Returns the max value for volume settings. We use a function to hide
+ * SDL_mixer constants outside of sound_handler.
+ */
+int32_t SoundHandler::get_max_volume() const {
+	return MIX_MAX_VOLUME;
+}
+
+/**
+ * Callback to notify \ref SoundHandler that a song has finished playing.
+ * Pushes an SDL_Event with type = SDL_USEREVENT and user.code = CHANGE_MUSIC.
  */
 void SoundHandler::music_finished_callback() {
 	// DO NOT CALL SDL_mixer FUNCTIONS OR SDL_LockAudio FROM HERE !!!
 
+	assert(!SoundHandler::is_backend_disabled());
+	// Trigger that we want a music change and leave the specifics to the application.
 	SDL_Event event;
-	if (g_sound_handler.current_songset_ == "intro") {
-		// Special case for splashscreen: there, only one song is ever played
-		event.type = SDL_KEYDOWN;
-		event.key.state = SDL_PRESSED;
-		event.key.keysym.sym = SDLK_ESCAPE;
-	} else {
-		// Else just play the next song - see general description for
-		// further explanation
-		event.type = SDL_USEREVENT;
-		event.user.code = CHANGE_MUSIC;
-	}
+	event.type = SDL_USEREVENT;
+	event.user.code = CHANGE_MUSIC;
 	SDL_PushEvent(&event);
 }
 
-/** Callback to notify \ref SoundHandler that a sound effect has finished
- * playing.
+/**
+ * Callback to notify \ref SoundHandler that a sound effect has finished
+ * playing. Removes the finished sound fx from the list of currently playing ones.
  */
 void SoundHandler::fx_finished_callback(int32_t const channel) {
 	// DO NOT CALL SDL_mixer FUNCTIONS OR SDL_LockAudio FROM HERE !!!
 
+	assert(!SoundHandler::is_backend_disabled());
 	assert(0 <= channel);
-	g_sound_handler.handle_channel_finished(static_cast<uint32_t>(channel));
+	g_sh->lock_fx();
+	g_sh->active_fx_.erase(static_cast<uint32_t>(channel));
+	g_sh->release_fx_lock();
 }
 
-/** Remove a finished sound fx from the list of currently playing ones
- * This is part of \ref fx_finished_callback
- */
-void SoundHandler::handle_channel_finished(uint32_t channel) {
-	// Needs locking because active_fx_ may be accessed
-	// from this callback or from main thread
-	if (fx_lock_)
+/// Lock the SDL mutex. Access to 'active_fx_' is protected by mutex because it can be accessed both from callbacks or from the main thread.
+void SoundHandler::lock_fx() {
+	if (fx_lock_) {
 		SDL_LockMutex(fx_lock_);
-	active_fx_.erase(channel);
-	if (fx_lock_)
+	}
+}
+
+/// Release the SDL mutex
+void SoundHandler::release_fx_lock() {
+	if (fx_lock_) {
 		SDL_UnlockMutex(fx_lock_);
+	}
 }

=== modified file 'src/sound/sound_handler.h'
--- src/sound/sound_handler.h	2019-02-23 11:00:49 +0000
+++ src/sound/sound_handler.h	2019-03-23 14:09:21 +0000
@@ -30,18 +30,12 @@
 #include <unistd.h>
 #endif
 
+#include <SDL_mutex.h>
+
 #include "random/random.h"
 #include "sound/fxset.h"
-
-struct Songset;
-struct SDL_mutex;
-class FileRead;
-
-/// How many milliseconds in the past to consider for
-/// SoundHandler::play_or_not()
-#define SLIDING_WINDOW_SIZE 20000
-
-extern class SoundHandler g_sound_handler;
+#include "sound/constants.h"
+#include "sound/songset.h"
 
 /** The 'sound server' for Widelands.
  *
@@ -54,47 +48,39 @@
  *
  * Background music for different situations (e.g. 'Menu', 'Gameplay') is
  * collected in songsets. Each Songset contains references to one or more
- * songs in ogg format. The only ordering inside a soundset is from the order
- * in which the songs were loaded.
+ * songs in ogg format.
  *
  * Other classes can request to start or stop playing a certain songset,
  * changing the songset is provided as a convenience method. It is also
  * possible to switch to some other piece inside the same songset - but there
  * is \e no control over \e which song out of a songset gets played. The
- * selection is either linear (the order in which the songs were loaded) or
- * completely random.
- *
- * The files for the predefined system songsets
- * \li \c intro
- * \li \c menu
- * \li \c ingame
- *
- * must reside directly in the directory 'sounds' and must be named
- * SONGSET_XX.??? where XX is a number from 00 to 99 and ??? is a filename
- * extension. All subdirectories of 'sounds' will be considered to contain
- * ingame music. The the music and sub-subdirectories found in them can be
- * arbitrarily named. This means: everything below sound/ingame_01 can have
- * any name you want. All audio files below sound/ingame_01 will be played as
+ * selection is random.
+ *
+ * The files must reside somewhere below the datadir and must be named
+ * SONGSET_XX.ogg where XX is a number from 00 to 99.
+ * The music and sub-subdirectories found in a directory can be
+ * arbitrarily named. For example, if we register songsets in <datadir>/music,
+ * this means that everything below <datadir>/music/ingame_01 can have
+ * any name you want. All audio files below <datadir>/music/ingame_01 will be played as
  * ingame music.
  *
- * For more information about the naming scheme, see load_fx()
- *
- * You should be using the ogg format for music.
+ * For more information about the naming scheme, see register_songs()
+ *
+ * You must use the ogg format for music.
+ *
  *
  * \par Sound effects
  *
- * Buildings and workers can use sound effects in their programs. To do so, use
- * e.g. "playsound blacksmith_hammer" in the appropriate conf file. The conf file
- * parser will then load one or more audio files for 'hammering blacksmith'
- * from the building's/worker's configuration directory and store them in an
- * FXset for later access, similar to the way music is stored in songsets.
- * For effects, however, the selection is always random. Sound effects are kept
- * in memory at all times, to avoid delays from disk access.
- *
- * The abovementioned sound effects are synchronized with a work program. It's
- * also possible to have sound effects that are synchronized with a
- * building/worker \e animation. For more information about this look at class
- * AnimationManager.
+ * Use register_fx() to record the file locations for each sound effect, to be loaded on first play.
+ * Sound effects are kept in memory at all times once they have been loaded, to avoid delays from disk access.
+ * Yo can use \ref remove_fx_set to deregister and unload sound effects from memory though.
+ * The file naming scheme is the same as for the songs, and if there are multiple files for an effect, they are picked at random too.
+ * Sound effects are categorized into multiple SoundType categories, so that the user can control which type of sounds to hear.
+ *
+ * For map objects, the abovementioned sound effects are synchronized with a work program or a
+ * building/immovable/worker animation. For more information about this look at class
+ * AnimationManager and see the online Lua reference for details.
+ *
  *
  * \par Usage of callbacks
  *
@@ -107,10 +93,10 @@
  *
  * Callbacks must use global(or static) functions \e but \e not normal member
  * functions of a class. If you must know why: ask google. But how can a
- * static function share data with an instance of it's own class? Usually not at
+ * static function share data with an instance of its own class? Usually not at
  * all.
  *
- * Fortunately, g_sound_handler already is a global variable,
+ * Fortunately, g_sh already is a global variable,
  * and therefore accessible to global functions. So problem 1 disappears.
  *
  * Problem 2:
@@ -155,112 +141,92 @@
  * change_music() from inside start_music(). It really is not recursive, trust
  * me :-)
  */
-// TODO(unknown): DOC: priorities
-// TODO(unknown): DOC: play-or-not algorithm
-// TODO(unknown): Environmental sound effects (e.g. wind)
-// TODO(unknown): repair and reenable animation sound effects for 1-pic-animations
-// TODO(unknown): accommodate runtime changes of i18n language
-// TODO(unknown): accommodate sound activation if it was disabled at the beginning
 
-// This is used for SDL UserEvents to be handled in the main loop.
+/// This is used for SDL UserEvents to be handled in the main loop.
 enum { CHANGE_MUSIC };
+
+/// Avoid clicks when starting/stopping music
+constexpr int kMinimumMusicFade = 250;
+
 class SoundHandler {
-	friend struct Songset;
-	friend struct FXset;
-
 public:
 	SoundHandler();
 	~SoundHandler();
 
-	void init();
-	void shutdown();
+	void save_config();
+	void load_config();
+	static bool is_backend_disabled();
+	static void disable_backend();
+
+	// This is static so that we can load the tribes and world without instantiating the sound system
+	static FxId register_fx(SoundType type, const std::string& fx_path);
+
+	void play_fx(SoundType type, FxId fx_id,
+	             uint8_t priority = kFxPriorityAlwaysPlay,
+	             int32_t stereo_position = kStereoCenter, int distance = 0);
+	void remove_fx_set(SoundType type);
+
+	void register_songs(const std::string& dir, const std::string& basename);
+	void stop_music(int fadeout_ms = kMinimumMusicFade);
+	void change_music(const std::string& songset_name = std::string(),
+	                  int fadeout_ms = kMinimumMusicFade);
+
+	const std::string current_songset() const;
+
+	bool is_sound_enabled(SoundType type) const;
+	void set_enable_sound(SoundType type, bool enable);
+	int32_t get_volume(SoundType type) const;
+	void set_volume(SoundType type, int32_t volume);
+
+	int32_t get_max_volume() const;
+
+private:
 	void read_config();
-	void load_system_sounds();
-	bool is_backend_disabled() const;
-
-	void load_fx_if_needed(const std::string& dir,
-	                       const std::string& basename,
-	                       const std::string& fx_name);
-
-	void play_fx(const std::string& fx_name,
-	             int32_t stereo_position,
-	             uint8_t priority = PRIO_ALLOW_MULTIPLE + PRIO_MEDIUM);
-
-	void register_song(const std::string& dir, const std::string& basename);
-	void start_music(const std::string& songset_name, int32_t fadein_ms = 0);
-	void stop_music(int32_t fadeout_ms = 0);
-	void change_music(const std::string& songset_name = std::string(),
-	                  int32_t fadeout_ms = 0,
-	                  int32_t fadein_ms = 0);
+
+	FxId do_register_fx(SoundType type, const std::string& fx_path);
+
+	void initialization_error(const char* const msg, bool quit_sdl);
+
+	bool play_or_not(SoundType type, FxId fx_id, uint8_t priority);
+	void start_music(const std::string& songset_name);
 
 	static void music_finished_callback();
 	static void fx_finished_callback(int32_t channel);
-	void handle_channel_finished(uint32_t channel);
-
-	bool get_disable_music() const;
-	bool get_disable_fx() const;
-	int32_t get_music_volume() const;
-	int32_t get_fx_volume() const;
-	void set_disable_music(bool disable);
-	void set_disable_fx(bool disable);
-	void set_music_volume(int32_t volume);
-	void set_fx_volume(int32_t volume);
-
-	/**
-	 * Return the max value for volume settings. We use a function to hide
-	 * SDL_mixer constants outside of sound_handler.
-	 */
-	int32_t get_max_volume() const {
-		return MIX_MAX_VOLUME;
-	}
-
-	/** Only for buffering the command line option --nosound until real initialization is done.
-	 * \see SoundHandler::SoundHandler()
-	 * \see SoundHandler::init()
-	 */
-	// TODO(unknown): This is ugly. Find a better way to do it
-	bool nosound_;
-
-private:
-	// Prints an error and disables the sound system.
-	void initialization_error(const std::string& msg);
-
-	void load_one_fx(const std::string& path, const std::string& fx_name);
-	bool play_or_not(const std::string& fx_name, int32_t stereo_position, uint8_t priority);
-
-	/** Can sounds be played?
-	 * true = they mustn't be played (e.g. because hardware is missing)
-	 * false = can be played
-	 */
-	bool is_backend_disabled_;
-
-	/// Whether to disable background music
-	bool disable_music_;
-	/// Whether to disable sound effects
-	bool disable_fx_;
-	/// Volume of music (from 0 to get_max_volume())
-	int32_t music_volume_;
-	/// Volume of sound effects (from 0 to get_max_volume())
-	int32_t fx_volume_;
-
-	/** Whether to play music in random order
-	 * \note Sound effects will \e always be selected at random (inside
-	 * their FXset, of course.
-	 */
-	bool random_order_;
+
+	void lock_fx();
+	void release_fx_lock();
+
+	/// Contains options for a sound type or the music
+	struct SoundOptions {
+		explicit SoundOptions(int vol, const std::string& savename) : enabled(true), volume(vol), name(savename) {
+			assert(!savename.empty());
+			assert(vol >= 0);
+			assert(vol <= MIX_MAX_VOLUME);
+		}
+
+		/// Whether the user wants to hear this type of sound
+		bool enabled;
+		/// Volume for sound effects or music (from 0 to get_max_volume())
+		int volume;
+		/// Name for saving
+		const std::string name;
+	};
+
+	/// Contains all options for sound types and music
+	std::map<SoundType, SoundOptions> sound_options_;
 
 	/// A collection of songsets
-	using SongsetMap = std::map<std::string, std::unique_ptr<Songset>>;
-	SongsetMap songs_;
+	std::map<std::string, std::unique_ptr<Songset>> songs_;
 
 	/// A collection of effect sets
-	using FXsetMap = std::map<std::string, std::unique_ptr<FXset>>;
-	FXsetMap fxs_;
+	std::map<SoundType, std::map<FxId, std::unique_ptr<FXset>>> fxs_;
+
+	/// Maps Fx names to identifiers to ensure that we register each effect only once
+	std::map<SoundType, std::map<std::string, FxId>> fx_ids_;
 
 	/// List of currently playing effects, and the channel each one is on
 	/// Access to this variable is protected through fx_lock_ mutex.
-	using ActivefxMap = std::map<uint32_t, std::string>;
-	ActivefxMap active_fx_;
+	std::map<uint32_t, FxId> active_fx_;
 
 	/** Which songset we are currently selecting songs from - not regarding
 	 * if there actually is a song playing \e right \e now.
@@ -275,6 +241,15 @@
 
 	/// Protects access to active_fx_ between callbacks and main code.
 	SDL_mutex* fx_lock_;
+
+	/**
+	 * Can sounds be played?
+	 * true = they mustn't be played, e.g. because hardware is missing or disable_backend() was called.
+	 * false = can be played
+	 */
+	static bool backend_is_disabled_;
 };
 
+extern SoundHandler* g_sh;
+
 #endif  // end of include guard: WL_SOUND_SOUND_HANDLER_H

=== modified file 'src/ui_basic/panel.cc'
--- src/ui_basic/panel.cc	2019-02-23 11:00:49 +0000
+++ src/ui_basic/panel.cc	2019-03-23 14:09:21 +0000
@@ -42,6 +42,7 @@
 bool Panel::allow_user_input_ = true;
 const Image* Panel::default_cursor_ = nullptr;
 const Image* Panel::default_cursor_click_ = nullptr;
+FxId Panel::click_fx_ = kNoSoundEffect;
 
 /**
  * Initialize a panel, link it into the parent's queue.
@@ -713,13 +714,15 @@
  * sound_handler.h in every UI subclass just for playing a 'click'
  */
 void Panel::play_click() {
-	g_sound_handler.play_fx("click", 128, PRIO_ALWAYS_PLAY);
-}
-void Panel::play_new_chat_message() {
-	g_sound_handler.play_fx("lobby_chat", 128, PRIO_ALWAYS_PLAY);
-}
-void Panel::play_new_chat_member() {
-	g_sound_handler.play_fx("lobby_freshmen", 128, PRIO_ALWAYS_PLAY);
+	g_sh->play_fx(SoundType::kUI, click_fx_);
+}
+
+/**
+ * This needs to be called once after g_soundhandler has been instantiated and before play_click() is called.
+ * We do it this way so that we don't have to register the same sound every time we create a new panel.
+ */
+void Panel::register_click() {
+	click_fx_ = SoundHandler::register_fx(SoundType::kUI, "sound/click");
 }
 
 /**

=== modified file 'src/ui_basic/panel.h'
--- src/ui_basic/panel.h	2019-02-23 11:00:49 +0000
+++ src/ui_basic/panel.h	2019-03-23 14:09:21 +0000
@@ -34,6 +34,7 @@
 #include "graphic/align.h"
 #include "graphic/font_handler.h"
 #include "graphic/panel_styles.h"
+#include "sound/constants.h"
 
 class RenderTarget;
 class Image;
@@ -293,6 +294,7 @@
 	}
 
 	virtual void die();
+	static void register_click();
 
 protected:
 	// This panel will never receive keypresses (do_key), instead
@@ -311,8 +313,6 @@
 	virtual void update_desired_size();
 
 	static void play_click();
-	static void play_new_chat_member();
-	static void play_new_chat_message();
 
 	static bool draw_tooltip(const std::string& text);
 	void draw_background(RenderTarget& dst, const UI::PanelStyleInfo&);
@@ -387,6 +387,8 @@
 	static Panel* mousein_;
 	static bool allow_user_input_;
 
+	static FxId click_fx_;
+
 	DISALLOW_COPY_AND_ASSIGN(Panel);
 };
 

=== modified file 'src/ui_basic/slider.cc'
--- src/ui_basic/slider.cc	2019-02-23 11:00:49 +0000
+++ src/ui_basic/slider.cc	2019-03-23 14:09:21 +0000
@@ -158,7 +158,7 @@
 		dst.brighten_rect(background_rect, MOUSE_OVER_BRIGHT_FACTOR);
 	}
 
-	if (pressed_) {       //  draw border
+	if (pressed_ || !enabled_) {
 		dst.brighten_rect  //  bottom edge
 		   (Recti(x, y + h - 2, w, 2), BUTTON_EDGE_BRIGHT_FACTOR);
 		dst.brighten_rect  //  right edge
@@ -307,6 +307,7 @@
 	highlighted_ = true;
 	relative_move_ = pointer - cursor_pos_;
 
+	clicked();
 	play_click();
 }
 

=== modified file 'src/ui_basic/slider.h'
--- src/ui_basic/slider.h	2019-02-23 11:00:49 +0000
+++ src/ui_basic/slider.h	2019-03-23 14:09:21 +0000
@@ -105,6 +105,7 @@
 	void set_highlighted(bool highlighted);
 
 public:
+	boost::signals2::signal<void()> clicked;
 	boost::signals2::signal<void()> changed;
 	boost::signals2::signal<void(int32_t)> changedto;
 

=== modified file 'src/ui_fsmenu/CMakeLists.txt'
--- src/ui_fsmenu/CMakeLists.txt	2018-02-13 16:52:12 +0000
+++ src/ui_fsmenu/CMakeLists.txt	2019-03-23 14:09:21 +0000
@@ -21,6 +21,7 @@
     profile
     sound
     widelands_ball_of_mud
+    wui_sound_options
 )
 
 wl_library(ui_fsmenu_base

=== modified file 'src/ui_fsmenu/internet_lobby.cc'
--- src/ui_fsmenu/internet_lobby.cc	2019-02-23 11:00:49 +0000
+++ src/ui_fsmenu/internet_lobby.cc	2019-03-23 14:09:21 +0000
@@ -33,6 +33,7 @@
 #include "network/internet_gaming.h"
 #include "profile/profile.h"
 #include "random/random.h"
+#include "sound/sound_handler.h"
 #include "ui_basic/messagebox.h"
 
 namespace {
@@ -57,6 +58,7 @@
      lisw_(get_w() * 623 / 1000),
      fs_(fs_small()),
      prev_clientlist_len_(1000),
+	 new_client_fx_(SoundHandler::register_fx(SoundType::kChat, "sound/lobby_freshmen")),
 
      // Text labels
      title(this, get_w() / 2, get_h() / 20, _("Metaserver Lobby"), UI::Align::kCenter),
@@ -113,6 +115,7 @@
      nickname_(nick),
      password_(pwd),
      is_registered_(registered) {
+
 	joingame_.sigclicked.connect(
 	   boost::bind(&FullscreenMenuInternetLobby::clicked_joingame, boost::ref(*this)));
 	hostgame_.sigclicked.connect(
@@ -295,7 +298,7 @@
 		}
 		// If a new player joins the lobby, play a sound.
 		if (clients->size() > prev_clientlist_len_ && !InternetGaming::ref().sound_off()) {
-			play_new_chat_member();
+			g_sh->play_fx(SoundType::kChat, new_client_fx_);
 		}
 		prev_clientlist_len_ = clients->size();
 	}

=== modified file 'src/ui_fsmenu/internet_lobby.h'
--- src/ui_fsmenu/internet_lobby.h	2019-02-23 11:00:49 +0000
+++ src/ui_fsmenu/internet_lobby.h	2019-03-23 14:09:21 +0000
@@ -52,6 +52,7 @@
 	uint32_t lisw_;
 	uint32_t fs_;
 	uint32_t prev_clientlist_len_;
+	FxId new_client_fx_;
 	UI::Textarea title, clients_, opengames_;
 	UI::Textarea servername_;
 	UI::Button joingame_, hostgame_, back_;

=== modified file 'src/ui_fsmenu/options.cc'
--- src/ui_fsmenu/options.cc	2019-02-23 11:00:49 +0000
+++ src/ui_fsmenu/options.cc	2019-03-23 14:09:21 +0000
@@ -163,9 +163,7 @@
                     UI::SpinBox::Units::kPixels),
 
      // Sound options
-     music_(&box_sound_, Vector2i::zero(), _("Enable music"), "", 0),
-     fx_(&box_sound_, Vector2i::zero(), _("Enable sound effects"), "", 0),
-     message_sound_(&box_sound_, Vector2i::zero(), _("Play a sound at message arrival"), "", 0),
+     sound_options_(box_sound_, UI::SliderStyle::kFsMenu),
 
      // Saving options
      sb_autosave_(&box_saving_,
@@ -254,16 +252,7 @@
 	box_windows_.add(&sb_dis_border_);
 
 	// Sound
-	box_sound_.add(&music_);
-	box_sound_.add(&fx_);
-	box_sound_.add(&message_sound_);
-
-	if (g_sound_handler.is_backend_disabled()) {
-		UI::Textarea* sound_warning = new UI::Textarea(
-		   &box_sound_, 0, 0, _("Sound is disabled due to a problem with the sound driver"));
-		sound_warning->set_color(UI_FONT_CLR_WARNING);
-		box_sound_.add(sound_warning);
-	}
+	box_sound_.add(&sound_options_);
 
 	// Saving
 	box_saving_.add(&sb_autosave_);
@@ -279,7 +268,7 @@
 	// Bind actions
 	language_dropdown_.selected.connect(
 	   boost::bind(&FullscreenMenuOptions::update_language_stats, this, false));
-	cancel_.sigclicked.connect(boost::bind(&FullscreenMenuOptions::clicked_back, this));
+	cancel_.sigclicked.connect(boost::bind(&FullscreenMenuOptions::clicked_cancel, this));
 	apply_.sigclicked.connect(boost::bind(&FullscreenMenuOptions::clicked_apply, this));
 	ok_.sigclicked.connect(boost::bind(&FullscreenMenuOptions::clicked_ok, this));
 
@@ -330,14 +319,6 @@
 	dock_windows_to_edges_.set_state(opt.dock_windows_to_edges);
 	animate_map_panning_.set_state(opt.animate_map_panning);
 
-	// Sound options
-	music_.set_state(opt.music);
-	music_.set_enabled(!g_sound_handler.is_backend_disabled());
-	fx_.set_state(opt.fx);
-	fx_.set_enabled(!g_sound_handler.is_backend_disabled());
-	message_sound_.set_state(opt.message_sound);
-	message_sound_.set_enabled(!g_sound_handler.is_backend_disabled());
-
 	// Saving options
 	zip_.set_state(opt.zip);
 	write_syncstreams_.set_state(opt.write_syncstreams);
@@ -404,9 +385,7 @@
 	sb_dis_border_.set_desired_size(tab_panel_width, sb_dis_border_.get_h());
 
 	// Sound options
-	music_.set_desired_size(tab_panel_width, music_.get_h());
-	fx_.set_desired_size(tab_panel_width, fx_.get_h());
-	message_sound_.set_desired_size(tab_panel_width, message_sound_.get_h());
+	sound_options_.set_desired_size(tab_panel_width, tabs_.get_inner_h());
 
 	// Saving options
 	sb_autosave_.set_unit_width(250);
@@ -551,6 +530,11 @@
 	end_modal<FullscreenMenuBase::MenuTarget>(FullscreenMenuBase::MenuTarget::kApplyOptions);
 }
 
+void FullscreenMenuOptions::clicked_cancel() {
+	g_sh->load_config();
+	clicked_back();
+}
+
 OptionsCtrl::OptionsStruct FullscreenMenuOptions::get_values() {
 	// Write all data from UI elements
 	// Interface options
@@ -573,11 +557,6 @@
 	os_.panel_snap_distance = sb_dis_panel_.get_value();
 	os_.border_snap_distance = sb_dis_border_.get_value();
 
-	// Sound options
-	os_.music = music_.get_state();
-	os_.fx = fx_.get_state();
-	os_.message_sound = message_sound_.get_state();
-
 	// Saving options
 	os_.autosave = sb_autosave_.get_value();
 	os_.rolling_autosave = sb_rolling_autosave_.get_value();
@@ -633,11 +612,6 @@
 	opt.panel_snap_distance = opt_section_.get_int("panel_snap_distance", 0);
 	opt.border_snap_distance = opt_section_.get_int("border_snap_distance", 0);
 
-	// Sound options
-	opt.music = !opt_section_.get_bool("disable_music", false);
-	opt.fx = !opt_section_.get_bool("disable_fx", false);
-	opt.message_sound = opt_section_.get_bool("sound_at_message", true);
-
 	// Saving options
 	opt.autosave = opt_section_.get_int("autosave", kDefaultAutosaveInterval * 60);
 	opt.rolling_autosave = opt_section_.get_int("rolling_autosave", 5);
@@ -674,11 +648,6 @@
 	opt_section_.set_int("panel_snap_distance", opt.panel_snap_distance);
 	opt_section_.set_int("border_snap_distance", opt.border_snap_distance);
 
-	// Sound options
-	opt_section_.set_bool("disable_music", !opt.music);
-	opt_section_.set_bool("disable_fx", !opt.fx);
-	opt_section_.set_bool("sound_at_message", opt.message_sound);
-
 	// Saving options
 	opt_section_.set_int("autosave", opt.autosave * 60);
 	opt_section_.set_int("rolling_autosave", opt.rolling_autosave);
@@ -696,8 +665,9 @@
 	WLApplication::get()->set_input_grab(opt.inputgrab);
 	i18n::set_locale(opt.language);
 	UI::g_fh->reinitialize_fontset(i18n::get_locale());
-	g_sound_handler.set_disable_music(!opt.music);
-	g_sound_handler.set_disable_fx(!opt.fx);
+
+	// Sound options
+	g_sh->save_config();
 
 	// Now write to file
 	g_options.write(kConfigFile.c_str(), true);

=== modified file 'src/ui_fsmenu/options.h'
--- src/ui_fsmenu/options.h	2019-02-23 11:00:49 +0000
+++ src/ui_fsmenu/options.h	2019-03-23 14:09:21 +0000
@@ -34,6 +34,7 @@
 #include "ui_basic/tabpanel.h"
 #include "ui_basic/textarea.h"
 #include "ui_fsmenu/base.h"
+#include "wui/sound_options.h"
 
 class FullscreenMenuOptions;
 class Section;
@@ -56,11 +57,6 @@
 		int32_t border_snap_distance;
 		bool animate_map_panning;
 
-		// Sound options
-		bool music;
-		bool fx;
-		bool message_sound;
-
 		// Saving options
 		int32_t autosave;          // autosave interval in minutes
 		int32_t rolling_autosave;  // number of file to use for rolling autosave
@@ -107,6 +103,8 @@
 
 	// Saves the options and reloads the active tab
 	void clicked_apply();
+	// Restores old options when canceled
+	void clicked_cancel();
 
 	const uint32_t padding_;
 	uint32_t butw_;
@@ -143,9 +141,7 @@
 	UI::SpinBox sb_dis_border_;
 
 	// Sound options
-	UI::Checkbox music_;
-	UI::Checkbox fx_;
-	UI::Checkbox message_sound_;
+	SoundOptions sound_options_;
 
 	// Saving options
 	UI::SpinBox sb_autosave_;

=== modified file 'src/website/CMakeLists.txt'
--- src/website/CMakeLists.txt	2018-09-02 15:09:05 +0000
+++ src/website/CMakeLists.txt	2019-03-23 14:09:21 +0000
@@ -10,7 +10,6 @@
     base_i18n
     graphic
     io_filesystem
-    sound
 )
 
 

=== modified file 'src/website/website_common.cc'
--- src/website/website_common.cc	2019-02-23 11:00:49 +0000
+++ src/website/website_common.cc	2019-03-23 14:09:21 +0000
@@ -26,7 +26,6 @@
 #include "graphic/graphic.h"
 #include "io/filesystem/filesystem.h"
 #include "io/filesystem/layered_filesystem.h"
-#include "sound/sound_handler.h"
 
 // Setup the static objects Widelands needs to operate and initializes systems.
 void initialize() {
@@ -39,19 +38,14 @@
 	g_fs = new LayeredFileSystem();
 	g_fs->add_file_system(&FileSystem::create(INSTALL_DATADIR));
 
-	// We don't really need graphics or sound here, but we will get error messages
+	// We don't really need graphics here, but we will get error messages
 	// when they aren't initialized
 	g_gr = new Graphic();
 	g_gr->initialize(Graphic::TraceGl::kNo, 1, 1, false);
-
-	g_sound_handler.init();
-	g_sound_handler.nosound_ = true;
 }
 
 // Cleanup before program end
 void cleanup() {
-	g_sound_handler.shutdown();
-
 	if (g_gr) {
 		delete g_gr;
 		g_gr = nullptr;

=== modified file 'src/wlapplication.cc'
--- src/wlapplication.cc	2019-03-09 12:13:02 +0000
+++ src/wlapplication.cc	2019-03-23 14:09:21 +0000
@@ -362,7 +362,16 @@
 	   config.get_bool("debug_gl_trace", false) ? Graphic::TraceGl::kYes : Graphic::TraceGl::kNo,
 	   config.get_int("xres", DEFAULT_RESOLUTION_W), config.get_int("yres", DEFAULT_RESOLUTION_H),
 	   config.get_bool("fullscreen", false));
-	g_sound_handler.init();  //  TODO(unknown): memory leak!
+
+	g_sh = new SoundHandler();
+
+	g_sh->register_songs("music", "intro");
+	g_sh->register_songs("music", "menu");
+	g_sh->register_songs("music", "ingame");
+
+	// Register the click sound for UI::Panel.
+	// We do it here to ensure that the sound handler has been created first, and we only want to register it once.
+	UI::Panel::register_click();
 
 	// This might grab the input.
 	refresh_graphics();
@@ -415,7 +424,7 @@
 	refresh_graphics();
 
 	if (game_type_ == EDITOR) {
-		g_sound_handler.start_music("ingame");
+		g_sh->change_music("ingame");
 		EditorInteractive::run_editor(filename_, script_to_run_);
 	} else if (game_type_ == REPLAY) {
 		replay();
@@ -443,18 +452,18 @@
 			throw;
 		}
 	} else {
-		g_sound_handler.start_music("intro");
+		g_sh->change_music("intro");
 
 		{
 			FullscreenMenuIntro intro;
 			intro.run<FullscreenMenuBase::MenuTarget>();
 		}
 
-		g_sound_handler.change_music("menu", 1000);
+		g_sh->change_music("menu", 1000);
 		mainmenu();
 	}
 
-	g_sound_handler.stop_music(500);
+	g_sh->stop_music(500);
 
 	return;
 }
@@ -488,10 +497,27 @@
 		}
 		break;
 
-	case SDL_USEREVENT:
-		if (ev.user.code == CHANGE_MUSIC)
-			g_sound_handler.change_music();
-		break;
+	case SDL_USEREVENT: {
+		if (ev.user.code == CHANGE_MUSIC) {
+			/* Notofication from the SoundHandler that a song has finished playing.
+			 * Usually, another song from the same songset will be started.
+			 * There is a special case for the intro screen's music: only one song will be
+			 * played. If the user has not clicked the mouse or pressed escape when the song
+			 * finishes, Widelands will automatically go on to the main menu.
+			 */
+			assert(!SoundHandler::is_backend_disabled());
+			if (g_sh->current_songset() == "intro") {
+				// Special case for splashscreen: there, only one song is ever played
+				SDL_Event new_event;
+				new_event.type = SDL_KEYDOWN;
+				new_event.key.state = SDL_PRESSED;
+				new_event.key.keysym.sym = SDLK_ESCAPE;
+				SDL_PushEvent(&new_event);
+			} else {
+				g_sh->change_music();
+			}
+		}
+	} break;
 
 	default:
 		break;
@@ -763,8 +789,6 @@
 	s.get_bool("dock_windows_to_edges");
 	s.get_bool("write_syncstreams");
 	// Undocumented on command line, appears in game options
-	s.get_bool("sound_at_message");
-	// Undocumented on command line, appears in game options
 	s.get_bool("transparent_chat");
 	// Undocumented. Unique ID used to allow the metaserver to recognize players
 	s.get_string("uuid");
@@ -869,7 +893,8 @@
 	alarm(5);
 #endif
 
-	g_sound_handler.shutdown();
+	delete g_sh;
+	g_sh = nullptr;
 
 	SDL_QuitSubSystem(SDL_INIT_TIMER | SDL_INIT_VIDEO | SDL_INIT_JOYSTICK);
 }
@@ -928,7 +953,7 @@
  */
 void WLApplication::handle_commandline_parameters() {
 	if (commandline_.count("nosound")) {
-		g_sound_handler.nosound_ = true;
+		SoundHandler::disable_backend();
 		commandline_.erase("nosound");
 	}
 	if (commandline_.count("nozip")) {
@@ -1212,6 +1237,8 @@
 			NEVER_HERE();
 		}
 
+		g_sh->change_music("ingame", 1000);
+
 		if (internet) {
 			std::string playername = mp.get_nickname();
 			std::string password(mp.get_password());
@@ -1265,6 +1292,7 @@
 				break;
 			}
 		}
+		g_sh->change_music("menu", 1000);
 	}
 }
 

=== modified file 'src/wlapplication_messages.cc'
--- src/wlapplication_messages.cc	2019-02-23 11:00:49 +0000
+++ src/wlapplication_messages.cc	2019-03-23 14:09:21 +0000
@@ -85,11 +85,7 @@
 	               "                      Port number of the metaserver for internet gaming.")
 	          << endl
 	          << endl
-
-	          << _("Sound options:") << endl
 	          << _(" --nosound            Starts the game with sound disabled.") << endl
-	          << _(" --disable_fx         Disable sound effects.") << endl
-	          << _(" --disable_music      Disable music.") << endl
 	          << endl
 	          << _(" --nozip              Do not save files as binary zip archives.") << endl
 	          << endl

=== modified file 'src/wui/CMakeLists.txt'
--- src/wui/CMakeLists.txt	2018-11-13 12:18:10 +0000
+++ src/wui/CMakeLists.txt	2019-03-23 14:09:21 +0000
@@ -1,3 +1,12 @@
+wl_library(wui_sound_options
+  SRCS
+    sound_options.cc
+    sound_options.h
+  DEPENDS
+    base_i18n
+    ui_basic
+)
+
 wl_library(wui_chat_ui
   SRCS
     chat_overlay.cc
@@ -302,5 +311,6 @@
     wui_mapview
     wui_mapview_pixelfunctions
     wui_quicknavigation
+    wui_sound_options
     wui_waresdisplay
 )

=== modified file 'src/wui/game_chat_panel.cc'
--- src/wui/game_chat_panel.cc	2019-02-23 11:00:49 +0000
+++ src/wui/game_chat_panel.cc	2019-03-23 14:09:21 +0000
@@ -22,6 +22,7 @@
 #include <limits>
 #include <string>
 
+#include "sound/sound_handler.h"
 #include "wui/chat_msg_layout.h"
 
 /**
@@ -46,7 +47,9 @@
              UI::Align::kLeft,
              UI::MultilineTextarea::ScrollMode::kScrollLogForced),
      editbox(this, 0, h - 20, w, 20, 2, style),
-     chat_message_counter(0) {
+     chat_message_counter(0),
+	 chat_sound(SoundHandler::register_fx(SoundType::kChat, "sound/lobby_chat")) {
+
 	editbox.ok.connect(boost::bind(&GameChatPanel::key_enter, this));
 	editbox.cancel.connect(boost::bind(&GameChatPanel::key_escape, this));
 	editbox.activate_history(true);
@@ -79,7 +82,7 @@
 		for (size_t i = chat_message_counter; i < msgs_size; ++i) {
 			if (!msgs[i].sender.empty()) {
 				// Got a message that is no system message. Beep
-				play_new_chat_message();
+				g_sh->play_fx(SoundType::kChat, chat_sound);
 				break;
 			}
 		}

=== modified file 'src/wui/game_chat_panel.h'
--- src/wui/game_chat_panel.h	2019-02-23 11:00:49 +0000
+++ src/wui/game_chat_panel.h	2019-03-23 14:09:21 +0000
@@ -66,6 +66,7 @@
 	UI::MultilineTextarea chatbox;
 	UI::EditBox editbox;
 	size_t chat_message_counter;
+	FxId chat_sound;
 	std::unique_ptr<Notifications::Subscriber<ChatMessage>> chat_message_subscriber_;
 };
 

=== modified file 'src/wui/game_options_menu.cc'
--- src/wui/game_options_menu.cc	2019-02-23 11:00:49 +0000
+++ src/wui/game_options_menu.cc	2019-03-23 14:09:21 +0000
@@ -26,7 +26,6 @@
 
 #include "base/i18n.h"
 #include "graphic/graphic.h"
-#include "sound/sound_handler.h"
 #include "wui/game_exit_confirm_box.h"
 #include "wui/game_main_menu_save_game.h"
 #include "wui/game_options_sound_menu.h"

=== modified file 'src/wui/game_options_sound_menu.cc'
--- src/wui/game_options_sound_menu.cc	2019-02-23 11:00:49 +0000
+++ src/wui/game_options_sound_menu.cc	2019-03-23 14:09:21 +0000
@@ -20,125 +20,25 @@
 
 #include "base/i18n.h"
 #include "sound/sound_handler.h"
-#include "ui_basic/multilinetextarea.h"
-
-GameOptionsSoundMenu::GameOptionsSoundMenu(InteractiveGameBase& gb,
+
+namespace {
+constexpr int kMargin = 12;
+} // namespace
+
+GameOptionsSoundMenu::GameOptionsSoundMenu(Panel& parent,
                                            UI::UniqueWindow::Registry& registry)
-   : UI::UniqueWindow(&gb, "sound_options_menu", &registry, 160, 160, _("Sound Options")),
-     ingame_music(this, Vector2i(hmargin(), vmargin()), _("Enable music")),
-     ingame_sound(this,
-                  Vector2i(hmargin(), vmargin() + kStateboxSize + vspacing()),
-                  _("Enable sound effects")),
-     ingame_music_volume_label(this,
-                               hmargin(),
-                               vmargin() + 2 * (kStateboxSize + vspacing()) + vbigspacing(),
-                               _("Music volume")),
-     ingame_music_volume(this,
-                         hmargin(),
-                         vmargin() + 2 * (kStateboxSize + vspacing()) + vbigspacing() +
-                            1 * vspacing() + ingame_music_volume_label.get_h(),
-                         get_inner_w() - 2 * hmargin(),
-                         slideh(),
-                         0,
-                         g_sound_handler.get_max_volume(),
-                         g_sound_handler.get_music_volume(),
-                         UI::SliderStyle::kWuiLight),
-     ingame_sound_volume_label(this,
-                               hmargin(),
-                               vmargin() + 2 * (kStateboxSize + vspacing()) + vbigspacing() +
-                                  2 * vspacing() + slideh() + ingame_music_volume_label.get_h(),
-                               _("Sound effects volume")),
-     ingame_sound_volume(this,
-                         hmargin(),
-                         vmargin() + 2 * (kStateboxSize + vspacing()) + vbigspacing() +
-                            3 * vspacing() + slideh() + ingame_music_volume_label.get_h() +
-                            ingame_music_volume_label.get_h(),
-                         get_inner_w() - 2 * hmargin(),
-                         slideh(),
-                         0,
-                         g_sound_handler.get_max_volume(),
-                         g_sound_handler.get_fx_volume(),
-                         UI::SliderStyle::kWuiLight) {
-	ingame_music.set_state(!g_sound_handler.get_disable_music());
-	ingame_sound.set_state(!g_sound_handler.get_disable_fx());
-
-	uint32_t boxes_width =
-	   kStateboxSize + hspacing() + std::max(ingame_music.get_w(), ingame_sound.get_w());
-	uint32_t labels_width =
-	   std::max(ingame_music_volume_label.get_w(), ingame_sound_volume_label.get_w());
-
-	set_inner_size(std::max(static_cast<uint32_t>(get_inner_w()),
-	                        2 * hmargin() + std::max(boxes_width, labels_width)),
-	               2 * vmargin() + 2 * (kStateboxSize + vspacing()) + vbigspacing() +
-	                  3 * vspacing() + 2 * slideh() + ingame_music_volume_label.get_h() +
-	                  ingame_music_volume_label.get_h());
-
-	if (g_sound_handler.is_backend_disabled()) {  //  disabling sound options
-		ingame_music.set_enabled(false);
-		ingame_sound.set_enabled(false);
-		ingame_music_volume.set_enabled(false);
-		ingame_sound_volume.set_enabled(false);
-
-		UI::MultilineTextarea* sound_warning = new UI::MultilineTextarea(
-		   this, hmargin(), ingame_sound_volume.get_y() + ingame_sound_volume.get_h() + vspacing(),
-		   get_inner_w() - 2 * hmargin(), 0, UI::PanelStyle::kWui,
-		   _("Sound is disabled due to a problem with the sound driver"), UI::Align::kLeft,
-		   UI::MultilineTextarea::ScrollMode::kNoScrolling);
-
-		sound_warning->set_color(UI_FONT_CLR_WARNING);
-		set_size(get_w(), get_h() + sound_warning->get_h() + vspacing());
-
-	} else {  // initial widget states
-		ingame_music.set_state(!g_sound_handler.get_disable_music());
-		ingame_sound.set_state(!g_sound_handler.get_disable_fx());
-		ingame_music_volume.set_enabled(!g_sound_handler.get_disable_music());
-		ingame_sound_volume.set_enabled(!g_sound_handler.get_disable_fx());
+   : UI::UniqueWindow(&parent, "sound_options_menu", &registry, 100, 100, _("Sound Options")),
+	 sound_options_(*this, UI::SliderStyle::kWuiLight) {
+	sound_options_.set_border(kMargin, kMargin, kMargin, kMargin);
+
+	set_center_panel(&sound_options_);
+
+	if (get_usedefaultpos()) {
+		center_to_parent();
 	}
-
-	//  ready signals
-	ingame_music.changedto.connect(
-	   boost::bind(&GameOptionsSoundMenu::changed_ingame_music, this, _1));
-	ingame_sound.changedto.connect(
-	   boost::bind(&GameOptionsSoundMenu::changed_ingame_sound, this, _1));
-	ingame_music_volume.changedto.connect(
-	   boost::bind(&GameOptionsSoundMenu::music_volume_changed, this, _1));
-	ingame_sound_volume.changedto.connect(
-	   boost::bind(&GameOptionsSoundMenu::sound_volume_changed, this, _1));
-
-	if (get_usedefaultpos())
-		center_to_parent();
-}
-
-/**
- * \brief The music checkbox has been toggled.
- */
-void GameOptionsSoundMenu::changed_ingame_music(bool on) {
-	ingame_music_volume.set_enabled(on);
-	g_sound_handler.set_disable_music(!on);
-}
-
-/**
- * \brief The FX checkbox has been toggled.
- */
-void GameOptionsSoundMenu::changed_ingame_sound(bool on) {
-	ingame_sound_volume.set_enabled(on);
-	g_sound_handler.set_disable_fx(!on);
-}
-
-/**
- * \brief Callback for the music volume slider.
- *
- * \param value The new music volume.
- */
-void GameOptionsSoundMenu::music_volume_changed(int32_t value) {
-	g_sound_handler.set_music_volume(value);
-}
-
-/**
- * \brief Callback for the sound volume slider.
- *
- * \param value The new sound volume value.
- */
-void GameOptionsSoundMenu::sound_volume_changed(int32_t value) {
-	g_sound_handler.set_fx_volume(value);
+}
+
+
+GameOptionsSoundMenu::~GameOptionsSoundMenu() {
+	g_sh->save_config();
 }

=== modified file 'src/wui/game_options_sound_menu.h'
--- src/wui/game_options_sound_menu.h	2019-02-23 11:00:49 +0000
+++ src/wui/game_options_sound_menu.h	2019-03-23 14:09:21 +0000
@@ -19,54 +19,19 @@
 #ifndef WL_WUI_GAME_OPTIONS_SOUND_MENU_H
 #define WL_WUI_GAME_OPTIONS_SOUND_MENU_H
 
-#include "ui_basic/checkbox.h"
-#include "ui_basic/slider.h"
-#include "ui_basic/textarea.h"
 #include "ui_basic/unique_window.h"
-#include "wui/interactive_gamebase.h"
+#include "wui/sound_options.h"
 
 /**
  * A window with all sound options.
  */
 struct GameOptionsSoundMenu : public UI::UniqueWindow {
-	GameOptionsSoundMenu(InteractiveGameBase&, UI::UniqueWindow::Registry&);
+	GameOptionsSoundMenu(Panel&, UI::UniqueWindow::Registry&);
+	/// Saves the sound options to config
+	~GameOptionsSoundMenu() override;
 
 private:
-	UI::Checkbox ingame_music;
-	UI::Checkbox ingame_sound;
-
-	UI::Textarea ingame_music_volume_label;
-	UI::HorizontalSlider ingame_music_volume;
-	UI::Textarea ingame_sound_volume_label;
-	UI::HorizontalSlider ingame_sound_volume;
-
-	/// Returns the horizontal/vertical spacing between widgets.
-	uint32_t hspacing() const {
-		return 5;
-	}
-	uint32_t vspacing() const {
-		return 5;
-	}
-	uint32_t vbigspacing() const {
-		return 8 + vspacing();
-	}
-	uint32_t slideh() const {
-		return 28;
-	}
-
-	/// Returns the horizontal/vertical margin between edge and buttons.
-	uint32_t hmargin() const {
-		return 2 * hspacing();
-	}
-	uint32_t vmargin() const {
-		return 2 * vspacing();
-	}
-
-	//  calbacks
-	void changed_ingame_music(bool on);
-	void changed_ingame_sound(bool on);
-	void music_volume_changed(int32_t value);
-	void sound_volume_changed(int32_t value);
+	SoundOptions sound_options_;
 };
 
 #endif  // end of include guard: WL_WUI_GAME_OPTIONS_SOUND_MENU_H

=== modified file 'src/wui/interactive_base.cc'
--- src/wui/interactive_base.cc	2019-02-27 17:19:00 +0000
+++ src/wui/interactive_base.cc	2019-03-23 14:09:21 +0000
@@ -25,7 +25,9 @@
 #include <boost/bind.hpp>
 #include <boost/format.hpp>
 
+#include "base/log.h"
 #include "base/macros.h"
+#include "base/math.h"
 #include "base/time_string.h"
 #include "economy/flag.h"
 #include "economy/road.h"
@@ -46,6 +48,7 @@
 #include "logic/widelands_geometry.h"
 #include "profile/profile.h"
 #include "scripting/lua_interface.h"
+#include "sound/sound_handler.h"
 #include "wui/game_chat_menu.h"
 #include "wui/game_debug_ui.h"
 #include "wui/interactive_player.h"
@@ -155,11 +158,7 @@
 		   adjust_toolbar_position();
 	   });
 	sound_subscriber_ = Notifications::subscribe<NoteSound>([this](const NoteSound& note) {
-		if (note.stereo_position != std::numeric_limits<uint32_t>::max()) {
-			g_sound_handler.play_fx(note.fx, note.stereo_position, note.priority);
-		} else if (note.coords != Widelands::Coords::null()) {
-			g_sound_handler.play_fx(note.fx, stereo_position(note.coords), note.priority);
-		}
+		play_sound_effect(note);
 	});
 
 	toolbar_.set_layout_toplevel(true);
@@ -729,25 +728,32 @@
 	Notifications::publish(lm);
 }
 
-/** Calculate  the position of an effect in relation to the visible part of the
- * screen.
- * \param position  where the event happened (map coordinates)
- * \return position in widelands' game window: left=0, right=254, not in
- * viewport = -1
- * \note This function can also be used to check whether a logical coordinate is
- * visible at all
+/**
+ * Plays a sound effect positioned according to the map coordinates in the note.
  */
-int32_t InteractiveBase::stereo_position(Widelands::Coords const position_map) {
-	assert(position_map);
-
-	// Viewpoint is the point of the map in pixel which is shown in the upper
-	// left corner of window or fullscreen
-	const MapView::ViewArea area = map_view_.view_area();
-	if (!area.contains(position_map)) {
-		return -1;
-	}
-	const Vector2f position_pix = area.move_inside(position_map);
-	return static_cast<int>((position_pix.x - area.rect().x) * 254 / area.rect().w);
+void InteractiveBase::play_sound_effect(const NoteSound& note) const {
+	if (!g_sh->is_sound_enabled(note.type)) {
+		return;
+	}
+
+	if (note.coords != Widelands::Coords::null() && player_hears_field(note.coords)) {
+		constexpr int kSoundMaxDistance = 255;
+		constexpr float kSoundDistanceDivisor = 4.f;
+
+		// Viewpoint is the point of the map in pixel which is shown in the upper
+		// left corner of window or fullscreen
+		const MapView::ViewArea area = map_view_.view_area();
+		const Vector2f position_pix = area.find_pixel_for_coordinates(note.coords);
+		const int stereo_pos = static_cast<int>((position_pix.x - area.rect().x) * kStereoRight / area.rect().w);
+
+		int distance = MapviewPixelFunctions::calc_pix_distance(egbase().map(), area.rect().center(), position_pix) / kSoundDistanceDivisor;
+
+		distance = (note.priority == kFxPriorityAlwaysPlay) ? (math::clamp(distance, 0, kSoundMaxDistance) / 2) : distance;
+
+		if (distance < kSoundMaxDistance) {
+			g_sh->play_fx(note.type, note.fx, note.priority, math::clamp(stereo_pos, kStereoLeft, kStereoRight), distance);
+		}
+	}
 }
 
 // Repositions the chat overlay

=== modified file 'src/wui/interactive_base.h'
--- src/wui/interactive_base.h	2019-02-27 17:19:00 +0000
+++ src/wui/interactive_base.h	2019-03-23 14:09:21 +0000
@@ -30,7 +30,6 @@
 #include "notifications/notifications.h"
 #include "profile/profile.h"
 #include "sound/note_sound.h"
-#include "sound/sound_handler.h"
 #include "ui_basic/box.h"
 #include "ui_basic/textarea.h"
 #include "ui_basic/unique_window.h"
@@ -53,8 +52,6 @@
  */
 class InteractiveBase : public UI::Panel, public DebugConsole::Handler {
 public:
-	friend class SoundHandler;
-
 	enum {
 		dfShowCensus = 1,      ///< show census report on buildings
 		dfShowStatistics = 2,  ///< show statistics report on buildings
@@ -247,8 +244,10 @@
 	/// Returns true if there is a workarea preview being shown at the given coordinates
 	bool has_workarea_preview(const Widelands::Coords& coords) const;
 
+	virtual bool player_hears_field(const Widelands::Coords& coords) const = 0;
+
 private:
-	int32_t stereo_position(Widelands::Coords position_map);
+	void play_sound_effect(const NoteSound& note) const;
 	void resize_chat_overlay();
 	void roadb_add_overlay();
 	void roadb_remove_overlay();

=== modified file 'src/wui/interactive_player.cc'
--- src/wui/interactive_player.cc	2019-03-23 14:09:20 +0000
+++ src/wui/interactive_player.cc	2019-03-23 14:09:21 +0000
@@ -106,7 +106,7 @@
 	Widelands::BaseImmovable* const imm = field.fcoords.field->get_immovable();
 	if (imm != nullptr && imm->get_positions(egbase).front() == field.fcoords) {
 		imm->draw(egbase.get_gametime(), filter_text_to_draw(text_to_draw, imm, player),
-		          field.rendertarget_pixel, scale, dst);
+		          field.rendertarget_pixel, field.fcoords, scale, dst);
 	}
 }
 
@@ -118,7 +118,7 @@
                                  RenderTarget* dst) {
 	for (Widelands::Bob* bob = field.fcoords.field->get_first_bob(); bob;
 	     bob = bob->get_next_bob()) {
-		bob->draw(egbase, filter_text_to_draw(text_to_draw, bob, player), field.rendertarget_pixel,
+		bob->draw(egbase, filter_text_to_draw(text_to_draw, bob, player), field.rendertarget_pixel, field.fcoords,
 		          scale, dst);
 	}
 }
@@ -134,20 +134,20 @@
 	if (player_field.constructionsite.becomes) {
 		assert(field.owner != nullptr);
 		player_field.constructionsite.draw(
-		   field.rendertarget_pixel, scale, field.owner->get_playercolor(), dst);
+		   field.rendertarget_pixel, field.fcoords, scale, field.owner->get_playercolor(), dst);
 
 	} else if (upcast(const Widelands::BuildingDescr, building, player_field.map_object_descr)) {
 		assert(field.owner != nullptr);
 		// this is a building therefore we either draw unoccupied or idle animation
-		dst->blit_animation(field.rendertarget_pixel, scale, building->get_unoccupied_animation(), 0,
+		dst->blit_animation(field.rendertarget_pixel, field.fcoords, scale, building->get_unoccupied_animation(), 0,
 		                    &field.owner->get_playercolor());
 	} else if (player_field.map_object_descr->type() == Widelands::MapObjectType::FLAG) {
 		assert(field.owner != nullptr);
-		dst->blit_animation(field.rendertarget_pixel, scale, field.owner->tribe().flag_animation(), 0,
+		dst->blit_animation(field.rendertarget_pixel, field.fcoords, scale, field.owner->tribe().flag_animation(), 0,
 		                    &field.owner->get_playercolor());
 	} else if (const uint32_t pic = player_field.map_object_descr->main_animation()) {
 		dst->blit_animation(
-		   field.rendertarget_pixel, scale, pic, 0, (field.owner == nullptr) ? nullptr : &field.owner->get_playercolor());
+		   field.rendertarget_pixel, field.fcoords, scale, pic, 0, (field.owner == nullptr) ? nullptr : &field.owner->get_playercolor());
 	}
 }
 
@@ -512,6 +512,17 @@
 void InteractivePlayer::cleanup_for_load() {
 }
 
+bool InteractivePlayer::player_hears_field(const Widelands::Coords& coords) const {
+	const Widelands::Player& plr = player();
+	if (plr.see_all()) {
+		return true;
+	}
+
+	const Widelands::Map& map = egbase().map();
+	const Widelands::Player::Field& player_field = plr.fields()[map.get_index(coords, map.get_width())];
+	return (player_field.vision > 1);
+}
+
 void InteractivePlayer::cmdSwitchPlayer(const std::vector<std::string>& args) {
 	if (args.size() != 2) {
 		DebugConsole::write("Usage: switchplayer <nr>");

=== modified file 'src/wui/interactive_player.h'
--- src/wui/interactive_player.h	2019-02-23 11:00:49 +0000
+++ src/wui/interactive_player.h	2019-03-23 14:09:21 +0000
@@ -74,6 +74,8 @@
 	void popup_message(Widelands::MessageId, const Widelands::Message&);
 
 private:
+	bool player_hears_field(const Widelands::Coords& coords) const override;
+
 	void cmdSwitchPlayer(const std::vector<std::string>& args);
 
 	Widelands::PlayerNumber player_number_;

=== modified file 'src/wui/interactive_spectator.cc'
--- src/wui/interactive_spectator.cc	2019-02-27 17:19:00 +0000
+++ src/wui/interactive_spectator.cc	2019-03-23 14:09:21 +0000
@@ -129,12 +129,12 @@
 
 		Widelands::BaseImmovable* const imm = field.fcoords.field->get_immovable();
 		if (imm != nullptr && imm->get_positions(the_game).front() == field.fcoords) {
-			imm->draw(gametime, text_to_draw, field.rendertarget_pixel, scale, dst);
+			imm->draw(gametime, text_to_draw, field.rendertarget_pixel, field.fcoords, scale, dst);
 		}
 
 		for (Widelands::Bob* bob = field.fcoords.field->get_first_bob(); bob;
 		     bob = bob->get_next_bob()) {
-			bob->draw(the_game, text_to_draw, field.rendertarget_pixel, scale, dst);
+			bob->draw(the_game, text_to_draw, field.rendertarget_pixel, field.fcoords, scale, dst);
 		}
 
 		// Draw work area previews.
@@ -179,6 +179,11 @@
 	return nullptr;
 }
 
+
+bool InteractiveSpectator::player_hears_field(const Widelands::Coords&) const {
+	return true;
+}
+
 // Toolbar button callback functions.
 void InteractiveSpectator::exit_btn() {
 	if (is_multiplayer()) {

=== modified file 'src/wui/interactive_spectator.h'
--- src/wui/interactive_spectator.h	2019-02-23 11:00:49 +0000
+++ src/wui/interactive_spectator.h	2019-03-23 14:09:21 +0000
@@ -47,13 +47,14 @@
 	void draw_map_view(MapView* given_map_view, RenderTarget* dst) override;
 
 private:
+	bool player_hears_field(const Widelands::Coords& coords) const override;
+
 	void exit_btn();
 	bool can_see(Widelands::PlayerNumber) const override;
 	bool can_act(Widelands::PlayerNumber) const override;
 	Widelands::PlayerNumber player_number() const override;
 	void node_action(const Widelands::NodeAndTriangle<>& node_and_triangle) override;
 
-private:
 	UI::UniqueWindow::Registry chat_;
 	UI::UniqueWindow::Registry options_;
 };

=== modified file 'src/wui/itemwaresdisplay.cc'
--- src/wui/itemwaresdisplay.cc	2019-03-23 14:09:20 +0000
+++ src/wui/itemwaresdisplay.cc	2019-03-23 14:09:21 +0000
@@ -119,7 +119,7 @@
 		if (it.worker) {
 			y += IWD_WorkerBaseline;
 			constexpr float kZoom = 1.f;
-			dst.blit_animation(Vector2f(x + (IWD_ItemWidth / 2.f), y + (IWD_ItemHeight / 2.f)), kZoom,
+			dst.blit_animation(Vector2f(x + (IWD_ItemWidth / 2.f), y + (IWD_ItemHeight / 2.f)), Widelands::Coords::null(), kZoom,
 			                   tribe.get_worker_descr(it.index)->main_animation(), 0,
 			                   &player().get_playercolor());
 		} else {

=== modified file 'src/wui/mapview.cc'
--- src/wui/mapview.cc	2019-03-12 08:23:51 +0000
+++ src/wui/mapview.cc	2019-03-23 14:09:21 +0000
@@ -245,7 +245,7 @@
 	return contains_map_pixel(MapviewPixelFunctions::to_map_pixel_with_normalization(map_, c));
 }
 
-Vector2f MapView::ViewArea::move_inside(const Widelands::Coords& c) const {
+Vector2f MapView::ViewArea::find_pixel_for_coordinates(const Widelands::Coords& c) const {
 	// We want to figure out to which pixel 'c' maps inside our rect_. Since
 	// Wideland's map is a torus, the current 'rect_' could span the origin.
 	// Without loss of generality we only discuss x - y follows accordingly.
@@ -257,8 +257,9 @@
 	// that the point is contained inside of 'rect_'. If we now convert to
 	// panel pixels, we are guaranteed that the pixel we get back is inside the
 	// screen bounds.
+	// Also supports coordinates outside of the view area, for use by the sound system
 	Vector2f p = MapviewPixelFunctions::to_map_pixel_with_normalization(map_, c);
-	assert(contains_map_pixel(p));
+	assert(!contains(c) || contains_map_pixel(p));
 
 	const float w = MapviewPixelFunctions::get_map_end_screen_x(map_);
 	const float h = MapviewPixelFunctions::get_map_end_screen_y(map_);
@@ -315,7 +316,7 @@
 	if (!area.contains(c)) {
 		return;
 	}
-	mouse_to_pixel(round(to_panel(area.move_inside(c))), transition);
+	mouse_to_pixel(round(to_panel(area.find_pixel_for_coordinates(c))), transition);
 }
 
 void MapView::mouse_to_pixel(const Vector2i& pixel, const Transition& transition) {

=== modified file 'src/wui/mapview.h'
--- src/wui/mapview.h	2019-03-12 08:23:51 +0000
+++ src/wui/mapview.h	2019-03-23 14:09:21 +0000
@@ -55,7 +55,7 @@
 		// Returns a map pixel 'p' such that rect().x <= p.x <= rect().x + rect().w similar
 		// for y. This requires that 'contains' would return true for 'coords', otherwise this will
 		// be an infinite loop.
-		Vector2f move_inside(const Widelands::Coords& coords) const;
+		Vector2f find_pixel_for_coordinates(const Widelands::Coords& coords) const;
 
 	private:
 		friend class MapView;

=== added file 'src/wui/sound_options.cc'
--- src/wui/sound_options.cc	1970-01-01 00:00:00 +0000
+++ src/wui/sound_options.cc	2019-03-23 14:09:21 +0000
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2019 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#include "wui/sound_options.h"
+
+#include "base/i18n.h"
+#include "sound/sound_handler.h"
+#include "ui_basic/checkbox.h"
+#include "ui_basic/multilinetextarea.h"
+#include "ui_basic/slider.h"
+
+namespace {
+
+/**
+ * UI elements to set sound properties for 1 type of sounds.
+ */
+class SoundControl : public UI::Box {
+private:
+	static constexpr int kSliderWidth = 200;
+	static constexpr int kCursorWidth = 28;
+	static constexpr int kSpacing = 16;
+
+public:
+/**
+	 * @brief SoundControl Creates a new sound control box
+	 * @param parent The parent panel
+	 * @param title The localized test label to display
+	 * @param type The type of sound to set the properties for
+	 * @param style The User interface style for the slider
+	 */
+	SoundControl(UI::Box* parent, UI::SliderStyle style, const std::string& title, SoundType type, FxId representative_fx = kNoSoundEffect)
+	   : UI::Box(parent, 0, 0, UI::Box::Horizontal),
+		 enable_(this, Vector2i::zero(), title),
+		 volume_(this, 0, 0, kSliderWidth, enable_.get_h(),
+				 0, g_sh->get_max_volume(), g_sh->get_volume(type), style,
+				 /** TRANSLATORS: Tooltip for volume slider in sound options */
+				 _("Changes the volume. Click to hear a sample."), kCursorWidth),
+		 type_(type),
+	fx_(representative_fx) {
+		set_inner_spacing(kSpacing);
+		add(&volume_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
+		add(&enable_, UI::Box::Resizing::kAlign, UI::Align::kCenter);
+
+		if (SoundHandler::is_backend_disabled()) {
+			enable_.set_enabled(false);
+			volume_.set_enabled(false);
+		} else {
+			enable_.set_state(g_sh->is_sound_enabled(type));
+			volume_.set_enabled(g_sh->is_sound_enabled(type));
+
+			enable_.changedto.connect([this] (bool on) { enable_changed(on); });
+			volume_.changedto.connect([this] (int32_t value) { volume_changed(value); });
+			volume_.clicked.connect([this] { play_sound_sample(); });
+		}
+		set_thinks(false);
+	}
+
+private:
+	/// Plays a system sound sample selected from the given sound type
+	void play_sound_sample() {
+		if (fx_ != kNoSoundEffect) {
+			g_sh->play_fx(type_, fx_);
+		}
+	}
+
+	/// Sets new enable/disable value in the sound handler for the sound type and enables/disables the volume slider accordingly
+	void enable_changed(bool on) {
+		enable_.set_state(on);
+		volume_.set_enabled(on);
+		g_sh->set_enable_sound(type_, on);
+	}
+
+	/// Sets the volume in the sound handler to the new 'value'
+	void volume_changed(int32_t value) {
+		g_sh->set_volume(type_, value);
+	}
+
+	/// Enable / disable sound type
+	UI::Checkbox enable_;
+	/// Control the volume for the sound type
+	UI::HorizontalSlider volume_;
+	/// The sound type to control
+	const SoundType type_;
+	/// Representative sound effect to play
+	const FxId fx_;
+};
+
+constexpr int kSpacing = 12;
+
+} // namespace
+
+SoundOptions::SoundOptions(UI::Panel& parent, UI::SliderStyle style)
+   : UI::Box(&parent, 0, 0, UI::Box::Vertical) {
+
+	set_inner_spacing(kSpacing);
+
+	add(new SoundControl(this, style, pgettext("sound_options", "Music"), SoundType::kMusic));
+
+	add(new SoundControl(this, style, pgettext("sound_options", "Chat Messages"), SoundType::kChat,
+						 g_sh->register_fx(SoundType::kChat, "sound/lobby_chat")));
+
+	add(new SoundControl(this, style, pgettext("sound_options", "Game Messages"), SoundType::kMessage,
+						 g_sh->register_fx(SoundType::kMessage, "sound/message")));
+
+	add(new SoundControl(this, style, pgettext("sound_options", "User Interface"), SoundType::kUI));
+
+	add(new SoundControl(this, style, pgettext("sound_options", "Ambient Sounds"), SoundType::kAmbient,
+						 g_sh->register_fx(SoundType::kAmbient, "sound/create_construction_site")));
+
+	// TODO(GunChleoc): There's a bug (probably somewhere in Box, triggered in combination with Window::set_center_panel) that will hide the bottom SoundControl in GameOptionsSoundMenu if the MultilineTextarea is not added to the Box. So, we create and add it even if its text is empty.
+	UI::MultilineTextarea* sound_warning = new UI::MultilineTextarea(
+	   this, 0, 0, 100, 0, UI::PanelStyle::kWui,
+	   "", UI::Align::kLeft,
+	   UI::MultilineTextarea::ScrollMode::kNoScrolling);
+	add(sound_warning, UI::Box::Resizing::kExpandBoth);
+
+	if (SoundHandler::is_backend_disabled()) {
+		sound_warning->set_text(_("Sound is disabled either due to a problem with the sound driver, or because it was switched off at the command line."));
+	}
+}

=== added file 'src/wui/sound_options.h'
--- src/wui/sound_options.h	1970-01-01 00:00:00 +0000
+++ src/wui/sound_options.h	2019-03-23 14:09:21 +0000
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 by the Widelands Development Team
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef WL_WUI_SOUND_OPTIONS_H
+#define WL_WUI_SOUND_OPTIONS_H
+
+#include "ui_basic/box.h"
+
+/**
+ * A box with all sound options.
+ * All changes to the sound settings take effect immediately, but are not saved to config.
+ */
+struct SoundOptions : public UI::Box {
+	SoundOptions(UI::Panel& parent, UI::SliderStyle style);
+};
+
+#endif  // end of include guard: WL_WUI_SOUND_OPTIONS_H

=== modified file 'src/wui/transport_draw.cc'
--- src/wui/transport_draw.cc	2019-03-23 14:09:20 +0000
+++ src/wui/transport_draw.cc	2019-03-23 14:09:21 +0000
@@ -28,7 +28,7 @@
 
 void Flag::draw(uint32_t gametime,
                 const TextToDraw,
-                const Vector2f& point_on_dst,
+                const Vector2f& field_on_dst, const Coords& coords,
                 float scale,
                 RenderTarget* dst) {
 	static struct {
@@ -37,12 +37,11 @@
 	                     {-6.f, -3.f}, {-1.f, -2.f}, {3.f, -2.f}, {8.f, -3.f}};
 
 	const RGBColor& player_color = owner().get_playercolor();
-	// NOCOM
 	dst->blit_animation(
-	   point_on_dst, scale, owner().tribe().flag_animation(), gametime - animstart_, &player_color);
+	   field_on_dst, coords, scale, owner().tribe().flag_animation(), gametime - animstart_, &player_color);
 
 	for (int32_t i = 0; i < ware_filled_; ++i) {  //  draw wares
-		Vector2f warepos = point_on_dst;
+		Vector2f warepos = field_on_dst;
 		if (i < 8) {
 			warepos.x += ware_offsets[i].x * scale;
 			warepos.y += ware_offsets[i].y * scale;
@@ -50,11 +49,11 @@
 			warepos.y -= (6.f + (i - 8.f) * 3.f) * scale;
 		}
 		dst->blit_animation(
-		   warepos, scale, wares_[i].ware->descr().get_animation("idle"), 0, &player_color);
+		   warepos, Widelands::Coords::null(), scale, wares_[i].ware->descr().get_animation("idle"), 0, &player_color);
 	}
 }
 
 /** The road is drawn by the terrain renderer via marked fields. */
-void Road::draw(uint32_t, const TextToDraw, const Vector2f&, float, RenderTarget*) {
+void Road::draw(uint32_t, const TextToDraw, const Vector2f&, const Coords&, float, RenderTarget*) {
 }
 }  // namespace Widelands


References