Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sonic Arranger research #2

Open
Pyrdacor opened this issue May 26, 2021 · 34 comments
Open

Sonic Arranger research #2

Pyrdacor opened this issue May 26, 2021 · 34 comments

Comments

@Pyrdacor
Copy link
Owner

Some findings while reverse engineering (based on AM2_CPU english 1.07):

  • 0x0024a0a8 seems to play the music (process one sample?)
  • 0x0024a6f6 applies instrument effects
  • Delays are in ticks (whatever a tick is) and are increment each tick by 1. If an active delay value reaches 0 the associated action (like an effect) is applied and the delay is set back to its value.
  • The order of sound manipulation is AMF -> Instrument effect -> ADSR
  • AMF seems to just decrease (or increase) the amplitude of the wave data based on a wave (needs more research). Maybe the difference to ADSR is the timing (AMF maybe is added byte-wise as a kind of overlay while ADSR is applied globally for a whole pattern or note duration -> only an assumption).
  • Low pass filter 1 seems to average the wave amplitudes over time. With each tick the amplitude of a sample is adjusted by 2 in the direction of the previous sample.
  • The wave negator effect really just inverts the wave (negates the sample value).
  • At A4 there seem to be some kind of control structure which stores playback information. Some finding:
    • 0x29: Current AMF wave data index
    • 0xac: Current instrument effect delay (counter, decreased by 1 each tick down to 0, then the effect is applied)
@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 26, 2021

ADSR

  • The ADSR wave byte is multiplied by "instrument volume / 64".

  • Then this is used as a volume factor itself for wave as "adsr volume / 64".

  • So in short form: out = in * volume * adsrIn / 4096

  • If no repeat and the index reached length and the ADSR volume is/was 0, the lowest bit of byte A4+0xb5 is set.

A4 struct

  • 0x24: This is added to 0x8e and the result is truncated to the range 0-64 and reinserted into 0x8e.
  • 0x28: Sustain counter? Is set to SustainVal if it is not 0 and decreased by 1 down to 0
    if the ADSR data index is >= SustainPt and some flags are set. SustainVal = 0 seems
    to avoid some code, maybe the release part).
  • 0x29: AMF data index
  • 0x8e: Current sample data byte?
  • 0x9e: ADSR data index
  • 0xa2: ADSR delay counter (dec by 1 for ADSR with no active sustain). If 0 it increases
    0x9e by 1. It also takes care of index > length+repeat etc. For non-repeat it
    sets the index to the last wave byte otherwise to wave[length].
  • 0xac: current effect delay (dec by 1 each tick, reset when 0)
  • 0xb2: Is reduced by 4 down to 0 every tick without ADSR (maybe volume fade out?).
    With ADSR it is set to quadruple of the current output amplitude.
    It is also set by note effect 0xC (Set volume) to ('amount' * 'instrVolume' / 64) * 4.
    So it should be the current volume but multiplied by 4 (0-255).
  • 0xb5: Lowest bit is set when a non-repeated ADSR wave has reached its end

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 27, 2021

Full ADSR logic

Basically 6 values from the instrument data are used:

  • ADSRWave: Index of the ADSR wave (0-based)
  • ADSRDelay
  • ADSRLength
  • ADSRRepeat
  • SustainPt
  • SustainVal

The current data index into the ADSR wave is stored in A4+0x9e.

First scenario is when no ADSR is present at all. This is true when ADSRLength and ADSRRepeat are both 0. In this case the current sample wave data is just multiplied by the current instrument volume.

If ADSR is present it can be in sustain mode or outside sustain mode. The sustain mode is active if the ADSR data index is >= SustainPt.

In sustain mode the ADSR data index is not increased for SustainVal - 1 ticks. So if SustainVal = 1 (default) this means that there is no real sustain. SustainVal is a counter that is decreased by 1 every tick and if it reaches 0 the ADSR data index is increased. Note that when this counter reaches 0 it is immediately set back to its value. So if SustainVal = 4 you will keep every part >= SustainPt for 3 cycles!

Regardless of sustain mode the ADSR data index is only increased after ADSRDelay - 1 ticks. The delay (like all delays in SA) is also a counter that is decreased by 1 every tick. So a delay of 1 (default) means no real delay.

Note that the delay counter is dependent on the sustain counter. So if you have SustainVal = 2 and Delay = 2 this means that the data index is only increased every 4 cycles as first the sustain counter has to become 0 before the delay counter is decreased.

There is a special sustain mode where SustainVal = 0. In this case the sustain part is kept forever and the data index is not increased anymore when reaching SustainPt.

In ASDR mode the current sample data is always multiplied by the current ADSR wave data byte modified by the current instrument volume. The actual formula is out = (in * ((adsrWave[index] * instrVolume) / 64)) / 64. ADSRWaves and volume levels in general are in the range 0-64.

If the ADSR data index reaches Length+Repeat it is set to:

  • Length if Repeat is not 0
  • Length-1 if Repeat is 0

So if the ADSR wave has no repeat part, the last part of the wave is used. In that case if the last part has a value of 0, a flag is set in A4+0xb5 which seems to control the volume fadeout. This fadeout is somehow also used when no ADSR is present.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 27, 2021

At 0x26537e there are 4 data arrays (103 words each) which stores the data of the 4 tracks / channels. When I talk about A4 this data structure is meant where A4 points to the current track's data.

So A4+8e means word 71 (0-based) of the current track's data.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 27, 2021

A tick seems to be one interrupt. The default SA setting is 50 irqs per second so one tick should be 20ms. It can be changed though.

Ticks are used for delays and all kind of counters and one tick represents one ADSR wave byte (with delay 1 and no sustain).

For example a 128 byte ADSR wave with delay 1 (no delay) and no sustain (sustainVal is 1) will need 128*20ms to complete which is 2.56 seconds. In SA this seems to be about right.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 28, 2021

AMF, ADSR and instrument effects only affect volume and period of the audio channel (Paula). This state seems to be affected on every interrupt and also when playing notes, etc.

AMF increases pitch by reducing the period value while ADSR changes the volume.

At 0x2651a0 there are the note period values as words. The first is 0 and should not be used. Notes in SA store a 1-based index (0 = no note) so use an 1-based index to get the period value. The lowest pitch note comes first so it is like:

Octave0: C C# D D# E F F# G G# A A# B
...

The last entry is 0xffff and is an end marker I guess. The values range from 0x3580 (13696) to 0x1C (28).

        ushort[] NotePeriodTable = new ushort[110]
        {
            // Index 0
            0x0000,
            // Octave 0
            0x3580, 0x3280, 0x2fa0, 0x2d00, 0x2a60, 0x2800, 0x25c0, 0x23a0, 0x21a0, 0x1fc0, 0x1e00, 0x1c50,
            // Octave 1
            0x1ac0, 0x1940, 0x17d0, 0x1680, 0x1530, 0x1400, 0x12e0, 0x11d0, 0x10d0, 0x0fe0, 0x0f00, 0x0e28,
            // Octave 2
            0x0d60, 0x0ca0, 0x0be8, 0x0b40, 0x0a98, 0x0a00, 0x0970, 0x08e8, 0x0868, 0x07f0, 0x0780, 0x0714,
            // Octave 3
            0x06b0, 0x0650, 0x05f4, 0x05a0, 0x054c, 0x0500, 0x04b8, 0x0474, 0x0434, 0x03f8, 0x03c0, 0x038a,
            // Octave 4
            0x0358, 0x0328, 0x02fa, 0x02d0, 0x02a6, 0x0280, 0x025c, 0x023a, 0x021a, 0x01fc, 0x01e0, 0x01c5,
            // Octave 5
            0x01ac, 0x0194, 0x017d, 0x0168, 0x0153, 0x0140, 0x012e, 0x011d, 0x010d, 0x00fe, 0x00f0, 0x00e2,
            // Octave 6
            0x00d6, 0x00ca, 0x00be, 0x00b4, 0x00aa, 0x00a0, 0x0097, 0x008f, 0x0087, 0x007f, 0x0078, 0x0071,
            // Octave 7
            0x006b, 0x0065, 0x005f, 0x005a, 0x0055, 0x0050, 0x004b, 0x0047, 0x0043, 0x003f, 0x003c, 0x0038,
            // Octave 8
            0x0035, 0x0032, 0x002f, 0x002d, 0x002a, 0x0028, 0x0025, 0x0023, 0x0021, 0x001f, 0x001e, 0x001c,
            // End marker
            0xffff,
        };

There is also a table for the vibrato effect which adjust the period value dependent on some settings that are basically an index into the table. It is located at 0x26527c and contains 256 bytes (interpreted as signed 8-bit values).

        byte[] VibratoTable = new byte[256]
        {
            0x00, 0x03, 0x06, 0x09, 0x0c, 0x10, 0x13, 0x16,
            0x19, 0x1c, 0x1f, 0x22, 0x25, 0x28, 0x2b, 0x2e,
            0x31, 0x34, 0x36, 0x39, 0x3c, 0x3f, 0x42, 0x44,
            0x47, 0x49, 0x4c, 0x4e, 0x51, 0x53, 0x56, 0x58,
            0x5a, 0x5c, 0x5e, 0x60, 0x62, 0x64, 0x66, 0x68,
            0x6a, 0x6c, 0x6d, 0x6f, 0x70, 0x72, 0x73, 0x74,
            0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7b, 0x7c,
            0x7d, 0x7d, 0x7e, 0x7e, 0x7e, 0x7f, 0x7f, 0x7f,
            0x7f, 0x7f, 0x7f, 0x7f, 0x7e, 0x7e, 0x7d, 0x7d,
            0x7c, 0x7c, 0x7b, 0x7a, 0x79, 0x78, 0x77, 0x76,
            0x75, 0x74, 0x72, 0x71, 0x70, 0x6e, 0x6c, 0x6b,
            0x69, 0x67, 0x65, 0x63, 0x61, 0x5f, 0x5d, 0x5b,
            0x59, 0x57, 0x54, 0x52, 0x50, 0x4d, 0x4b, 0x48,
            0x45, 0x43, 0x40, 0x3d, 0x3b, 0x38, 0x35, 0x32,
            0x2f, 0x2c, 0x29, 0x27, 0x24, 0x20, 0x1d, 0x1a,
            0x17, 0x14, 0x11, 0x0e, 0x0b, 0x08, 0x05, 0x02,
            0xff, 0xfc, 0xf9, 0xf6, 0xf2, 0xef, 0xec, 0xe9,
            0xe6, 0xe3, 0xe0, 0xdd, 0xda, 0xd7, 0xd4, 0xd1,
            0xce, 0xcb, 0xc9, 0xc6, 0xc3, 0xc0, 0xbe, 0xbb,
            0xb8, 0xb6, 0xb3, 0xb1, 0xae, 0xac, 0xaa, 0xa8,
            0xa5, 0xa3, 0xa1, 0x9f, 0x9d, 0x9b, 0x99, 0x98,
            0x96, 0x94, 0x93, 0x91, 0x90, 0x8e, 0x8d, 0x8c,
            0x8a, 0x89, 0x88, 0x87, 0x86, 0x86, 0x85, 0x84,
            0x84, 0x83, 0x83, 0x82, 0x82, 0x82, 0x82, 0x82,
            0x82, 0x82, 0x82, 0x82, 0x82, 0x83, 0x83, 0x84,
            0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b,
            0x8c, 0x8d, 0x8f, 0x90, 0x92, 0x93, 0x95, 0x97,
            0x98, 0x9a, 0x9c, 0x9e, 0xa0, 0xa2, 0xa4, 0xa6,
            0xa9, 0xab, 0xad, 0xb0, 0xb2, 0xb5, 0xb7, 0xba,
            0xbc, 0xbf, 0xc2, 0xc4, 0xc7, 0xca, 0xcd, 0xd0,
            0xd3, 0xd6, 0xd9, 0xdb, 0xde, 0xe2, 0xe5, 0xe8,
            0xeb, 0xee, 0xf1, 0xf4, 0xf7, 0xfa, 0xfd, 0x00
        };

@Pyrdacor
Copy link
Owner Author

Maybe a bit more background info. The volume and period values are written to registers 0xDFF0A8 and 0xDFF0A6 (audio channel 0) etc. It is documented here: http://amiga-dev.wikidot.com/information:hardware

So the period values are in clocks and 124 (0x7c) is the minimum value to set. The SA replayer limits the period value to 0x71 though. I don't know if this is a bug in the replayer or maybe some kind of hack.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 28, 2021

One clock (bus cycle) is exactly 279.365 nanoseconds (billionths of second) or 2.79365 10"7 seconds.

The hardware LPF lets frequency up to 4kHz pass through, diminishes volume amplitude for 4 to 7kHz and cuts off everything from 7kHz and above.

In register DMACON (0xDFF096) the 4 lowest bits enable audio DMA for channel 0 to 3. Lowest bit for channel 0 etc.

When DMA is active it copies data from the given data address to the output (Paula). It stores the data address and length in internal registers so changing the public ones while DMA copy is active won't have an effect.

Disabling and re-enabling DMA will start copying from the new address. If the DMA copied all data (given length in words) it will start again at the given address and with given length (both could be changed in the meantime).

Sound/music can easily be turned off by setting the audio DMA enable bits to 0.

Bit 9 in DMACON enables/disables all the DMAs so it should be set when some DMA should be used and will also turn off all sound if it is cleared.

@kermitfrog
Copy link

Ok.. so moving it here...

[..] The most urgent thing to find out is how often the methods are called. There has to be some timing involved like a sample rate. There are delays which seem to be some kind of counters. [..]

I'm not quite sure if I'm looking for the right thing, but .. let's try..
I looked at the Interrupt that leads to the call playing the music.
The first music related function to be called is FUN_00249df0. Prior to calling it, a global variable is checked, which I believe to the Music-On/Off option.

There DAT_002658e8 is checked (as long). If it is != 0, FUN_00249d4e is called up to 4 times (depending on states of the individual bytes) and finally DAT_002658e8 is set to 0.
What can only be seen in the disassembler, is that DAT_002658e8 gets parameters into A3, and A4. Those put into A3 match the addresses of the 4 Audio channel locations from the Amiga hardware information link you posted.

DAT_002658e8 is also used in FUN_00249d28, FUN_00249df0 and FUN_00249e92. Maybe the places it is written to are related to what you are looking for?

After those 0-4 calls, DAT_002658f4 is incremented and if checked against DAT_002658da.
If DAT_002658f4 >= DAT_002658da, it is set to 0. If DAT_002658e6 is != 0, FUN_00249e92 is called, which seems to handle most of the music stuff in it's subcalls.

Assuming the other interesting calls handle actual music play and FUN_00249e92 is for data preparation, then DAT_002658da may contain for how long the same frequency is played? Or is this maybe the exact memory variable you are looking for? (Not sure how this is handled by the hardware)

After this FUN_0024a3c4 is called, which in turn calls FUN_0024a41e for each audio channel (just as before, putting audio channel pointers into A3 before each call and other data into A4, and this time D7, too)

I hope this helps somehow

@Pyrdacor
Copy link
Owner Author

Thanks I will look at it. There are 4 distinct audio channels which should explain the 0-4 iterations.

The length field in the hardware audio registers determines how long something is played. The period field sets the play frequency. For waves the whole wave is played at once and is looped. The hardware automatically loops the data if you don't disable the sound or change the data and length fields.

However there is the concept of patterns and note lengths. For example you can set the length of one pattern to be a 1/16th note or a quarter note. So independent of the hardware registers the length of one pattern has to be controlled.

To stop a sound from playing or change the sound data the LC registers has to be changed or the length register or the DMACON enable bit has to be set to 0.

I also found something where the LC register is set to some data location which is only 4 or so bytes long and the length is set to 1 (= 1 word). I guess this data field is just always 0 and serves as a dummy buffer to play nothing without disabling the sound via enable bit.

The control when a note is started or stopped is still interesting and in the end it has to change one of the mentioned stuff.

@Pyrdacor
Copy link
Owner Author

A short info about the functionality I assume:

There is a timer interrupt of about 20ms which calls a method that handles all kind of effects and modifies the period and volume hardware registers.

There is also a structure (A4) which holds playback data like current indices into tables, counters, etc.

The sound playback itself is handled by the hardware. Only the period, volume, length and data address have to be set.

I guess this is done when a note should start playing or is stopped. Then only the mentioned effect interrupt changes pitch and volume.

I found some code that might start a note. There is another large switch block that also runs a version of the effect method and does some more stuff.

The question for me is how the note is stopped and by which criteria.

Starting notes, adding effects etc shouldn't be that hard anymore. And as I now emulate Paula's registers the playback control should be easy as well.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 31, 2021

FUN_00249d4e is interesting. If I read it correctly it uses A4+0x8a as A5. I think A4+0x8a stores a pointer to some "currently played note data". A5 itself seems to have a pointer to the actual playback data at offsets 4 and 6. At 6 the lower word (for AUDxLCL) is located. If it is 0, the audio hardware address is set to the address of 0x2656b6. I mentioned this earlier. This seem to be just a few 0-bytes so the hardware just loops no sound. So everytime AUDxLC is set to &0x2656b6, this is kind of a "stop playing" or "mute" operation.

A5+4 seems not to hold the high word (AUDxLCH). Instead A4+0xbc is used there but is increased by (A5+4)*2. So I assume A4+0xbc holds a base address pointer for the high word. But I could not make sense out of A5+4 yet. Maybe you could check with debugger/dump what values are in A5 and A4+0xbc while FUN_00249d4e is running.

So all in all I think FUN_00249d4e is used to update the audio address which should be equal to an instrument change on the track or just stop playing a note. Pitch changes (different notes) should not affect the data address though.

FUN_0024a0a8 seems interesting as well. I think it is used to start playing some note. There is a big switch block but my ghidra shows this in a strange way. The cases seem to be some kind of command. Actually SA uses commands with a parameter on each note. I guess this could be it.

I will look further.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 31, 2021

Ok I misinterpreted FUN_00249d4e:

A4+0xbc is the base address and A5+4 is the actual offset in words. So basically A5+4 is the sample index divided by 2. Samples can only be transferred as pairs.

A4+6 instead is the length of the data to play.

So I guess finding uses of A4+0x8a should show us the places where the playback data is changed.

@Pyrdacor
Copy link
Owner Author

As DAT_002658e8 is used in an interrupt I think this stores for every audio track if a data address update is required. Because if a bit is set only the hardware audo data address is updated dependent on A4 and A5 (A4+0x8a).

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 31, 2021

Ok now I really understand it. DAT_002658e8 stores a bit for each channel which is "playback finished". So the hardware processed all the data and now triggers the interrupt.

A4+0x8a actually is the pointer to the Instrument structure so A5 is the instrument data.

A5+6 is the "repeat size" in words. So if this is 0 there is no repeat and if playback has finished, nothing is done. This means without a given repeat portion the whole data is looped/repeated. If nothing is done, the hardware will just keep going and start playing the current data from the beginning.

If repeat is 1 or the data pointer is invalid the data pointer is set to the "empty dummy data address" so that no sound is played. So repeat=1 seems to be used to disable looping.

If repeat is > 1 the data pointer is set to base pointer + length * 2 which makes sense as the repeat portion follows the "normal portion" which ends at length. So FUN_00249d4e handles partial sound repeat portions and the special "no loop" option.

So FUN_00249df0 is called by the audio hardware when at least one channel has processed all its data. But I guess it is called by the timer which runs at 20ms by default because ...

In addition to the above it will also update effect values like ADSR, instrument effects and AMF.

And it seems to also trigger new notes or advance offsets and decrease counters when the time is right. Have to dig deeper into this.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 31, 2021

FUN_0024a0a8 indeed processes the note commands.

  • 0x2: Sets the current ADSR data index
  • 0x4: Sets the vibrato values (VibSpeed = higher nibble * 2, VibLevel = 160 - lower nibble * 16)
  • 0x6: Set instrument volume to 0-63 (this will most likely be kept for further notes)
  • 0x7: Set note period value
  • 0x8: Set note period value to 0 (mute note)
  • 0xC: Set playback volume
  • 0xE: Enable/disable the Amiga LED (this controls the LPF so this enables/disables the hardward low-pass filter)
  • 0xF: Set song speed

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 31, 2021

Finally the waves sound similar to the original. But I have still some issues with note durations/timing. Will try to fix this. I also haven't implemented all note commands and no instrument effects yet but I think I'm getting somewhere.

The Paula audio registers are kind of clever. I can even play the Sonic Arranger music with higher sampling rates.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented May 31, 2021

Most important stuff to decode is:

  • All instrument effects (switch block)
  • All note commands (other switch block)
  • Fade out behavior (note id = 0 after a note and/or ADSR release phase)
  • Note and sound transpose (stored in Voice structure)
  • Tremolo and arpeggio

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 1, 2021

Yay the Ambermoon songs now all sound pretty good. Now finetuning and effects need to be added. :)

@Metibor
Copy link

Metibor commented Jun 1, 2021

This is good to hear! *badum tish*

...although I was able to listen to the songs months ago. 😜

But seriously: It is great to see what you have accomplished so far and there is even a small chance to have a fully playable version available to celebrate the 1st birthday ddc12c0ef7824fb7e7204f94560fc194cb253146 of the remake! 🎂

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 1, 2021

This is good to hear! badum tish

...although I was able to listen to the songs months ago.

But seriously: It is great to see what you have accomplished so far and there is even a small chance to have a fully playable version available to celebrate the 1st birthday ddc12c0 of the remake!

Nice to hear from you. * badum tish *
Did you also listen to the original SonicArranger tracks? 😝

Didn't know it is almost time for birthday. Hope I will finish everything till then. Implementing all instrument effects at the moment via disassembling. Half way there. 😀

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 1, 2021

Small update on the A4 structure:

A4 struct

  • 0x00: Stores the current wave data (up to 128 bytes).
  • 0x80: Note to play?
  • 0x82: Seems like another note or index into the period table.
    The period is set to A4+0x9c if A4+0x9a is not 0 and A4+0x9c is 0.
    Then the difference with the normal note is calculated and from there a new note period.
    I think this is note transpose.
    Hm it is set to A4+0x80 and then A4+0x80 gets a new value. If then A4+0x82 is 0
    it is also set to old A4+0x80. So maybe this is the last note value and equal to
    the current if last note was 0.
  • 0x84: The period value is decreased by it. Maybe some effect value? Pitch slider?
    Sometimes the instrument finetuning value is set to this. I guess finetuning is added
    to the note's period.
  • 0x86: It is cleared when a note is hit. It seems note effect 0x1 sets this to param.
  • 0x8a: If 0 the output is set to 0 and no further processing is done. Only A4+0xb2 is decreased by 4 each tick. This is a pointer to the instrument data.
  • 0x8e: Current sample volume?
  • 0x90: This is added to 0x8e and the result is truncated to the range 0-64 and reinserted into 0x8e.
    This is some volume per tick adjustment. It is cleared when a note is hit I think.
    Most likely a volume fader/slider.
  • 0x92: Index into the vibrato table (0x26527c). The note period value is modified if the VibLevel
    is not 0: period = period + vibTable[A4+0x92] * 4 / VibLevel. The vibTable entries are
    interpreted as signed shorts.
  • 0x94: Seems to be the current vibrato delay counter. Is decreased by 1, if 0 vibrato effect
    is processed. Is set to the instrument's vibrato delay when a note is hit. If -1 (255) no vibrato is active.
  • 0x96: Should be the vibrato speed
  • 0x98: Should be the vibrato level
  • 0x9a: Should be the current note's period value
  • 0x9c: Should be the current transposed note's period value or last note period value
  • 0x9e: ADSR data index (it seems note effect 0x2 sets this to param).
  • 0xa0: Sustain counter. Is set to SustainVal if it is not 0 and decreased by 1 down to 0
    if the ADSR data index is >= SustainPt and some flags are set. SustainVal = 0 seems
    to avoid some code, maybe the release part).
  • 0xa2: ADSR delay counter (dec by 1 for ADSR with no active sustain). If 0 it increases
    0x9e by 1. It also takes care of index > length+repeat etc. For non-repeat it
    sets the index to the last wave byte otherwise to wave[length].
  • 0xa4: AMF data index
  • 0xa6: AMF delay counter
  • 0xa8: Current wave data byte?
  • 0xaa: Current effect runs (used by some effects like FMDrum)
  • 0xac: current effect delay (dec by 1 each tick, reset when 0)
  • 0xb2: Is reduced by 4 down to 0 every tick without ADSR (maybe volume fade out?) or if instrument is 0.
    With ADSR it is set to quadruple of the current output amplitude.
    It is also set by note effect 0xC (Set volume) to ('amount' * 'instrVolume' / 64) * 4.
    So it should be the current volume but multiplied by 4 (0-256).
  • 0xb4: Lowest bit is set when a non-repeated ADSR wave has reached its end. Then the output
    seems to be set to 0 in following cycles. I guess 0xb4 is some 16 bit flag word or just stores this one bit as a word.
  • 0xb8: Length of synth wave data in words
  • 0xbc: It is a base address for the audio data. Basically the higher 5-bit portion of the
    address that is used in AUDxLCH. It is increased by some value*2 which should result
    in an increase 0x10000 per each of the value.

@a1exh
Copy link

a1exh commented Jun 1, 2021

Congrats. Lots of hard work here by all involved. Looking forward to listening when it is released.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 1, 2021

Thanks Alex. Yeah it's really time consuming and hard brain work to understand all this bits and bytes.

@Metibor If you have some time at hand there are some bugs regarding saving and some other open issues here on GitHub. I am very involved in the music stuff at the moment so maybe you can fix a bug or two. If you're busy, no problem. ;)

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 2, 2021

Ok had a look on the note play logic.

First of all the Note.Command has two parts. The known note effect (0x0 - 0xf) in the lower nibble. But there are option bits in the higher nibble. Bits 2 and 3 control if the transposes are used.

int flags = note.Command >> 4;
if (flags & 4 == 0)
    note.Value = note.Value + voice.NoteTranspose;
if (note.Instrument != 0 && flags & 8 == 0)
    note.Instrument = note.Instrument + voice.SoundTranspose;

So this also reveals how the transpose is used. :) I guess the transpose values are 8-bit signed values as I saw many values aroud 250 which I guess is a low negative number then.

There are some special note values:

  • 0x80 (or -0x80 or -128): If this is set the last note is just continued without changing anything.
  • 0x7f (or 127): If this is set the whole audio output of the channel is muted.
  • 0x00 (or 0): The behavior depends on if the instrument index is also 0. If instrument is also 0 this behaves similar to 0x80. So nothing is touched and last note continues. If instrument is not 0, all values are initialized (counters, indices, nearly the whole A4 struct).
  • For all other values the A4 struct is initalized.

When initializing, if the instrument index is 0 and the currently (last) played instrument is valid, only the effect indices and counters (including ADSR etc) are reset. If the last instrument is not valid (index 0) the whole A4 struct is initialized.

The DMACON register to disable whole hardware audio channels seems to be used for sampled audio data only.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 2, 2021

