-
Notifications
You must be signed in to change notification settings - Fork 1
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
Comments
ADSR
A4 struct
|
Full ADSR logicBasically 6 values from the instrument data are used:
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 If the ADSR data index reaches Length+Repeat it is set to:
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. |
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. |
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. |
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:
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
}; |
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. |
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. |
Ok.. so moving it here...
I'm not quite sure if I'm looking for the right thing, but .. let's try.. 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. 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. 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 |
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. |
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. |
Found this PDF which is really good in explaining audio programming: https://www.google.com/url?sa=t&source=web&rct=j&url=https://www.ikod.se/wp-content/uploads/2018/10/Amiga_System_Programmers_Guide_1988_Abacus.pdf&ved=2ahUKEwiptc29i-_wAhWegf0HHf0vBnkQFjAAegQIAxAC&usg=AOvVaw1uZp_xRrZlu3GI0yg6Esj5 |
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. |
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. |
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). |
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. |
FUN_0024a0a8 indeed processes the note commands.
|
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. |
Most important stuff to decode is:
|
Yay the Ambermoon songs now all sound pretty good. Now finetuning and effects need to be added. :) |
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! 🎂 |
Nice to hear from you. * badum tish * 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. 😀 |
Small update on the A4 structure: A4 struct
|
Congrats. Lots of hard work here by all involved. Looking forward to listening when it is released. |
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. ;) |
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:
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. |
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. |
@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? |
All this looks like a lot of progress :)
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.
|
Thanks. Yeah the stopping issue is resolved. |
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. |
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). |
@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. |
So I used a memory watchdog to see where else it is accessed.
The values copied in (1) don't seem to be read anywhere. I hope this helps. |
Will come back to it. Have to fix some other things first. But maybe this is already helping. :) |
Some findings while reverse engineering (based on AM2_CPU english 1.07):
The text was updated successfully, but these errors were encountered: