User:Popcorn/Dynamic Laser Music

From Valve Developer Community
Jump to navigation Jump to search
Prop laser relay2.jpg

This article is about the full process of adding synchronized looping music to a map in Portal 2 Portal 2 for laser catchers or laser relays as they are activated by a laser beam.

Not much of this article is specific to Portal 2. It is mainly specific to sound editing, music and the sound system in Source Source. If your game supports Operator Stacks and custom soundscripts, this article may be interesting for you aswell. I learnt a lot about how sound works in Source when I made this, so if you're interested, read along. Some information on this page should probably also be on other pages about sound where it's missing, but I've yet to find the right places.

Overview

The result should be a map with some looping base background music as well as additional looping music emitted from laser catchers, synchronized to the base music, turning on and off.

The process we will undergo is to...

  • create music, export audio files, loop them,
  • add an ambient_generic to the map for each music file playing a custom soundscript entry, and trigger them appropriately,
  • create those custom soundscript entries in maps/<mapname>_level_sounds.txt which will use Operator Stacks,
  • test them in-game.

Composing music

First off, we need music files to work with. If you already have them, you can skip this section.

The programs I have used to create music so far are MuseScore and LMMS as well as other MIDI software for editing, playing and converting MIDIs.

Create a looping section and add instruments or whatsoever that you can turn on and off, especially when exporting. Remember that you want files with the same length in the end.
One problem that may arise if you work with reverb for example, you have trailing sound after the loop that may be cut off when looping back. That needs to be mixed to the beginning of the loop. One solution is to export the sound WITH the trailing reverb, i. e. longer than the actual loop, and to edit the sound later to remedy that, see the next section. Try to get the loop length in milliseconds (or even samples), or find the bpm and the number of measures to be able to calculate it as if it had no trailing reverb: The loop length is measures * beats_per_measure / bpm * 60 s/min seconds.

In case you intend music tracks to be played simultaneously, then you should keep in mind that the Source engine is the system that will finally mix your audio files by simply summing them. This means that you may want the audio signal to not clip at any point, or at least not noticeably. Try to avoid using compression when playing multiple of the individual music tracks in your DAW such that it will sound the same in-game.

You most likely want music to be stereo since it offers a better experience especially for headphone users. It's good to know whether some part of the music will be played either "everywhere" or from a specific location in the 3D world (spatialized). In the latter case, mono music is sufficient, which you can already regard when composing music.

Finally, keep the number of channels per file in mind to avoid surprises later.

See also:  Music Composition

Preparing audio files

At this point, we have music files and must ensure that Source will accept them.

Software I use to edit audio files are Audacity and FFmpeg.

We're assuming that we have music files of the same length that, if played simultaneously and started at the same time, sum up to the "full" music track.

File requirements

For synchronized music, the audio files "should" be...

  • WAV, sample rate 44100 Hz, 16 bit PCM, stereo or mono,
  • exactly the same length,
  • looped,
  • sounding good when looping.

Most of the above audio file properties can be confirmed on Windows by checking the file properties:

  • file type: Wavesound (.wav)
  • bitrate of 1411 kBit/s (stereo) or 705 kBit/s (mono)
  • same file size in bytes among the files

The bitrate equals bit_depth * sample_rate * number_of_channels. For 16 bit PCM, 44100 Hz mono, you get 16 * 44100 * 1 = 705,600 or for stereo 16 * 44100 * 2 = 1,411,200. A sample rate of 48000 Hz or a bitrate of 24 bit, which don't work in Source, would yield different numbers. 22050 Hz works, too, which comes with half the file size but worse quality (~352 kBit/s).

Warning.pngWarning:The music you want to play in Source cannot be indefinitely long. At first I tried playing five stereo tracks, 1:20 min each, simultaneously in-game and I was wondering why the game started to lag hard and not play any sound. Apparently I overwhelmed the audio system's buffers such that it couldn't play anymore (that's 5 * (44100*16*2) Bit/s * 80 s / 8 Bit/Byte = 70.56 MB of cache). Since I wanted two pairs of files to always together anyway, I combined them to one file, leaving me with only three files, and since the fifth one was spatialized in-game, it could be mono, cutting its file size in half. Altogether I halved the data in size, which in the end made my idea work at all.

Editing

Either way, you should take a look at the file in Audacity:

  • Ensure that the file loops properly. Play the track with (Audacity-)loop points enabled (right click on timeline or use/edit keybinds for setting/clearing loop points).
    • Remove unintended preceding or trailing silence in case the loop is slightly too long.
    • Wherever you hear a "crack", there is probably a "jump" in the waveform. Locate it, select a few samples in a region around it and Effects > Repair.
    • If there is a crack when looping from the end to the beginning (and even if not), add a very short fade in and fade out at the beginning and end, only like 10-20 samples long.
  • If the music you have opened will be played spatialized, mono may be sufficient. Duplicate your stereo track, mix the copy down to mono, play and compare the two. If the mono track is fine (or identical), then use that, which saves file size.
  • Ensure you export the file with one of the above file formats. When exported, open and play the file to check the beginning and end.

Wrapping trailing sound to a loop

If you have music files with trailing reverb that should be wrapped to the beginning, you should edit the audio file.

  • Using Audacity: Open the file (better: all audio files at once), select the loop length (that you hopefully found out) from the beginning, split all clips at that time, move the trailing sound of one file to a new track (cut, new track, paste), shift it to the beginning (if it isn't there already) and merge the track with the original track it belongs to; Do that for each file.
  • Using FFmpeg: Create a batch file that calls FFmpeg and adjust the duration %LOOP_LENGTH% appropriately. You must replace "ffmpeg" with the path to ffmpeg.exe if you don't have the %PATH% environment variable set to include the FFmpeg installation folder.
@echo off
rem Drag-drop WAV files on this batch file to create a new WAV from it that wraps the sound at the specified time.
rem Unintended silence at the beginning is bad. The resulting audio length (-t %LOOP_LENGTH%) is the loop length.
rem We split each file at the specified time into two parts named [start] and [end] and mix them back together.
set "LOOP_LENGTH=40.0s"

for %%F in (%*) do (
	ffmpeg -loglevel quiet -i "%%~F" -filter_complex "[0:a]atrim=end=%LOOP_LENGTH%[start];[0:a]atrim=start=%LOOP_LENGTH%[end];[start][end]amix=duration=first:inputs=2:normalize=0:dropout_transition=0" -t %LOOP_LENGTH% "%%~dpnF_wrap.wav"
	echo Created wrapped file: %%~nF_wrap.wav
)
pause

Loop marker

Looping a sound file (in its entirety!) on Windows is as easy as creating a text file with text to paste in, save as .bat and dragging the files on it.
But you can use Wavosaur, too: Open the program, drag a WAV file in, double click the waveform to select all, press L, save with Ctrl+S, close the internal waveform window and then the entire program (works for Counter-Strike: Global Offensive).

Source loops audio files ONLY based on the loop marker in the file. The spawnflag Is NOT looped seen in ambient_generic entities makes it seem like you had a choice, but you don't. The engine needs it to start and stop the audio file properly.

Audio files can also be looped differently. If the loop marker is not at the beginning of the file, then playing the file will start at the very beginning. When the end is reached, Source will skip back to the loop point, continuing to play until the end is reached etc.

Any WAV file with a loop marker does contain the four (!) ASCII characters "cue " (batch file) or "smpl" (Wavosaur loop). If a file doesn't contain any of the two, it has no loop point. Can be tested by opening the audio file with a text editor and searching for those two strings.

Mapping

1024 thermaldiscouragement.jpg

The following is what needs to be done in the map editor in Hammer. In fact, this can already be done even if the exact music is not known yet, which is good in production.

We need to create one ambient_generic for every in-game event that potentially should cause a change in music ("This plays when..."), set any unique soundscript entry name to play (which will be defined in a soundscript later), give it some targetname to be able to fire inputs, and finally fire the PlaySound and StopSound inputs whenever intended. These are the "music hooks" for the musician: All the remaining work can be done in the soundscripts, which doesn't require a recompile of the map!

The base music for example can be started and stopped by a trigger_once.

Ambient generic.png

Name Music.Base
Sound name music.b0
Flags ☑ Start Silent
☐ Is NOT looped

Toolstrigger.gif

  My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnTrigger Music.Base PlaySound 0.00 No

Toolstrigger.gif

  My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnTrigger Music.Base StopSound 0.00 No
// maps/<mapname>_level_sounds.txt:

"music.b0"
{
	wave "my/base/music/file.wav"
	
	// ...
}

The laser music should be triggered using a prop_laser_catcher or prop_laser_relay entity. Let Music.Laser be an arbitrary targetname of the ambient_generic and let music.l0 be an arbitrary soundscript entry name it refers to. Then we need to set things up as follows:

Ambient generic.png

Name Music.Laser
Sound name music.l0
Flags ☑ Start Silent
☐ Is NOT looped

Prop laser catcher.jpg

  My Output Target Entity Target Input Parameter Delay Only Once
Io11.png OnPowered Music.Laser PlaySound 0.00 No
Io11.png OnUnpowered Music.Laser StopSound 0.00 No
// maps/<mapname>_level_sounds.txt:

"music.l0"
{
	wave "my/laser/music/file.wav"
	
	// synchronize to "music.b0"...
}
Icon-Important.pngImportant:
  • The game reads sound properties such as volume and pitch primarily from the soundscript entry that is played and NOT from the ambient_generic entity keyvalues! Those are used only if a raw file is played.
  • If an ambient_generic plays an undefined sound entry, the game plays error.wav (Half-Life 2 "Ooh, fiddlesticks, what now?").
Tip.pngTip:
  • The location of the ambient_generic in the world, i. e. the coordinates where the sound comes from, can be altered later.
  • Unneeded ambient_generics can play the null sound common/null.wav and don't need to be removed from the map.
  • One ambient_generic can play multiple files. No need to create multiple of them just because there are multiple sounds to play.
  • Stopping an ambient_generic is not always necessary. A sound that starts can stop another one. Also, a sound that stops can start another one.
  • Stopping an ambient_generic can initiate a fade out instead of stopping immediately, depending on the soundscript entry.
  • Delaying any sound is possible via soundscript, but not playing it earlier.
Note.pngNote:

The soundscript entry names are also absolutely arbitrary but it seems that Mike Morasky tried to use some naming pattern when creating the music soundscript entries for Portal 2:

  • music.b0, music.b1, ... for base music where at most one of them should play at once,
  • music.x0, music.x1, ... for additional music intended to be played on top of b0 etc.,
  • music.l0, music.l1, ... like x0 etc. but for laser catchers/relays,
  • music.c0, music.c1, ... like x0 etc. but for Faith Plate flights.

Making sounds accessible to the game

In Portal 2, it was necessary for me to pack custom audio files to the map file. Otherwise the game can't play them because it would normally not precache anything beyond the game_sounds_manifest.txt (apart from map-specific soundscapes and such). This was never a problem for me in CS:GO though, apparently because you can use custom sounds only as RAW audio files in ambient_generic entities, which the game seems to precache. Custom soundscripts seem to be a different story, which mapmakers can't use in CS:GO.

To pack sounds to the map when compiling, you can create a compile option for that. To use BSPZIP, create a text file common/<Game Name>/sdk_content/maps/<mapname>.txt" along the VMF listing all files to pack, alternating between internal (/) and external (\) paths:

sound/music/track_01.wav
C:\...\sound\music\track_01.wav

Add a new compile option in expert mode before the default Copy File option: Command: $exedir\bin\bspzip.exe, parameters: -addlist $path\$file.bsp $path\$file.txt $path\$file.bsp

You can also use a third party tool such as VIDE or AutoBSPpackingTool. The latter is great for reading your soundscript file and finding the sound files that appear inside them automatically, so you don't have to create the packfile, however, it also packs the soundscript file itself, such that you can no longer change and test the local soundscript file because the game uses the packed soundscript file first.

Once you managed to have the sound files packed to the BSP and not the soundscript, you can now work on the script whilst testing it in-game.

Soundscript

If you're done with the above, this is the point where we can keep the game open to test the soundscript that we're writing.

Create the text file maps/<mapname>_level_sounds.txt and create an entry for each soundscript entry that you used earlier as the sound name for the ambient_generic entities. The following is an example for background music "music.b0", laser music synchronized to it "music.l0" and an unused sound entry "music.b1". There are more sound operators you can override and use, see Soundscripts/Operator Stacks#Music.

The following example should yield consistently synchronized music.

Icon-Bug.pngBug:There seem to be issues if the newly starting track that should start synchronized has a fade in time set. I have a map with one base music track (b0, stereo), one laser music (l0, mono) and an extension of the base music (b1, stereo), all three part of one piece of music (80 s each): Playing b0 and l0 together seems to be fine, but whenever b1 joins in, it's never entirely synchronized to b0, even if turned off and on again. This must have something to do with the fade in time, because if it's 0, the tracks are perfectly synchronized, both with or without the stream character. So, this is not a problem if there is no fade in intended, for example for laser catcher music, which can start with full volume, but it is a problem if you want to smoothly fade two synchronized tracks into each other.


maps/<mapname>_level_sounds.txt
// base background music, stereo, plays everywhere
"music.b0"
{
	channel    CHAN_STATIC
	soundlevel SNDLVL_NONE // "play everywhere"
	volume     1.00
	wave       "*music/track_01.wav" // note the * prefix
	soundentry_version 2
	operator_stacks
	{
		update_stack
		{
			import_stack "update_music_stereo"
			
			volume_fade_in
			{
				input_max 16.0 // fade in time in seconds
			}
		}
	}
}

// laser catcher music, mono, plays from a specific place
"music.l0"
{
	channel    CHAN_STATIC
	soundlevel SNDLVL_80DB // don't "play everywhere"
	volume     1.00
	wave       "*music/track_02.wav" // note the * prefix
	soundentry_version 2
	operator_stacks
	{		
		start_stack
		{
			import_stack "start_sync_to_entry"
			elapsed_time
			{
				entry "music.b0" // essential!
			}
			duration_div
			{
				input2 1 // length. use 1 for same length
			}
		}
		
		update_stack
		{
			import_stack "update_music_spatial"
			dsp_output
			{
				input_float 0.2 // dsp volume, 0-1
			}
			speakers_spatialize
			{
				input_radius 100 // radius outside which the sound becomes directional
			}

			// don't use volume_fade_in or keep fade-in time lower
			// equal 1.0 because it destroys the synchronization
		}
	}
}

// a sound entry that was added to the map, but which ended up not being
// needed. however, we need to define the entry and play something silent,
// otherwise if an ambient_generic is told to play a sound entry that
// doesn't exist, the game plays some audible default error sound instead
"music.b1"
{
	channel    CHAN_STATIC
	soundlevel SNDLVL_NONE
	volume     1.00
	wave       "common/null.wav"
}

Testing

If everything went well, the music should play as intended.

Console command Comment
cl_precacheinfo
sv_precacheinfo
Check whether the game has loaded your custom sound files.
sv_soundemitter_flush Whenever you change something in the soundscript, reload the soundemitter.
playgamesound Play your soundscript entries without having to play the map. Note: Any sounds will "play everywhere" this way!
stopsound Stop all sound, especially looping sounds started from the console that can't be stopped otherwise.


Thanks for reading, I hope you found something useful.