Decoded and implemented arpeggio, portamento and all instruments effects but Oscillator1 and Metawdrpk.

I also implemented more note commands.

The songs sound better and better. Some are nearly perfect. But some have strange parts. Either something is still missing or I made a mistake somewhere which is likely by the amount of stuff there.

Could be hart to find those small mistakes but I am optimistic.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 3, 2021

@kermitfrog Can you have a look at 0x24a4ce. My ghidra can't dissamble those 2 bytes and messes up the whole decompilation view. Is this also true on your end?

@kermitfrog
Copy link

All this looks like a lot of progress :)
Unfortunately, I won't get much done this week. Is the stopping issue resolved now? If not, I can probably look into it starting tuesday.

[..] Can you have a look at 0x24a4ce [..]

Works fine on my end. It may be the result of starting the disassembly at the wrong byte. I'll just upload the whole function.

involved_in_music_6' is FUN_0024a6f6`

FUN_0024a41e.zip

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 3, 2021

Thanks. Yeah the stopping issue is resolved.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 3, 2021

The songs sound good enough to release the first version of the sonic arranger package. I will do so today or in the next days. Now I will focus on implementing the player inside of Ambermoon. At the moment I only generate WAV files from the data but now I will focus to play it streamed in Ambermoon and trigger it at the right spots etc.

I will also provide a small converter to produce WAV files from SA files.

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 3, 2021

You can now download the first prototype of the converter here: https://github.com/Pyrdacor/SonicArranger

It's an open source project. The converter is a command line tool which should run on Windows and Linux. It can convert SA files to WAV files. Still WIP. The Ambermoon songs sound good (not checked all).

@Pyrdacor
Copy link
Owner Author

Pyrdacor commented Jun 7, 2021

At 0x26537e there are 4 data arrays (103 words each) which stores the data of the 4 tracks / channels. When I talk about A4 this data structure is meant where A4 points to the current track's data.

So A4+8e means word 71 (0-based) of the current track's data.

@kermitfrog At offset 0xb2 in this A4 structs there is a 16-bit value which is set to 4 times the volume in many cases and seems to be reduced by 4 every tick at some points. I am not sure where this is really used and as some tones still sound a bit off, this might be the reason. Currently I am not using this value. Maybe you can find the spot where it is used in ghidra.

@kermitfrog
Copy link

  • (1) The only use I could find directly in ghidra, is at 0024a06e, where the lower bytes (0xb3) is copied to the lower bytes of a word array at 00249a44.

So I used a memory watchdog to see where else it is accessed.

  • (2) 0024a604 and 2 instructions later, decreased by 4 if != 0 or the value can be set at 0024a646, depending on values from DAT_00249a5c and A4+0x8e.
    Which path is taken, is determined at 0024a5f0 and seems to alternate between the 2 - so, at least for my test cases, 0024a604 is used for array 0 and 2, and 0024a646 for 1 and 3.

  • The watchdog sometimes reports reads at every CPU step after this, where the instructions clearly don't read the value. I think this is because of a bug, but it's possible the value is accessed by the hardware (it is in chip mem, so that's a possibility). I switched to a write-only watchdog after this.

  • (3) after the value was decreased by 4 a few times, but before reaching 0, it was overwritten at 0024a392.

  • (1) is in FUN_00249e92, (2) in FUN_0024a41e and (3) is in FUN_0024a350.

  • as for the frequency:

    • (2) is called 28 (or 7 per array) times between (1). That number held true in my tests, but it could be different at times.
    • (3) is called via FUN_0024a1bc in FUN_00249e92, right before (1) if some conditions are true (checked per channel).

The values copied in (1) don't seem to be read anywhere.

I hope this helps.

@Pyrdacor
Copy link
Owner Author

Will come back to it. Have to fix some other things first. But maybe this is already helping. :)

@Pyrdacor Pyrdacor transferred this issue from Pyrdacor/Ambermoon.net Jun 26, 2021
@Pyrdacor Pyrdacor transferred this issue from Pyrdacor/Ambermoon Jun 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants