Welcome to the Creatures Wiki! Log in and join the community.

Difference between revisions of "MNG files"

From Creatures Wiki
Jump to navigation Jump to search
(Here's the Java equivalent (Warning: Untested code) (I put it above C because VB and Java are OOP, not to be a C-hater ;))
(One intermediate revision by the same user not shown)
Line 2: Line 2:
  
 
==Outline==
 
==Outline==
This information was gained by [[GreenReaper]] during a university project and should not be considered official - no details of the MNG format were ever released by [[Creature Labs]], only a three-page article [http://web.archive.org/web/20020614201723/http://www.creatures.co.uk/library/science/lib_science_musicbehind1.htm The Music of Creatures] describing some features.
+
This information was gained by [[GreenReaper]] during a university project and should not be considered official - no details of the MNG format were ever released by [[Creature Labs]], only a three-page article [https://web.archive.org/web/20020614201723/http://www.creatures.co.uk/library/science/lib_science_musicbehind1.htm The Music of Creatures] describing some features.
  
The music is formed of separate samples in a partial WAV format combined with a script which is interpreted by the music engine. The sample format is a standard Windows WAV format with the first 16 bytes removed - typically in '''16-bit 22kHz mono''' (if you need to convert your WAV files to this, open them up in Sound Recorder and Save As to this format).
+
The music is formed of separate samples in a partial WAV format combined with a script which is interpreted by the music engine. The sample format is a standard Windows WAV format with the first 16 bytes removed - typically in '''16-bit 22kHz mono''' (if you need to convert your WAV files to this, open them up in [[wikipedia:Sound Recorder (Windows)|Sound Recorder]] or [[wikipedia:Audacity (audio editor)|Audacity]] and Save As to this format).
  
 
The file starts off with an index of samples, then the script data chunk, then sample chunks. The ''names'' of samples are stored implicitly - the first name in the script refers to the first sample chunk, the second name is the second, and so on, ignoring names that are already associated with samples.
 
The file starts off with an index of samples, then the script data chunk, then sample chunks. The ''names'' of samples are stored implicitly - the first name in the script refers to the first sample chunk, the second name is the second, and so on, ignoring names that are already associated with samples.
Line 341: Line 341:
  
 
==Script Encryption==
 
==Script Encryption==
The music scripts of MNG files are saved in a 'scrambled' format, encrypted with an XOR function. This function works on a byte level, with a starting operand value of 0x5 and an increment of 0xC1. Sample routines follow - as XOR is a reversible operation, they can be used to both scramble and descramble scripts.
+
The music scripts of MNG files are saved in a 'scrambled' format, encrypted with an [[wikipedia:XOR function|XOR function]]. This function works on a byte level, with a starting operand value of 0x5 and an increment of 0xC1. Sample routines follow - as XOR is a reversible operation, they can be used to both scramble and descramble scripts.
  
 
===Visual Basic Routine===
 
===Visual Basic Routine===

Revision as of 06:39, 12 September 2017

Munge files (extension: MNG) are used to store music for Creatures 2, Creatures 3 and Docking Station. This format should not be confused with the MNG animation format, which was called PNF at the time of C2.

Outline

This information was gained by GreenReaper during a university project and should not be considered official - no details of the MNG format were ever released by Creature Labs, only a three-page article The Music of Creatures describing some features.

The music is formed of separate samples in a partial WAV format combined with a script which is interpreted by the music engine. The sample format is a standard Windows WAV format with the first 16 bytes removed - typically in 16-bit 22kHz mono (if you need to convert your WAV files to this, open them up in Sound Recorder or Audacity and Save As to this format).

The file starts off with an index of samples, then the script data chunk, then sample chunks. The names of samples are stored implicitly - the first name in the script refers to the first sample chunk, the second name is the second, and so on, ignoring names that are already associated with samples.

Although you do not need to understand the above to write a script, you do need to understand that if you save a file all the samples must be used in a "Wave()" somewhere in the script, otherwise there will be no way for programs to know what their name is later.

Script Format

Tracks

The basic unit of music is a Track, which in the game is associated with either a specific event - for example, the death of a creature - or an area, like the Volcano track. Typically only one track plays at once; switching between tracks is accomplished by fading in and out, adhering to the FadeIn and FadeOut track parameters. Each Track has one or more Layers, which are played simultaneously. Comments are indicated by a double slash (//) and may be placed within Track, Effect, Voice or Update declarations, as well as at the top level.

Track(UpperTemple)
    {
    FadeIn(5)
    FadeOut(5)

    LoopLayer(Chord)
        {
        …
        }

    AleotoricLayer(StickMelody)
        {
        …
        }
    }

Variables

MNG scripts have a concept of local variables, which reside within these layers. Variables must be declared before use, with the name and an initial value. The variables are floating point values associated with names. Some variables are special – Pan and Volume because they affect the samples to be played, and Interval because it affects the length of the track.

AleotoricLayer(Pad)
    {
    Variable(temp,4.0)
        …

Layers

Layers are the "instruments" of a track, in that they either play one sample repeatedly (in the case of LoopLayers) or one or more samples, enclosed within Voices (AleotoricLayers).

LoopLayers

A LoopLayer consists of a single Wave and an Update block. The Wave is played constantly and repeatedly. The Update is called at regular intervals and typically causes some change in the presentation of the samples (for example, it may pan the output from side to side, or alter the volume).

    LoopLayer(HighBreath)
        {
        Variable(counter,0.0)
        Variable(temp,0.0)
        Update
            {
            // Gradually, pan around at a random rate
            temp = Random(0.0, 0.1)
            counter = Add(counter, temp)
            Pan = CosineWave(counter, 30)
            // Scale the volume according to mood
            Volume = Multiply(Mood,0.4)
            Volume = Add(Volume,0.6)
            }
        UpdateRate(0.1)
        Wave(HighBreathG)
        }

AleotoricLayers and Voices

An AleotoricLayer consists of one or more Voices to be played sequentially. Effects and Volume may be specified for the layer. The Interval of a layer specifies how long it is before the next Voice of an AleotoricLayer is to be played – it is possible to change this within the Voice.

Voices are individual Waves with optional Conditions and Intervals. Conditions are used to decide whether or not the Wave should be played – the value of the specified variable must be between the two specified values. Intervals allow the script to specify how long to wait before the next sample.

    AleotoricLayer(BendyEcho)
        {
        Volume(0.4)
        Effect(PingPong160)
        Interval(4)
        Voice
            {
            Condition(Mood,0.2,0.6)
            Wave(Bnd0)
            Interval ( Random( 4.0, 9.4) )
            }
        Voice
            {
            Condition(Mood,0.4,1.0)
            Wave(Bnd1)
            Interval ( Random( 4.0, 9.4) )
            }
        }

Update Blocks

Both the LoopLayer and AleotoricLayer structures may have one Update block. This block consists of assignments to variables (which may be special variables such as Volume or Pan) that are carried out each time an update is called, and also when beginning to play a layer. The time period for updates is set by the UpdateRate or BeatSynch statement in LoopLayers, or each time the last Voice is considered for AleotoricLayers. The Update blocks may also be placed within Voice blocks, in which case the update takes effect after the voice’s Wave has been played.

AleotoricLayer(Pad)
    {
    // The track sparsely plays pads ranging from the gentle (drm)
    // for low threat, with harsher (vce) for heigher threats
    // Volume increases with mood and threat, 
    // The interval is decreased with threat
    Volume(0.4)
    Variable(temp,0.0)
    Update 
        {
        // Volume = 0.5 + 0.25 * (Mood + Threat)
        temp = Multiply(Mood, 0.25)
        Volume = Add(0.5,temp) 
        temp = Multiply(Threat, 0.25)
        Volume = Add(Volume,temp) 
        Interval = Random ( 4.0, 6.0 )
        temp = Multiply( Threat, 2.0)
        Interval = Subtract( Interval, temp)
        }
    …

Intervals

Intervals represent a pause in the output of a layer, either between Voices if specified for a particular voice or between iterations of a layer if in the main body. Processing of a layer does not continue until a pause for the length of the interval has taken place. The expression is evaluated anew each time.

Beats, BeatLength and BeatSynch

Beats are an alternative method of specifying intervals between periods of music. Instead of directly specifying a length in seconds, the BeatLength is specified in the Track, and a BeatSynch is given that measures an Interval in this number of beats. The following specifies an interval of 0.3 * 16 = 4.8 seconds for the Guitar:

Track(Underwater)
    {
    BeatLength(0.3)

    AleotoricLayer(Guitar)
        {
        BeatSynch(16.0)
…

Effects

The script also specifies Effects, which are preset sequences of setting changes applied to AleotoricLayers (not LoopLayers). An Effect has one or more Stages, each of which may make changes to the panning or volume for that layer. After a specified delay, the effect moves onto the next Stage in the sequence.

Effects are applied to the output of a Layer; they essentially take this output and repeat it several times . How many times is defined by the number of Stage declarations in the Effect. As an example, a simple effect might "bounce" the sound from one side to the other, slowly fading the volume at the same time.

Each Stage contains a declaration for the Volume to play the output at, the Pan value (how far to the left or right of centre the sound should be played) and either a Delay or TempoDelay, indicating how long to pause before moving on to start the next effect. Values may be expressions or constants. TempoDelays are present in effects intended for layers using the BeatSynch and are measured in beats as defined in the Track currently playing, whilst Delay is measured in seconds.

Effect(RandomPad)
    {
    // Produces randomly panned echoes, staggered at close
    // random times
    Stage
        {
        Pan(Random(-1.0,1.0)) Volume(1) Delay(Random(0.25,0.4))
        }
    Stage
        {
        Pan(Random(-1.0,1.0)) Volume(0.92) Delay(Random(0.25,0.4))
        }
    Stage
        {
        Pan(Random(-1.0,1.0)) Volume(0.84) Delay(Random(0.25,0.4))
        }
    }

If you just want to edit music files, not write music editing programs, you can stop reading now.


File Structure

Remember that Windows and other x86-based platforms use least-significant bit byte ordering for numbers. You may have to convert on other platforms.

 Position

 Length

 Description

 Notes

 0

 4

 Number of samples

  

 4

 4

 Position of script

 Zero-based byte position

 8

 4

 Length of script

 Length in bytes

 12

 4

 Position of first sample

 Zero-based byte position

 16

 4

 Length of first sample

 Length in bytes

 20

 4

 Position of second sample

 Zero-based byte position

 …

 …

 …

  

 N * 8 + 8

 4

 Position of last sample

 N is the number of samples

 N * 8 + 12

 Variable

 First sample

 Followed by the rest of the samples

Script Encryption

The music scripts of MNG files are saved in a 'scrambled' format, encrypted with an XOR function. This function works on a byte level, with a starting operand value of 0x5 and an increment of 0xC1. Sample routines follow - as XOR is a reversible operation, they can be used to both scramble and descramble scripts.

Visual Basic Routine

Private Function Scramble(ByVal data As Byte()) As Byte()
  Dim hb as Byte, count as Integer
  hb = 5
  For count = 0 to data.Length - 1
    data(count) = data(count) Xor hb
    If hb < &H3F Then
      hb = CByte(hb + &HC1)
    Else
      hb = Cbyte(hb + (&HC1 - &H100))
    End If
  Next count
  Return data
End Function

Java Function

public static final int MNG_START = 0x5;
public static final int MNG_STEP = 0xC1;

/* Note: This function differs from the above VB version
 * in that it modifies its first argument,
 * rather than returning a new byte string.
 * 
 * For those unfamiliar with array-offset-length functions, they're useful for circumstances where the data you want to act upon is only a subsection of the data in the actual array; they pop up a lot in the Java standard library
 */
public static void scramble(byte[] data, int offset, int length)
{
    byte key = MNG_START; //used for xor'ing
    
    for (int i = 0; i < length; i++)
    {
        data[offset+i] = (byte)(data[offset+i] ^ key);
        key += MNG_STEP;
    }
}

public static void scramble(byte[] data)
{
    scramble(data, 0, data.length);
}

C Function

#define MNG_START 0x5
#define MNG_STEP  0xC1

/* Note: This function differs from the above VB version
 * in that it modifies its first argument,
 * rather than returning a new byte string. */
void scramble(unsigned char *data, unsigned int length)
{
    unsigned char key = MNG_START;
    
    while (length--) {
        /* xor *data, than increment data by one */
        *(data++) ^= key;
        key       += MNG_STEP;
    }
}

Assembly Routine

.text:00543400 sub_0_543400    proc near               .text:00543400 
.text:00543400 arg_0           = dword ptr  8
.text:00543400 arg_4           = dword ptr  0Ch
.text:00543400 
.text:00543400                 push    ebp
.text:00543401                 mov     ebp, esp
.text:00543403                 push    esi
.text:00543404                 mov     esi, [ebp+arg_4]
.text:00543407                 xor     eax, eax
.text:00543409                 mov     cl, 5
.text:0054340B                 test    esi, esi
.text:0054340D                 jle     short loc_0_543424
.text:0054340F                 mov     edx, [ebp+arg_0]
.text:00543412                 push    ebx
.text:00543413 
.text:00543413 loc_0_543413:   ; CODE XREF: sub_0_543400+21
.text:00543413                 mov     bl, [eax+edx]
.text:00543416                 xor     bl, cl
.text:00543418                 add     cl, 0C1h
.text:0054341B                 mov     [eax+edx], bl
.text:0054341E                 inc     eax
.text:0054341F                 cmp     eax, esi
.text:00543421                 jl      short loc_0_543413
.text:00543423                 pop     ebx
.text:00543424 
.text:00543424 loc_0_543424:   ; CODE XREF: sub_0_543400+D
.text:00543424                 pop     esi
.text:00543425                 pop     ebp
.text:00543426                 retn
.text:00543426 sub_0_543400    endp

Related links