Optimization using the example of intro 'Start'

ZXNet echo conference «code.zx»

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_1 .W ══════════════════ (c) Ivan Roshchin, Moscow Fido: 2:5020/689.53 ZXNet: 500:95/462.53 E-mail: asder_ffc@softhome.net WWW: http://www.ivr.da.ru Optimization using the example of intro "Start" ════════════════════════════════════ ("Radiomir. Your computer" 7-10/2001) Surely you have heard about the largest Russian demoparty 2000 of the year - Chaos Constructions 000. My works, one of which is the 512-byte intro "Start". What is intro? This is a small program that combines graphic effects and (usually) musical accompaniment. Creating an intro without any exaggeration can be called real art. Often a program whose running time is a few minutes, they write for many months. And then, on demoparty, viewers choose the best intro, which implements the most beautiful effects for a given size limit. Competitions (intro compo) are held in various categories: 512 b, 1 Kb, 4 Kb and so on. These numbers determine the maximum length of the intro code block. After booting from disk, as a rule, the intro can use all available RAM. Therefore, the authors do not they especially try to optimize the intro in length, preferring write "loose" code and then compress it with specialpacker programs (of which there are more than a dozen). This is quite justified: in most cases automatic packaging gives better results compared to manual optimization, not to mention the investment of time and effort. But this method is not suitable for 512-byte intros. I think you already guessed why: an unpacking program, even the most primitive LZ, will take up a relatively large amount of space compared to the intro itself. So you have to optimize the code manually, using many known methods and inventing new ones and winning byte by byte as a result. It's not enough for this just know assembler well - you need to have a clear understanding imagine what each command will look like in machine code and for how many cycles will it execute; need to know perfectly ZX Spectrum architecture, including many undocumented features... In general, intro optimization is not a task for "dummies". :-) In this article I just want to look at the example of my intro "Start" use of various optimization techniques. Perhaps among them there will be those whose existence you know didn't even suspect it! Not all techniques are specific to the ZX Spectrum - something may be useful even if you have absolutely no another computer. First, I’ll say a few words about what we will see, by launching intro. You will need this information to understand -and what exactly does the program do? Intro can work on any ZX Spectrum 48/128, but "Pentagon" and faster models are recommended - firstly, to make pre-calculations faster, and secondly - so that the screen is cleared at the beginning and at the end "instantly" (see section "Increasing performance due to opening cycles"). After startup, about 12 seconds are spent on preliminary calculations - at this time you can only wait, looking at black screen. Then the “attribute plasma” effect appears. (All the beauty of this effect is in movement, and the motionless drawing alas, I am not able to convey it.) ┌─────────────────────────────┐ │pic_1.scr │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────────────────────────────┘ Fig. 1 There is musical accompaniment (by the way, out of six exhibited at Chaos Constructions 512 byte intro music was only in "Start"!). Intro duration - 48 seconds,according to the length of the melody. Along with 12 seconds at the beginning it turns out exactly a minute - this was exactly the limit in party rules So, with what the intro looks like for an inexperienced viewer, we figured it out. If interested, you can download it from my web pages, the address of which is indicated at the beginning of the article, and all of them themselves see and hear. And now, as I promised, let’s look at the intro from the inside. :-) ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_1 .W ══════════════════ (c) Ivan Roshchin, Moscow Fido: 2:5020/689.53 ZXNet: 500:95/462.53 E-mail: asder_ffc@softhome.net WWW: http://www.ivr.da.ru Optimization using the example of intro "Start" ════════════════════════════════════ ("Radiomir. Your computer" 7-10/2001) Surely you have heard about the largest Russian demoparty 2000 of the year - Chaos Constructions 000. My works, one of which is the 512-byte intro "Start". What is intro? This is a small program that combines graphic effects and (usually) musical accompaniment. Creating an intro without any exaggeration can be called real art. Often a program whose running time is a few minutes, they write for many months. And then, on demoparty, viewers choose the best intro, which implements the most beautiful effects for a given size limit. Competitions (intro compo) are held in various categories: 512 b, 1 Kb, 4 Kb and so on. These numbers determine the maximum length of the intro code block. After booting from disk, as a rule, the intro can use all available RAM. Therefore, the authors do not they especially try to optimize the intro in length, preferring write "loose" code and then compress it with specialpacker programs (of which there are more than a dozen). This is quite justified: in most cases automatic packaging gives better results compared to manual optimization, not to mention the investment of time and effort. But for 512-byte intros this method is not suitable. I think you already guessed why: an unpacking program, even the most primitive LZ, will take up a relatively large amount of space compared to the intro itself. So you have to optimize the code manually, using many known methods and inventing new ones and winning byte by byte as a result. It's not enough for this just know assembler well - you need to have a clear understanding imagine what each command will look like in machine code and for how many cycles will it execute; need to know perfectly ZX Spectrum architecture, including many undocumented features... In general, intro optimization is not a task for "dummies". :-) In this article I just want to look at the example of my intro "Start" use of various optimization techniques. Perhaps among them there will be those whose existence you know didn't even suspect it! Not all techniques are specific to the ZX Spectrum - something may be useful even if you have absolutely no another computer. First, I’ll say a few words about what we will see, by launching intro. You will need this information to understand -and what exactly does the program do? Intro can work on any ZX Spectrum 48/128, but "Pentagon" and faster models are recommended - firstly, to make pre-calculations faster, and secondly - so that the screen is cleared at the beginning and at the end "instantly" (see section "Increasing performance due to opening cycles"). After startup, about 12 seconds are spent on preliminary calculations - at this time you can only wait, looking at black screen. Then the “attribute plasma” effect appears. (All the beauty of this effect is in movement, and the motionless drawing alas, I am not able to convey it.) ┌─────────────────────────────┐ │pic_1.scr │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────────────────────────────┘ Fig. 1 There is musical accompaniment (by the way, out of six exhibited at Chaos Constructions 512 byte intro music was only in "Start"!). Intro duration - 48 seconds,according to the length of the melody. Along with 12 seconds at the beginning it turns out exactly a minute - this was exactly the limit in party rules So, with what the intro looks like for an inexperienced viewer, we figured it out. If interested, you can download it from my web pages, the address of which is indicated at the beginning of the article, and all of them themselves see and hear. And now, as I promised, let’s look at the intro from the inside. :-) ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_2 .W ══════════════════ Location of data structures ──────────────────────────── First, let's give some definitions and explanations. _Tables_, _arrays_ are ordinary one-dimensional arrays. These are the ones I will keep in mind when talking about data structures. For convenience, I call tables those arrays that do not change during program operation. Unless otherwise noted, the length of each element assumed to be one byte. _Segment_ - a memory section 256 bytes long, starting from an address that is a multiple of 256. Let me remind you that 256 is 100 in hexadecimal number system. Therefore, junior the segment start address byte is zero. Segment definitely determined by the high byte of its start address. High bytes the addresses of all cells in the segment are the same. Let's call two segments _adjacent_ if one of them directly follows the other. Property of adjacent segments - if you take any memory cell from one segment and any of another, then the high bytes of their addresses will differ by unit. Let's call the data structure _aligned to the segment boundary_, if it starts from the beginning of this segment. Obviously junior the address byte of the first element of such a structure is zero. Let's call a data structure _single-segment_ if it lies entirely in any one segment. All elementsIn this structure, the high byte of the address is the same. Table TABLE_SIN (line 14), table PALETTE (line 18) and the array with the contents of the AY registers (lines 60-61) are aligned to segment boundary. What is the reason for this arrangement of these data structures? Firstly, it simplifies access to their elements. Valid but to access any element, you must first calculate its address. If each element takes up one byte (and for the above data structures this is exactly the case), then for to calculate the address of the i-th element, you need to add i to the address the beginning of the structure. So, if the structure is aligned to the boundary segment (and then, as we remember, the low byte of the address of its beginning is equal to zero), and the number of elements of this structure does not exceed 256 (which means it is single-segment), then the high byte of the address of the i-th element will be equal to the most significant byte of the address of the beginning of the structure, and the low byte will be equal to i. Thus, when calculating the address you can do without the addition operation and still win in speed and program length. Secondly, the addresses of all elements of such a data structure differ only in the low byte. Therefore, when sequential access to elements, it is enough to increase (decrement) only the low byte of the address, which also provides gain in speed and program length. Thirdly, if the length of the structure is 256 bytes, thenthe possibility of its automatic looping, that is, transition from last element to first and from first to last without any special checks. When we increase junior byte address of the last element of the structure, then automatically we get the address of its first element (for example, HL=#80FF, with the command INC L increase the low byte of the address and get HL=#8000). Similarly, when we decrement the low byte of the address of the first element of the structure, then we automatically obtain its address last element (HL=#8000, DEC L -> HL=#80FF). This is very convenient if the data structure contains periodic values functions (TABLE_SIN) or values that can be interpreted in a similar way (smooth color transitions in the PALETTE table). Fourth, this arrangement of the data structure provides the ability to optimize the processing of all its elements in a loop (for more details, see "Optimizing Loops"). As you can see, the TABLE_SIN table location address is equals #AA00. Why was this particular address chosen and not any other multiple of 256? The point is that by the time the address TABLE_SIN must be placed in the register pair DE, in register D already contains the number #AA. It is also known that The accumulator value by this time is zero. And single-byte with the command LD E,A (line 147) we place the address we need in DE. And if the TABLE_SIN address were different, we would have to usethree-byte command LD DE,TABLE_SIN. The PALETTE table and the TABLE_SIN table are located in adjacent segments. This was done for a reason. In the inner loop effect "plasma" (lines 238-264) work is in progress with one table, then on the other. The H register contains the highest byte of the address of the currently used table. Since these high bytes differ by one, instead of rebooting register H using the LD H,n instruction becomes possible using the INC H and DEC H commands (lines 252, 260), thanks which gives us a gain in both length and speed. Note the data structure containing the information about musical patterns (lines 753-810). This is a single segment structure. What does this give? Ease of reference to its elements. Everyone an element is uniquely specified by the low byte of its address, so as the most significant byte of the address is the same for all elements. Thanks to this, in the POSITIONS table (lines 402-430), where the order of playing patterns is determined, each element (which is a link to information about the corresponding pattern) takes up one byte. And if the link indicated the full address, it would take up two bytes. Selecting the most advantageous data presentation ────────────────────── ─────────────────────── The music for the intro was written in the Pro Tracker 3.4 editor. The length of the module compiled with the player turned out to be equal to4558 bytes. But, according to the conditions, the length of the intro should not be exceed 512 bytes! How was this contradiction resolved? Significant reduction in size (and in addition - time) execution of the playback procedure!) was achieved due to representation of the data necessary to play music in the most advantageous form. The choice of this form was largely due to the individual characteristics of the melody. Let's consider this with examples. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_2 .W ══════════════════ Location of data structures ──────────────────────────── First, let's give some definitions and explanations. _Tables_, _arrays_ are ordinary one-dimensional arrays. These are the ones I will keep in mind when talking about data structures. For convenience, I call tables those arrays that do not change during program operation. Unless otherwise noted, the length of each element assumed to be one byte. _Segment_ - a memory section 256 bytes long, starting from an address that is a multiple of 256. Let me remind you that 256 is 100 in hexadecimal number system. Therefore, junior the segment start address byte is zero. Segment definitely determined by the high byte of its start address. High bytes the addresses of all cells in the segment are the same. Let's call two segments _adjacent_ if one of them directly follows the other. Property of adjacent segments - if you take any memory cell from one segment and any of another, then the high bytes of their addresses will differ by unit. Let's call the data structure _aligned to the segment boundary_, if it starts from the beginning of this segment. Obviously junior the address byte of the first element of such a structure is zero. Let's call a data structure _single-segment_ if it lies entirely in any one segment. All elementsIn this structure, the high byte of the address is the same. Table TABLE_SIN (line 14), table PALETTE (line 18) and the array with the contents of the AY registers (lines 60-61) are aligned to segment boundary. What is the reason for this arrangement of these data structures? Firstly, it simplifies access to their elements. Valid but to access any element, you must first calculate its address. If each element takes up one byte (and for the above data structures this is exactly the case), then for to calculate the address of the i-th element, you need to add i to the address the beginning of the structure. So, if the structure is aligned to the boundary segment (and then, as we remember, the low byte of the address of its beginning is equal to zero), and the number of elements of this structure does not exceed 256 (which means it is single-segment), then the high byte of the address of the i-th element will be equal to the most significant byte of the address of the beginning of the structure, and the low byte will be equal to i. Thus, when calculating the address you can do without the addition operation and still win in speed and program length. Secondly, the addresses of all elements of such a data structure differ only in the low byte. Therefore, when sequential access to elements, it is enough to increase (decrement) only the low byte of the address, which also provides gain in speed and program length. Thirdly, if the length of the structure is 256 bytes, thenthe possibility of its automatic looping, that is, transition from last element to first and from first to last without any special checks. When we increase junior byte address of the last element of the structure, then automatically we get the address of its first element (for example, HL=#80FF, with the command INC L increase the low byte of the address and get HL=#8000). Similarly, when we decrement the low byte of the address of the first element of the structure, then we automatically obtain its address last element (HL=#8000, DEC L -> HL=#80FF). This is very convenient if the data structure contains periodic values functions (TABLE_SIN) or values that can be interpreted in a similar way (smooth color transitions in the PALETTE table). Fourth, this arrangement of the data structure provides the ability to optimize the processing of all its elements in a loop (for more details, see "Optimizing Loops"). As you can see, the TABLE_SIN table location address is equals #AA00. Why was this particular address chosen and not any other multiple of 256? The point is that by the time the address TABLE_SIN must be placed in the register pair DE, in register D already contains the number #AA. It is also known that The accumulator value by this time is zero. And single-byte with the command LD E,A (line 147) we place the address we need in DE. And if the TABLE_SIN address were different, we would have to usethree-byte command LD DE,TABLE_SIN. The PALETTE table and the TABLE_SIN table are located in adjacent segments. This was done for a reason. In the inner loop effect "plasma" (lines 238-264) work is in progress with one table, then on the other. The H register contains the highest byte of the address of the currently used table. Since these high bytes differ by one, instead of rebooting register H using the LD H,n instruction becomes possible using the INC H and DEC H commands (lines 252, 260), thanks which gives us a gain in both length and speed. Note the data structure containing the information about musical patterns (lines 753-810). This is a single segment structure. What does this give? Ease of reference to its elements. Everyone an element is uniquely specified by the low byte of its address, so as the most significant byte of the address is the same for all elements. Thanks to this, in the POSITIONS table (lines 402-430), where the order of playing patterns is determined, each element (which is a link to information about the corresponding pattern) takes up one byte. And if the link indicated the full address, it would take up two bytes. Selecting the most advantageous data presentation ────────────────────── ─────────────────────── The music for the intro was written in the Pro Tracker 3.4 editor. The length of the module compiled with the player turned out to be equal to4558 bytes. But, according to the conditions, the length of the intro should not be exceed 512 bytes! How was this contradiction resolved? Significant reduction in size (and in addition - time) execution of the playback procedure!) was achieved due to representation of the data necessary to play music in the most advantageous form. The choice of this form was largely due to the individual characteristics of the melody. Let's consider this with examples. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_3 .W ══════════════════ Let's start with the fact that to play music you need a frequency table with division coefficients for each note. B universal player this table contains division coefficients (numbers in the range 0-4095) for all 96 notes. Accordingly, Such a table takes up 96*2=192 bytes. But, knowing what notes are involved in the melody, you can store only them in the table coefficients. The melody written for the intro features only ten different notes - which means that for storing a table only twenty bytes are required. Now let’s pay attention to the range in which they are coefficient values for these ten notes (lines 360-369). This range #6E..#114. If the coefficients were in the range #00-#FF, then to store each of them it would be enough just one byte, not two. So, it turns out that the coefficients can be converted to the specified range if first reduce their values by a certain amount (freq_shift - see line 371). As a result of this optimization, the frequency table (FREQ_TABLE) takes half as much - ten bytes (see lines 742-751). And when during the course of the program you need to place in registers AY value of the division coefficient taken from the table freq_shift is added to the number (line 701). By the way, if the division coefficients were even, we couldit would be even simpler: store their values in a table, divided by two. Then line 701 would contain the command ADD A,A - one byte shorter than ADD A,freq_shift, and faster to execute. As you must have already understood from the above, each of ten notes used in the melody are indicated by a certain number (in number 0..9 - see lines 349-358), and when you need to find out the division coefficient corresponding to a given note, this number serves as an index in the frequency table. It is obvious that in principle it makes absolutely no difference how it will be established correspondence between notes and their numbers - as long as there is one one-to-one. The lowest note does not have to be assigned number 0, and the highest - 9. Thus, there is some freedom of choice. I took advantage of this freedom for further optimization purposes. To understand the essence of the matter, I’ll tell you a little more about the used in the intro melody. The melody itself sounds in channels A and B, and in channel C sounds the accompaniment chords (more precisely, the chord is when the notes sound simultaneously, and in this case the notes sound at the same time queues, but quickly replace each other). Total used four different chords: C5-E5-G5, D5-F5-A5, E5-G5-B5 and A4-C5- E5. Each of them has its own number. Naturally, there is a need for a special table, from which, by chord number, one could extract information about its notes.So, I chose the note numbers in such a way that the frequency the table also served as a chord chart! Look at lines 389-392 - there each chord is assigned some number that is an offset in the ORNAMENTS table (which in turn is part of the FREQ_TABLE table). Three consecutive bytes in this table are division factors for three notes of a chord. Amount of winnings obtained by combining tables, amounted in this case eight bytes. For another program, maybe this is not much, but for a 512-byte intro this is more than one and a half percent! Let's move on to the sample used in the melody. Here how it looks in Pro Tracker: ┌─────────────────────────────┐ │pic_2.scr │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────────────────────────────┘ Fig. 2 For each element of this sample you need to store a value amplitude (number #0-#F) and frequency offset (-2, 0 or +2).The data storage format used in intro allows you to fit both in one byte: in the lower four bits - the value amplitude, and in the higher two - the frequency offset code (see lines 816-822). Thanks to this, it is possible to reduce both the size of the table SAMPLE, and the length of those program fragments where manipulations with this table. Now pay attention to the table of positions (POSITIONS, lines 402-430), which indicates the playback order patterns. Usually when writing a player they do this: in each element This table stores the pattern number (one byte), and with the help of of one table, the pattern number determines its address (length patterns are not the same). Knowing the number of positions in the melody (24) and the number of patterns (13), you can calculate the memory consumption when using this method: 24+13*2=50 bytes. You can store in each element of the position table directly the address of the corresponding pattern. In this case Memory consumption will be 2*24=48 bytes. However, note that all information about patterns (lines 758-810) lies in the segment #8100-#81FF. This means the high bytes Pattern addresses will be the same and equal to #81. So why then store them in a table? Let's leave only the low bytes there, and the entire table will take only 24 bytes! This is what was done in intro. How is the pattern address determined?you can look at lines 487-488. Yes, about patterns: let’s look at how their features influenced structures to the format in which they are stored. Let's list these features first. Firstly, in every eight note pattern. Secondly, each pattern uses just one chord. Third, the volume in the pattern can either decrease (from #F to #C), or increase (from #C to #F), or remain constant (#F). Fourth, many patterns consist of two alternating notes (for example, like this: C5-G4-C5-G4-C5-G4-C5- G4). When choosing the pattern storage format, all this was taken into account, and as a result, information about one pattern takes up either two, or five bytes (see lines 753-810). So little? Yes! And that's how it is it worked. The first byte of the pattern contains in the most significant two bits information about the nature of the volume change - one of three possible values (see lines 378-383). The fifth bit is located "packing" sign - if this bit is set, then this pattern consists of two alternating notes (see lines 385-387). Juniors four bits is the number of the chord used in the pattern (and more precisely - the offset in the ORNAMENTS table). In one (for a packed pattern) or four (for unpacked) the next bytes contain information about notes - two notes in each byte. I already mentioned above that in the melody uses only ten different notes - thanks tohalf a byte is enough to store the note number. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_3 .W ══════════════════ Let's start with the fact that to play music you need a frequency table with division coefficients for each note. B universal player this table contains division coefficients (numbers in the range 0-4095) for all 96 notes. Accordingly, Such a table takes up 96*2=192 bytes. But knowing what notes are involved in the melody, you can store only them in the table coefficients. The melody written for the intro features only ten different notes - which means that for storing a table only twenty bytes are required. Now let’s pay attention to the range in which they are coefficient values for these ten notes (lines 360-369). This range #6E..#114. If the coefficients were in the range #00-#FF, then to store each of them it would be enough just one byte, not two. So, it turns out that the coefficients can be converted to the specified range if first reduce their values by a certain amount (freq_shift - see line 371). As a result of this optimization, the frequency table (FREQ_TABLE) takes half as much - ten bytes (see lines 742-751). And when during the course of the program you need to place in registers AY value of the division coefficient taken from the table freq_shift is added to the number (line 701). By the way, if the division coefficients were even, we couldit would be even simpler: store their values in a table, divided by two. Then line 701 would contain the command ADD A,A - one byte shorter than ADD A,freq_shift, and faster to execute. As you must have already understood from the above, each of ten notes used in the melody are indicated by a certain number (in number 0..9 - see lines 349-358), and when you need to find out the division coefficient corresponding to a given note, this number serves as an index in the frequency table. It is obvious that in principle it makes absolutely no difference how it will be established correspondence between notes and their numbers - as long as there is one one-to-one. The lowest note doesn't have to be assigned number 0, and the highest - 9. Thus, there is some freedom of choice. I took advantage of this freedom for further optimization purposes. To understand the essence of the matter, I’ll tell you a little more about the used in the intro melody. The melody itself sounds in channels A and B, and in channel C sounds the accompaniment chords (more precisely, the chord is when the notes sound simultaneously, and in this case the notes sound at the same time queues, but quickly replace each other). Total used four different chords: C5-E5-G5, D5-F5-A5, E5-G5-B5 and A4-C5- E5. Each of them has its own number. Naturally, there is a need for a special table, from which, by chord number, one could extract information about its notes.So, I chose the note numbers in such a way that the frequency the table also served as a chord chart! Look at lines 389-392 - there each chord is assigned some number that is an offset in the ORNAMENTS table (which in turn is part of the FREQ_TABLE table). Three consecutive bytes in this table are division factors for three notes of a chord. Amount of winnings obtained by combining tables, amounted in this case eight bytes. For another program, maybe this is not much, but for a 512-byte intro this is more than one and a half percent! Let's move on to the sample used in the melody. Here how it looks in Pro Tracker: ┌─────────────────────────────┐ │pic_2.scr │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────────────────────────────┘ Fig. 2 For each element of this sample you need to store a value amplitude (number #0-#F) and frequency offset (-2, 0 or +2).The data storage format used in intro allows you to fit both in one byte: in the lower four bits - the value amplitude, and in the higher two - the frequency offset code (see lines 816-822). Thanks to this, it is possible to reduce both the size of the table SAMPLE, and the length of those program fragments where manipulations with this table. Now pay attention to the table of positions (POSITIONS, lines 402-430), which indicates the playback order patterns. Usually when writing a player they do this: in each element This table stores the pattern number (one byte), and with the help of of one table, the pattern number determines its address (length patterns are not the same). Knowing the number of positions in the melody (24) and the number of patterns (13), you can calculate the memory consumption when using this method: 24+13*2=50 bytes. You can store in each element of the position table directly the address of the corresponding pattern. In this case Memory consumption will be 2*24=48 bytes. However, note that all information about patterns (lines 758-810) lies in the segment #8100-#81FF. This means the high bytes Pattern addresses will be the same and equal to #81. So why then store them in a table? Let's leave only the low bytes there, and the entire table will take only 24 bytes! This is what was done in intro. How is the pattern address determined?you can look at lines 487-488. Yes, about patterns: let’s look at how their features influenced structures to the format in which they are stored. Let's list these features first. Firstly, in every eight note pattern. Secondly, each pattern uses just one chord. Third, the volume in the pattern can either decrease (from #F to #C), or increase (from #C to #F), or remain constant (#F). Fourth, many patterns consist of two alternating notes (for example, like this: C5-G4-C5-G4-C5-G4-C5- G4). When choosing the pattern storage format, all this was taken into account, and as a result, information about one pattern takes up either two, or five bytes (see lines 753-810). So little? Yes! And that's how it is it worked. The first byte of the pattern contains in the most significant two bits information about the nature of the volume change - one of three possible values (see lines 378-383). The fifth bit is located "packing" sign - if this bit is set, then this pattern consists of two alternating notes (see lines 385-387). Juniors four bits is the number of the chord used in the pattern (and more precisely - the offset in the ORNAMENTS table). In one (for a packed pattern) or four (for unpacked) the next bytes contain information about notes - two notes in each byte. I already mentioned above that in the melody uses only ten different notes - thanks tohalf a byte is enough to store the note number. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_4 .W ══════════════════ So, with the help of a well-chosen data representation, you can significantly reduce program size and/or increase it performance. Consider only what is most compact presentation of data is not always the most beneficial. Don't forget monitor the length of program fragments processing these data, and check whether there will be a win in the end. Using program sections as command codes, and as data ────────────────────────────── ─────────────────────────────── Pay attention to the memory area at addresses #8001-#8009. There is a fragment of the sample table construction procedure. This table is built once at the very beginning program operation, and then the specified area is used for storing the contents of the music coprocessor registers (AY). When playing a melody in register R7 (byte at address #8007) should contain the value %??111000 (high two bits can be any), and in register R10 (byte at address #800A) - %???01001 (the most significant three bits can be anything). So, in the program at address #8007 contains the command LD A,B (line 92), and the code of this command - %01111000 - subsequently serves as as the value written to the 7th register AY. Likewise, The XOR C command located at address #800A (line 101) has the code%10101001 and henceforth serves as the value, written to the 10th register AY. So first the bytes in cells #8007 and #800A are used as command codes, and then - as data. This technique allows you to reduce the length of the program, in this case case - by ten bytes (there is no need for commands LD A,%00111000: LD (#8007),A: LD A,%00001001: LD (#800A),A). Of course, the command codes match the required values AY registers are not accidental. I had to carefully think through the logic work of the sample table construction procedure and select the address compiling the intro so that the necessary commands end up in the right ones places. It helped that it was absolutely accurate matching command codes and AY register values was not required - differences were allowed in the most significant bits that did not affect sound formation. Now pay attention to the execution once in the very at the beginning of the program, the procedure for filling the screen with texture (lines 121-140). Line 134 contains the command JR Z,GRID_2 (in other words, JR Z,$+3), which in machine code looks like #28, #01. So, the variable then used in the program QUARK is located at the same address as the second byte of this teams. As we can see, value #01 is first used as part of jump commands and then as the initial value variable, that is, as data.The QUARK variable could be placed in any free memory cell - but then before using it for the first time you need to would be to initialize with the value #01, and this would take more five bytes (LD A,1: LD (QUARK),A). These five bytes are me and saved. Naturally, the procedure for filling the screen with texture was specially written so that it contained the command JR Z,$+3. In the section "Selecting the most advantageous data representation" it was mentioned that the frequency table stores values division coefficients reduced by the constant freq_shift. The value of this constant must be such that the reduced division coefficients were in the range 0..#FF. Based on this, it is not difficult to come to the conclusion that the value of freq_shift should lie in the range #15..#6E, i.e. there is a certain freedom of choice. Taking advantage of this freedom, I chose freq_shift like so, so that the first element of the FREQ_TABLE table is equal to #C9 (see. line 371). What is this for? The point is that directly before the FREQ_TABLE table there is a procedure IZM_FRQ (rows 682-715), ending with the RET command. The code for this command is just and there is #C9. This means that the RET command can be removed from the program (here why is it commented out in line 715), and in its quality there will be act as the first element of the FREQ_TABLE table. Optimization of placing constants in registers and memory cells──────────────────────────── ───────────────────────────── Loading constants into registers or writing them to memory is performed in a real program quite often, and optimization These actions can bring very tangible results. Oh some methods of such optimization used in writing the intro, I’ll tell you. When it is necessary to reset the accumulator, the command LD A,0 is usually are replaced by XOR A (see lines 234, 311, 525, 647), which allows save 1 byte/3 clock cycles on each such replacement. True, this the command, unlike LD, affects the flags, so you have to be here attentive. For example, this fragment (lines 522-526): BIT 5,(HL) LD A,#23 JR Z,SET_COD XOR A SET_COD LD (ADR_CODE),A Under no circumstances should you write it like this: BIT 5,(HL) XOR A JR NZ,SET_COD LD A,#23 SET_COD LD (ADR_CODE),A because after the XOR A command the Z flag will always be set (it is set by the result of the operation, and the result is null!), so further checking its value (JR NZ) loses all meaning and the program will not work correctly. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_4 .W ══════════════════ So, with the help of a well-chosen data representation, you can significantly reduce program size and/or increase it performance. Consider only what is most compact presentation of data is not always the most beneficial. Don't forget monitor the length of program fragments processing these data, and check whether there will be a win in the end. Using program sections as command codes, and as data ────────────────────────────── ─────────────────────────────── Pay attention to the memory area at addresses #8001-#8009. There is a fragment of the sample table construction procedure. This table is built once at the very beginning program operation, and then the specified area is used for storing the contents of the music coprocessor registers (AY). When playing a melody in register R7 (byte at address #8007) should contain the value %??111000 (high two bits can be any), and in register R10 (byte at address #800A) - %???01001 (the most significant three bits can be anything). So, in the program at address #8007 contains the command LD A,B (line 92), and the code of this command - %01111000 - subsequently serves as as the value written to the 7th register AY. Likewise, The XOR C command located at address #800A (line 101) has the code%10101001 and henceforth serves as the value, written to the 10th register AY. So first the bytes in cells #8007 and #800A are used as command codes, and then - as data. This technique allows you to reduce the length of the program, in this case case - by ten bytes (there is no need for commands LD A,%00111000: LD (#8007),A: LD A,%00001001: LD (#800A),A). Of course, the command codes match the required values AY registers are not accidental. I had to carefully think through the logic work of the sample table construction procedure and select the address compiling the intro so that the necessary commands end up in the right ones places. It helped that it was absolutely accurate matching command codes and AY register values was not required - differences were allowed in the most significant bits that did not affect sound formation. Now pay attention to the execution once in the very at the beginning of the program, the procedure for filling the screen with texture (lines 121-140). Line 134 contains the command JR Z,GRID_2 (in other words, JR Z,$+3), which in machine code looks like #28, #01. So, the variable then used in the program QUARK is located at the same address as the second byte of this teams. As we can see, value #01 is first used as part of jump commands and then as the initial value variable, that is, as data.The QUARK variable could be placed in any free memory cell - but then before using it for the first time you need to would be to initialize with the value #01, and this would take more five bytes (LD A,1: LD (QUARK),A). These five bytes are me and saved. Naturally, the procedure for filling the screen with texture was specially written so that it contained the command JR Z,$+3. In the section "Selecting the most advantageous data representation" it was mentioned that the frequency table stores values division coefficients reduced by the constant freq_shift. The value of this constant must be such that the reduced division coefficients were in the range 0..#FF. Based on this, it is not difficult to come to the conclusion that the value of freq_shift should lie in the range #15..#6E, i.e. there is a certain freedom of choice. Taking advantage of this freedom, I chose freq_shift like so, so that the first element of the FREQ_TABLE table is equal to #C9 (see. line 371). What is this for? The point is that directly before the FREQ_TABLE table there is a procedure IZM_FRQ (rows 682-715), ending with the RET command. The code for this command is just and there is #C9. This means that the RET command can be removed from the program (here why is it commented out in line 715), and in its quality there will be act as the first element of the FREQ_TABLE table. Optimization of placing constants in registers and memory cells──────────────────────────── ───────────────────────────── Loading constants into registers or writing them to memory is performed in a real program quite often, and optimization These actions can bring very tangible results. Oh some methods of such optimization used in writing the intro, I’ll tell you. When it is necessary to reset the accumulator, the command LD A,0 is usually are replaced by XOR A (see lines 234, 311, 525, 647), which allows save 1 byte/3 clock cycles on each such replacement. True, this the command, unlike LD, affects the flags, so you have to be here attentive. For example, this fragment (lines 522-526): BIT 5,(HL) LD A,#23 JR Z,SET_COD XOR A SET_COD LD (ADR_CODE),A Under no circumstances should you write it like this: BIT 5,(HL) XOR A JR NZ,SET_COD LD A,#23 SET_COD LD (ADR_CODE),A because after the XOR A command the Z flag will always be set (it is set by the result of the operation, and the result is null!), so further checking its value (JR NZ) loses all meaning and the program will not work correctly. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_5 .W ══════════════════ On line 116 you need to place a 0 at the address given in register pair HL. It would be possible to do this with the LD (HL) command, 0. But it is known that by this moment register B has been reset to zero after DJNZ. Thus, it is sufficient to use the command LD (HL),B. The savings are 1 byte/3 clock cycles. Before filling the screen with the texture (lines 121-140) it is necessary to place in HL the address of the first byte to be processed video memory - #57FF. But by this time the HL already contains this meaning: it ended up there after the CLS procedure performed cleaning the screen. Therefore, the LD HL,#57FF command is not needed. The savings are 3 bytes/10 clock cycles. In line 147, you need to load the value #AA00 into DE (address tables TABLE_SIN). But we know that D=#AA and A=0 (see lines 142-143). Then instead of the command LD DE,#AA00 you can use command LD E,A. The savings are 2 bytes/6 clock cycles. Address table TABLE_SIN was deliberately chosen equal to #AA00 (see section "Location of data structures") so that this optimization becomes possible. In line 189 you need to load the table address into DE PALETTE. It is known that before this the address was set in DE tables TABLE_SIN, and these 256-byte tables are located next to each other behind a friend. Then it is enough to use the INC D command instead LD DE,PALETTE. The savings are 2 bytes/6 clock cycles.The fact that the tables PALETTE and TABLE_SIN are located in adjacent segments, is used in the future. Yes, in the line 252 command LD H,PALETTE/256 can be replaced with INC H, since it is known that register H contains the high byte of the table address TABLE_SIN. Similarly, on line 260, instead of the LD H command, TABLE_SIN/256 the DEC H command is used. In both In this case, the savings are 1 byte/3 clock cycles. The location in adjacent segments of the POSITIONS table and pattern data (rows 478-489). And the fact that the A_AMP variable and the FREQ_TABLE table are located in one segment, allows you to do without loading at all high byte of the address (lines 566-582). In lines 291-294 you need to increase SP by 4 (otherwise speaking, remove two values stored there from the stack, now unnecessary) and place 0 at address TRIGGER. It's possible to do this like this: POP AF: POP AF: XOR A: LD (TRIGGER),A. But it is known that The most significant byte of the value at the top of the stack is zero (line 469). Then the XOR A command is not needed - just write at address TRIGGER the value that will end up in the accumulator after the first POP AF command. The savings are 1 byte/4 clock cycles. In lines 475-476, in order to reset DE, we use two commands LD D,A: LD E,A, despite the fact that A=0. This allows save 1 byte/2 clock cycles compared to using the LD instruction DE,0.If you want to learn more about download optimization constants into registers, I recommend studying [1]. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_5 .W ══════════════════ On line 116 you need to place a 0 at the address given in register pair HL. It would be possible to do this with the LD (HL) command, 0. But it is known that by this moment register B has been reset after DJNZ. Thus, it is sufficient to use the command LD (HL),B. The savings are 1 byte/3 clock cycles. Before filling the screen with the texture (lines 121-140) it is necessary to place in HL the address of the first byte to be processed video memory - #57FF. But by this point the HL already contains this meaning: it ended up there after the CLS procedure performed cleaning the screen. Therefore, the LD HL,#57FF command is not needed. The savings are 3 bytes/10 clock cycles. In line 147, you need to load the value #AA00 into DE (address tables TABLE_SIN). But we know that D=#AA and A=0 (see lines 142-143). Then instead of the command LD DE,#AA00 you can use command LD E,A. The savings are 2 bytes/6 clock cycles. Address table TABLE_SIN was deliberately chosen equal to #AA00 (see section "Location of data structures") so that this optimization becomes possible. In line 189 you need to load the table address into DE PALETTE. It is known that before this the address was set in DE tables TABLE_SIN, and these 256-byte tables are located next to each other behind a friend. Then it is enough to use the INC D command instead LD DE,PALETTE. The savings are 2 bytes/6 clock cycles.The fact that the tables PALETTE and TABLE_SIN are located in adjacent segments, is used in the future. Yes, in the line 252 command LD H,PALETTE/256 can be replaced with INC H, since it is known that register H contains the high byte of the table address TABLE_SIN. Similarly, on line 260, instead of the LD H command, TABLE_SIN/256 the DEC H command is used. In both In this case, the savings are 1 byte/3 clock cycles. The location in adjacent segments of the POSITIONS table and pattern data (rows 478-489). And the fact that the A_AMP variable and the FREQ_TABLE table are located in one segment, allows you to do without loading at all high byte of the address (lines 566-582). In lines 291-294 you need to increase SP by 4 (otherwise speaking, remove two values stored there from the stack, now unnecessary) and place 0 at address TRIGGER. It's possible to do this like this: POP AF: POP AF: XOR A: LD (TRIGGER),A. But it is known that The most significant byte of the value at the top of the stack is zero (line 469). Then the XOR A command is not needed - just write at address TRIGGER the value that will end up in the accumulator after the first POP AF command. The savings are 1 byte/4 clock cycles. In lines 475-476, in order to reset DE, we use two commands LD D,A: LD E,A, despite the fact that A=0. This allows save 1 byte/2 clock cycles compared to using the LD instruction DE,0.If you want to learn more about download optimization constants into registers, I recommend studying [1]. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_6 .W ══════════════════ Cycle optimization ────────────────── You can often benefit from replacing the cycle with advance a known number of repetitions ("for") per loop with output at condition ("repeat-until"). Consider, for example, filling the screen with a texture (lines 129-140). The length of the filled area is known in advance - #1800 byte. If we used a "for" loop, we would have to create a separate counter (and allocate a register pair for it), enter the number of repetitions into it (#1800), and then in each pass through the loop, decrease the counter value and check it against equal to zero. How is it done in the intro? No special counter there no, but instead after each pass of the loop it produces check: whether the sixth bit of register H has become zero (line 139). Let me explain the meaning of this check. Video memory full (#4000-#57FF) comes from high to low addresses, and in The HL register pair stores the address of the currently filled cell. Each time the loop passes, this address is decreased by one. When all video memory is full, the address is decremented to #3FFF. Now pay attention to the high byte of the address: when it lies in the range #40-#57 (%01000000-%01010111), it the sixth bit is set; but as soon as it becomes equal to #3F (%00111111), the sixth bit is reset. So it workscondition for exiting the loop. Simple and effective, isn't it? The general technique for such optimization is as follows: a special variable is created for the quantity counter executions of the loop, but instead after each pass a specific condition associated with an address is checked data processed in a loop. Other examples of loops optimized in this way can be found here: can see in lines 149-180 (sin table calculation), 191-197 (shaping the palette), 238-264 (internal effect loop "plasma"), 315-326 (screen clearing), 667-677 (writing to registers AY). Another example is the main intro loop (lines 213-269). First of all, we note that it is organized somewhat unusually: the exit condition is checked in the PLAY subroutine, responsible for the musical accompaniment (in lines 478-485). Yes done because the duration of the intro is determined length of the melody. After such an exit from the cycle, naturally, it is necessary to remove unnecessary values from the stack (stored in subroutine contents AF and return address), which is executed in lines 291-294. The main loop should exit when it reaches end of the POSITIONS table. The length of this table (lines 402-430) is not exceeds 256 bytes. In this case, keep an eye on reaching the end You can do it very simply - by the low byte of the address of the current element.As soon as it becomes equal to the low byte of the address of the first cell memory lying after the table is all, the table is processed (rows 483-485). As you can see, this comparison with the transition takes only five bytes. If the POSITIONS table were longer 256 bytes, then it would contain at least two cells with identical low bytes of the address. Then I would have to compare the full address of an element with the address of the first next memory cells: LD BC,PLAY AND A SBC HL,BC JR Z,END And this already takes eight bytes - three bytes longer. Increased speed due to table specification of functions ────────────────────────── ─────────────────────────── The "plasma" graphic effect used in the intro requires calculating the sine when calculating each element of the image. Naturally, in order to have time to calculate the entire image in 1/50 seconds, the sine calculation should be as fast as possible. The tabular method is most suitable for this purpose. The essence of it is as follows: there is a table containing values functions for all possible values of the argument, and when in the program needs to calculate the value of the function, from the table just the finished result is taken. The use of this method, of course, increases the size program, but at the same time significantly increases performance. You can take a number from the table in a few clock cycles, and the calculationsine using a ROM calculator (in fact - calculating the sum series) will take several orders of magnitude longer. In this it is easy to verify if you notice that the formation of the table when launching the intro takes as much as 12 seconds, and is calculated at the same time there are only 256 sine values. However, be careful when using table specification of functions on faster processors compared to the Z80. Make sure first that the winnings are valid there will be. The fact is that the processor usually runs at higher frequency than memory, and computing the function may take less time than the relatively slow reading from memory. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_6 .W ══════════════════ Cycle optimization ────────────────── You can often benefit from replacing the cycle with advance a known number of repetitions ("for") per loop with output at condition ("repeat-until"). Consider, for example, filling the screen with a texture (lines 129-140). The length of the filled area is known in advance - #1800 byte. If we used a "for" loop, we would have to create a separate counter (and allocate a register pair for it), enter the number of repetitions into it (#1800), and then in each pass through the loop, decrease the counter value and check it against equal to zero. How is it done in the intro? No special counter there no, but instead after each pass of the loop it produces check: whether the sixth bit of register H has become zero (line 139). Let me explain the meaning of this check. Video memory full (#4000-#57FF) comes from high to low addresses, and in The HL register pair stores the address of the currently filled cell. Each time the loop passes, this address is decreased by one. When all video memory is full, the address is decremented to #3FFF. Now pay attention to the high byte of the address: when it lies in the range #40-#57 (%01000000-%01010111), it the sixth bit is set; but as soon as it becomes equal to #3F (%00111111), the sixth bit is reset. So it workscondition for exiting the loop. Simple and effective, isn't it? The general technique for such optimization is as follows: a special variable is created for the quantity counter executions of the loop, but instead after each pass a specific condition associated with an address is checked data processed in a loop. Other examples of loops optimized in this way can be found here: can see in lines 149-180 (sin table calculation), 191-197 (shaping the palette), 238-264 (internal effect loop "plasma"), 315-326 (screen clearing), 667-677 (writing to registers AY). Another example is the main intro loop (lines 213-269). First of all, we note that it is organized somewhat unusually: the exit condition is checked in the PLAY subroutine, responsible for the musical accompaniment (in lines 478-485). Yes done because the duration of the intro is determined length of the melody. After such an exit from the cycle, naturally, it is necessary to remove unnecessary values from the stack (stored in subroutine contents AF and return address), which is executed in lines 291-294. The main loop should exit when it reaches end of the POSITIONS table. The length of this table (lines 402-430) is not exceeds 256 bytes. In this case, keep an eye on reaching the end You can do it very simply - by the low byte of the address of the current element.As soon as it becomes equal to the low byte of the address of the first cell memory lying after the table is all, the table is processed (rows 483-485). As you can see, this comparison with the transition takes only five bytes. If the POSITIONS table were longer 256 bytes, then it would contain at least two cells with identical low bytes of the address. Then I would have to compare the full address of an element with the address of the first next memory cells: LD BC,PLAY AND A SBC HL,BC JR Z,END And this already takes eight bytes - three bytes longer. Increased speed due to table specification of functions ────────────────────────── ─────────────────────────── The "plasma" graphic effect used in the intro requires calculating the sine when calculating each element of the image. Naturally, in order to have time to calculate the entire image in 1/50 seconds, the sine calculation should be as fast as possible. The tabular method is most suitable for this purpose. The essence of it is as follows: there is a table containing values functions for all possible values of the argument, and when in the program needs to calculate the value of the function, from the table just the finished result is taken. The use of this method, of course, increases the size program, but at the same time significantly increases performance. You can take a number from the table in a few clock cycles, and the calculationsine using a ROM calculator (in fact - calculating the sum series) will take several orders of magnitude longer. In this it is easy to verify if you notice that the formation of the table when launching the intro takes as much as 12 seconds, and is calculated at the same time there are only 256 sine values. However, be careful when using table specification of functions on faster processors compared to the Z80. Make sure first that the winnings are valid there will be. The fact is that the processor usually runs at higher frequency than memory, and computing the function may take less time than the relatively slow reading from memory. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_7 .W ══════════════════ Increased speed by opening loops ───────────────────── ────────────────────── When it is necessary to optimize a program primarily in terms of length, this method is rarely used - after all, when opening cycles, the length programs are increasing. However, in one place the intro is like this the technique is still used. Pay attention to how the cleaning procedure works screen (lines 311-328). First, the impulse is waited vertical synchronization (HALT command), then set the border color is black and the attribute buffer (length 768 bytes), and zeroing goes from high to low addresses (so It’s more convenient to check the loop exit condition). So, reset it attributes preferably before the ray begins to draw the first line of the main screen, otherwise it won’t be very pleasant effect: in one frame the screen will not be completely cleared. Let's calculate the total operating time of the most optimized by cycle length (to the right of each command it is indicated how long it will take clock cycles it is executed): CLS_1 LD (HL),A ;7 DEC HL ;6 BIT 3,H ;8 JR NZ,CLS_1 ;12 It turns out (7+6+8+12)*768=25344 clock cycles. During this time the beam will draw more than 1/3 of the screen, and this is not suitable for us. If there were no limit on the total cycle timeso rigid (say, no more than 24000 cycles), one could do this: replace the JR command with the faster JP (it is on byte is longer), which would reduce operating time by 2*768=1536 clock cycles. But in this case such an increase the speed turns out to be insufficient. But if you expand the loop, as done in the intro: CLS_1 LD (HL),A ;7 DEC L ;4 LD(HL),A ;7 DEC HL ;6 BIT 3,H ;8 JR NZ,CLS_1 ;12 then the length will increase by only two bytes, and the execution time will be (7+4+7+6+8+12)*384=16896 cycles. During this time on the screen 75 lines will have time to be drawn (at 224 cycles per line). This is already quite acceptable: on the Pentagon (namely on it demonstrated work on CC 000) upper part of the curb is 80 lines, so by the time the beam starts draw the first line of the main screen, the attributes will already be reset. Oh, by the way: note that when expanding a loop, it doesn’t just repeated twice (LD HL,A:DEC HL:LD HL,A:DEC HL). First the DEC HL command is replaced by the faster DEC L. This is possible since when it is executed, the high byte HL will certainly not be change. Self-modifying code ──────────────────────── How to make a program very simple, fast and short? Often this can be achieved by making it self-modifyingchanging - changing itself in the process of work. This is one of the most powerful optimization methods. In the section "Selecting the most advantageous data representation" I already mentioned that in each pattern the volume is either increases, decreases, or remains constant, and that information about this is in the two most significant bits of the first byte pattern descriptor. So, in the procedure of playing music when when moving to the next pattern, the value of these two is checked bits (see lines 491-508), and according to one of three possible cases, either the command is entered at the address CODE_I_D_N INC (HL), or DEC (HL), or NOP (lines 513-514). How easy it is guess, it is this command that will change the volume during pattern playing time (see lines 564-567). If for information on how the volume changes in current pattern, a separate variable was selected, then it would be necessary to check its value each time and depending change the volume from it. This is of course less efficient. Further, in the same section, I mentioned that patterns in which two notes are repeated, stored in packaged form, and that information about pattern packaging is in the fifth bit of the first byte of its descriptor. When moving to the next pattern, the value this bit is checked, and in accordance with it at the address ADR_CODE is placed either the command code NOP (if the patternpacked), or the command code INC HL (otherwise) (see lines 518-526). What happens next? Each pattern byte stores information about two notes. If the pattern is packed, then it contains only one byte with information about notes; if not, then four bytes. When the next two notes are played, the command located at address is executed ADR_CODE (see lines 571-572) and changing the position in the pattern. If there is NOP, then the position will not change, which means that further the same two notes are played (as required). If there command INC HL, then the transition to the next byte will occur pattern, with information about the next two notes. Easier and not do, right? And one last example. On line 629 is the command JR C_NORM - switch to channel C processing. When the melody ends, on line 292 in the second byte of this command (where transition offset is stored) 0 is written. Zero offset in a command, JR simply means moving on to the next command. So Thus, instead of outputting three alternating sound of different frequencies (as was the case throughout the entire melody), the program will output the same sample as in channels A and B, and as a result we will hear the final chord. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_7 .W ══════════════════ Increased speed by opening loops ───────────────────── ────────────────────── When it is necessary to optimize a program primarily in terms of length, this method is rarely used - after all, when opening cycles, the length programs are increasing. However, in one place the intro is like this the technique is still used. Pay attention to how the cleaning procedure works screen (lines 311-328). First, the impulse is waited vertical synchronization (HALT command), then set the border color is black and the attribute buffer (length 768 bytes), and zeroing goes from high to low addresses (so It’s more convenient to check the loop exit condition). So, reset it attributes preferably before the ray begins to draw the first line of the main screen, otherwise it won’t be very pleasant effect: in one frame the screen will not be completely cleared. Let's calculate the total operating time of the most optimized by cycle length (to the right of each command it is indicated how long it will take clock cycles it is executed): CLS_1 LD (HL),A ;7 DEC HL ;6 BIT 3,H ;8 JR NZ,CLS_1 ;12 It turns out (7+6+8+12)*768=25344 clock cycles. During this time the beam will draw more than 1/3 of the screen, and this is not suitable for us. If there were no limit on the total cycle timeso rigid (say, no more than 24000 cycles), one could do this: replace the JR command with the faster JP (it is on byte is longer), which would reduce operating time by 2*768=1536 clock cycles. But in this case such an increase the speed turns out to be insufficient. But if you expand the loop, as done in the intro: CLS_1 LD (HL),A ;7 DEC L ;4 LD(HL),A ;7 DEC HL ;6 BIT 3,H ;8 JR NZ,CLS_1 ;12 then the length will increase by only two bytes, and the execution time will be (7+4+7+6+8+12)*384=16896 cycles. During this time on the screen 75 lines will have time to be drawn (at 224 cycles per line). This is already quite acceptable: on the Pentagon (namely on it demonstrated work on CC 000) upper part of the curb is 80 lines, so by the time the beam starts draw the first line of the main screen, the attributes will already be reset. Oh, by the way: note that when expanding a loop, it doesn’t just repeated twice (LD HL,A:DEC HL:LD HL,A:DEC HL). First the DEC HL command is replaced by the faster DEC L. This is possible since when it is executed, the high byte HL will certainly not be change. Self-modifying code ──────────────────────── How to make a program very simple, fast and short? Often this can be achieved by making it self-modifyingchanging - changing itself in the process of work. This is one of the most powerful optimization methods. In the section "Selecting the most advantageous data representation" I already mentioned that in each pattern the volume is either increases, decreases, or remains constant, and that information about this is in the two most significant bits of the first byte pattern descriptor. So, in the procedure of playing music when when moving to the next pattern, the value of these two is checked bits (see lines 491-508), and according to one of three possible cases, either the command is entered at the address CODE_I_D_N INC (HL), or DEC (HL), or NOP (lines 513-514). How easy it is guess, it is this command that will change the volume during pattern playing time (see lines 564-567). If for information on how the volume changes in current pattern, a separate variable was selected, then it would be necessary to check its value each time and depending change the volume from it. This is of course less efficient. Further, in the same section, I mentioned that patterns in which two notes are repeated, stored in packaged form, and that information about pattern packaging is in the fifth bit of the first byte of its descriptor. When moving to the next pattern, the value this bit is checked, and in accordance with it at the address ADR_CODE is placed either the command code NOP (if the patternpacked), or the command code INC HL (otherwise) (see lines 518-526). What happens next? Each pattern byte stores information about two notes. If the pattern is packed, then it contains only one byte with information about notes; if not, then four bytes. When the next two notes are played, the command located at address is executed ADR_CODE (see lines 571-572) and changing the position in the pattern. If there is NOP, then the position will not change, which means that further the same two notes are played (as required). If there command INC HL, then the transition to the next byte will occur pattern, with information about the next two notes. Easier and not do, right? And one last example. On line 629 is the command JR C_NORM - switch to channel C processing. When the melody ends, on line 292 in the second byte of this command (where transition offset is stored) 0 is written. Zero offset in a command, JR simply means moving on to the next command. So Thus, instead of outputting three alternating sound of different frequencies (as was the case throughout the entire melody), the program will output the same sample as in channels A and B, and as a result we will hear the final chord. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_8 .W ══════════════════ Placing Variables Directly in Load Commands ──────────────────────────── ───────────────────────────── The meaning of this widely used optimization technique is as follows: instead of allocating separate memory cells for storage one- and two-byte variables and then loading them from memory into a register or register pair, variables are allocated directly in the boot commands. Here is a typical example: Was: VAR DB 15 ...... LD A,(VAR) ...... ────────────────── 4 bytes, 13 clock cycles Became: ...... LD A,15 VAR EQU $-1 ...... ───────────────── 2 bytes, 7 clock cycles As we can see, in this case it is possible to reduce both the size of the program, and the time it takes to complete it. The table below shows what is the gain depending on the register/register pairs where data is loaded. Please note that the Z80 does not have boot commands bytes from memory to registers B, C, D, E, H, L and index halves registers Because of this, the program has to, for example, first load a byte from memory into the accumulator, and then send it value into the required register (sometimes you also have tostore the accumulator value on the stack and then restore). Direct loading in these cases gives special advantage. ╔═══════════════════════╤═══════ ═══════════════════╤═══════════╗ ║ Direct │ Loading from memory │ Saving ║ ║ loading │ │ ╟ ╟───────────┬─────┬─────┼─────── ───────┬─────┬─────┼─────┬─────╢ ║ team │ L │ T │ team │ L │ T │ L │ T ║ ╠═══════════╪═════╪═════╪═══════ ═══════╪═════╪═════╪═════╪═════╣ ║ LD A,N │ 2 │ 7 │ LD A,(addr) │3(+1)│ 13 │ 2 │ 6 ║ ╟───────────┼─────┼─────┼─────── ───────┴─────┴─────┼─────┼─────╢ ║ LD B,N │ │ │ │ │ ║ ║ LD C,N │ │ │ │ │ ║ ║ LD D,N │ 2 │ 7 │ no │ >2 │ >6 ║ ║ LD E,N │ │ │ │ │ ║ ║ LD H,N │ │ │ │ │ ║ ║ LD L,N │ │ │ │ │ ║ ╟───────────┼─────┼─────┼─────── ───────────────────┼─────┼─────╢ ║ LD XH,N │ │ │ │ │ ║ ║ LD XL,N │ 3 │ 11 │ no │ >2 │ >6 ║ ║ LD YH,N │ │ │ │ │ ║ ║ LD YL,N │ │ │ │ │ ║╟───────────┼─────┼─────┼──────────────┬─────┬─────┼─────┼─────╢ ║ LD HL,NN │ 3 │ 10 │ LD HL,(addr) │3(+2)│ 16 │ 2 │ 6 ║ ╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢ ║ LD DE,NN │ 3 │ 10 │ LD DE,(addr) │4(+2)│ 20 │ 3 │ 10 ║ ║ LD BC,NN │ │ │ LD BC,(addr) │ │ │ │ ║ ╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢ ║ LD IX,NN │ 4 │ 14 │ LD IX,(addr) │4(+2)│ 20 │ 2 │ 6 ║ ║ LD IY,NN │ │ │ LD IY,(addr) │ │ │ │ ║ ╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢ ║ LD SP,NN │ 3 │ 10 │ LD SP,(addr) │4(+2)│ 20 │ 3 │ 10 ║ ╚═══════════╧═════╧═════╧══════════════╧═════╧═════╧═════╧═════╝ Таблица 1. Сравнение команд загрузки. L - длина (в байтах), T - время (в тактах). В столбце "Загрузка из памяти" длина указывается в виде "X(+Y)", где X - длина команды, а Y - длина загружаемых данных. Экономия указывается с учетом длины данных. Примеры применения этого метода в intro вы можете увидеть в строках 465-466, 478-479, 551-552, 598-600, 602-603, 613-614, 621-623, 641-642 и 650-651. Если загрузка какой-либо переменной из памяти осуществляется в нескольких местах программы, то необходимо выбрать, в каком именно месте она будет заменена на непосредственную загрузку. При выборе следует руководствоваться тем, чтобы экономия былаmaximum. Let's say, if from several possible replacement places one is inside the cycle (and otherwise they are equivalent), then you need to choose it, because with a large number of repetitions cycle, the time gain will also be large. Here is an example from the intro: (1) 448 LD HL,(FREQ_A) 449 LD (FREQ_B),HL ............................... (2) 598 LD DE,9*256+(freq_g4-freq_shift) 599 FREQ_A EQU $-2 ;E 600 A_AMP EQU $-1 ;D Why is direct loading used in (2), and not in (1)? Because in (2) the loading is performed into the register pair DE, and in (1) - in HL. And, judging by the table data, the gain will be greatest when replacing the load in DE (3 bytes/10 clock cycles versus 2 bytes/6 clock cycles). ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_8 .W ══════════════════ Placing Variables Directly in Load Commands ──────────────────────────── ───────────────────────────── The meaning of this widely used optimization technique is as follows: instead of allocating separate memory cells for storage one- and two-byte variables and then loading them from memory into a register or register pair, variables are allocated directly in the boot commands. Here is a typical example: Was: VAR DB 15 ...... LD A,(VAR) ...... ────────────────── 4 bytes, 13 clock cycles Became: ...... LD A,15 VAR EQU $-1 ...... ───────────────── 2 bytes, 7 clock cycles As we can see, in this case it is possible to reduce both the size of the program, and the time it takes to complete it. The table below shows what is the gain depending on the register/register pairs where data is loaded. Please note that the Z80 does not have boot commands bytes from memory to registers B, C, D, E, H, L and index halves registers Because of this, the program has to, for example, first load a byte from memory into the accumulator, and then send it value into the required register (sometimes you also have tostore the accumulator value on the stack and then restore). Direct loading in these cases gives special advantage. ╔═══════════════════════╤═══════ ═══════════════════╤═══════════╗ ║ Direct │ Loading from memory │ Saving ║ ║ loading │ │ ╟ ╟───────────┬─────┬─────┼─────── ───────┬─────┬─────┼─────┬─────╢ ║ team │ L │ T │ team │ L │ T │ L │ T ║ ╠═══════════╪═════╪═════╪═══════ ═══════╪═════╪═════╪═════╪═════╣ ║ LD A,N │ 2 │ 7 │ LD A,(addr) │3(+1)│ 13 │ 2 │ 6 ║ ╟───────────┼─────┼─────┼─────── ───────┴─────┴─────┼─────┼─────╢ ║ LD B,N │ │ │ │ │ ║ ║ LD C,N │ │ │ │ │ ║ ║ LD D,N │ 2 │ 7 │ no │ >2 │ >6 ║ ║ LD E,N │ │ │ │ │ ║ ║ LD H,N │ │ │ │ │ ║ ║ LD L,N │ │ │ │ │ ║ ╟───────────┼─────┼─────┼─────── ───────────────────┼─────┼─────╢ ║ LD XH,N │ │ │ │ │ ║ ║ LD XL,N │ 3 │ 11 │ no │ >2 │ >6 ║ ║ LD YH,N │ │ │ │ │ ║ ║ LD YL,N │ │ │ │ │ ║╟───────────┼─────┼─────┼──────────────┬─────┬─────┼─────┼─────╢ ║ LD HL,NN │ 3 │ 10 │ LD HL,(addr) │3(+2)│ 16 │ 2 │ 6 ║ ╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢ ║ LD DE,NN │ 3 │ 10 │ LD DE,(addr) │4(+2)│ 20 │ 3 │ 10 ║ ║ LD BC,NN │ │ │ LD BC,(addr) │ │ │ │ ║ ╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢ ║ LD IX,NN │ 4 │ 14 │ LD IX,(addr) │4(+2)│ 20 │ 2 │ 6 ║ ║ LD IY,NN │ │ │ LD IY,(addr) │ │ │ │ ║ ╟───────────┼─────┼─────┼──────────────┼─────┼─────┼─────┼─────╢ ║ LD SP,NN │ 3 │ 10 │ LD SP,(addr) │4(+2)│ 20 │ 3 │ 10 ║ ╚═══════════╧═════╧═════╧══════════════╧═════╧═════╧═════╧═════╝ Таблица 1. Сравнение команд загрузки. L - длина (в байтах), T - время (в тактах). В столбце "Загрузка из памяти" длина указывается в виде "X(+Y)", где X - длина команды, а Y - длина загружаемых данных. Экономия указывается с учетом длины данных. Примеры применения этого метода в intro вы можете увидеть в строках 465-466, 478-479, 551-552, 598-600, 602-603, 613-614, 621-623, 641-642 и 650-651. Если загрузка какой-либо переменной из памяти осуществляется в нескольких местах программы, то необходимо выбрать, в каком именно месте она будет заменена на непосредственную загрузку. При выборе следует руководствоваться тем, чтобы экономия былаmaximum. Let's say, if from several possible replacement places one is inside the cycle (and otherwise they are equivalent), then you need to choose it, because with a large number of repetitions cycle, the time gain will also be large. Here is an example from the intro: (1) 448 LD HL,(FREQ_A) 449 LD (FREQ_B),HL ............................... (2) 598 LD DE,9*256+(freq_g4-freq_shift) 599 FREQ_A EQU $-2 ;E 600 A_AMP EQU $-1 ;D Why is direct loading used in (2), and not in (1)? Because in (2) the loading is performed into the register pair DE, and in (1) - in HL. And, judging by the table data, the gain will be greatest when replacing the load in DE (3 bytes/10 clock cycles versus 2 bytes/6 clock cycles). ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_9 .W ══════════════════ Building tables ───────────────── If the program uses any tables, then in many cases its length can be reduced due to the following simple trick: do not store tables in the body of the program, but shape them before use. The winnings come from that the program fragment that builds the table turns out to be shorter than the table itself. Of course, we must take into account that the construction of the table it takes some time, sometimes quite considerable. However, tables are usually formed only once, at the very beginning of the program, so it doesn’t have that much great value. Here's how this technique is used in the intro. Sine table (more precisely, table of function values y=[(1+sin (2*Pi*x/255))*127]) 256 bytes long is created using 32-byte program fragment (lines 145-180). We get the gain is 224 bytes - however, about 12 are spent on construction seconds There are, however, faster ones (but also more long) ways to construct a sine table - if interested, I recommend that you read [2] (section “Trigonometry”). The PALETTE table, which contains the attribute values, also takes 256 bytes. And here is a fragment of the program that builds it (lines 182-197) takes only 14 bytes, plus 12 bytes for data(lines 274-285) - a total of 26 bytes. How easy it is to calculate the gain is 230 bytes. Table with information about what is used during playback sample music... Here the situation is more interesting. If you look at the structure of the sample (Fig. 2), you will see that it can be divided into two parts - the beginning and the looping section. Yes here, the beginning (the first four elements) is stored in the body programs (lines 826-829). And the continuation of the sample is completed in lines 75-116 (25 bytes are spent on this). As a result, the total costs memory is 29 bytes, and the resulting table SAMPLE occupies 77 bytes. As you can see, 48 bytes are possible save. Using undocumented processor capabilities ──────────────────────────── ───────────────────────────── In lines 221,223,230,245 you can see undocumented commands for working with junior and senior bytes of the 16-bit register IX as with separate 8-bit registers XL and XH. They are convenient to use when storing Variables lack regular registers. You can do the same work with halves of register IY, however, this is not possible in intro managed to find applications. Although teams working with halves index registers, one byte longer and 4 clock cycles slower compared to commands working with conventional 8-bit registers (due to the prefix byte), their use is stillmore beneficial than storing variables in memory. And, between by the way, in later modifications of the Z80 processor (Z380 and etc.) these commands are no longer undocumented. In lines 666-677 (writing values to AY registers) for Checking the condition for exiting the loop is used in an undocumented manner. A special feature of the OUTD command: after its execution, the carry flag is set if the value of register H has changed and at the same time the value output to the port is not zero; otherwise flag reset. Actually, in the process of writing the intro I discovered this feature - those interested can read about this in [3]. The cycle is constructed in such a way that writing to the AY registers occurs in in reverse order - from R10 to R0. After writing to R0 the value of HL decreases from #8000 to #7FFF. So that the carry flag turned out to be installed (and this serves as a condition for exiting the cycle), It is necessary that the value displayed in R0 is not equal to zero. This value represents the low byte of the frequency divider in channel A. Throughout the entire melody, this byte is never becomes equal to zero - this happened, in general, by accident. If it suddenly turned out that at some moments this byte resets to zero, then you would have to change the melody - say, by selecting a different tone. When using undocumented features, The question is how will all this work when running the program underemulator. Commands that work with index halves registers are supported in all emulators known to me, and about the correct setting of flags when executing a command OUTD I have no such confidence. However, as it shows practice, the authors of emulators are constantly improving their creation, and the apparent inoperability of any program only serves as an additional incentive to increase emulation quality. :-) A few more tricks ─────────────────────── The image in the "plasma" effect moves along a trajectory, described by equations of the form x=sin(t), y=sin(t+fi). Such the trajectory is called a Lissajous figure, and its appearance in this case depends on the value fi - phase difference. If fi=0 or Pi, the figure degenerates into a straight line segment; if fi=Pi/2 - turns into a circle, and for other values it is an ellipse (which is exactly what is needed). In the program fi values from 0 to 2*Pi correspond to numbers from 0 to #100. Thus, it is clear that The phase difference can be any, but not a multiple of #40 (i.e. Pi/2). Taking advantage of this freedom, I chose it equal to #AA - this allowed on line 227 instead of the ADD A,N command (2 bytes/7 clock cycles) use the ADD A,H command (1 byte/4 clock cycles). The point is the fact that the H register already contains the number #AA at this point - high byte of the table address TABLE_SIN. That's how it worked shorten the program by another byte.The initial value t (located in the XL register) has influence only on the point from which the movement begins, and the view the trajectory does not depend on it. So I don't set this value and This way I don't waste extra bytes. So if you will run intro at different XL values, then every time you'll see him a little different. :-) ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_9 .W ══════════════════ Building tables ───────────────── If the program uses any tables, then in many cases its length can be reduced due to the following simple trick: do not store tables in the body of the program, but shape them before use. The winnings come from that the program fragment that builds the table turns out to be shorter than the table itself. Of course, we must take into account that the construction of the table it takes some time, sometimes quite considerable. However, tables are usually formed only once, at the very beginning of the program, so it doesn’t have that much great value. Here's how this technique is used in the intro. Sine table (more precisely, table of function values y=[(1+sin (2*Pi*x/255))*127]) 256 bytes long is created using 32-byte program fragment (lines 145-180). We get the gain is 224 bytes - however, about 12 are spent on construction seconds There are, however, faster ones (but also more long) ways to construct a sine table - if interested, I recommend that you read [2] (section “Trigonometry”). The PALETTE table, which contains the attribute values, also takes 256 bytes. And here is a fragment of the program that builds it (lines 182-197) takes only 14 bytes, plus 12 bytes for data(lines 274-285) - a total of 26 bytes. How easy it is to calculate the gain is 230 bytes. Table with information about what is used during playback sample music... Here the situation is more interesting. If you look at the structure of the sample (Fig. 2), you will see that it can be divided into two parts - the beginning and the looping section. Yes here, the beginning (the first four elements) is stored in the body programs (lines 826-829). And the continuation of the sample is completed in lines 75-116 (25 bytes are spent on this). As a result, the total costs memory is 29 bytes, and the resulting table SAMPLE occupies 77 bytes. As you can see, 48 bytes are possible save. Using undocumented processor capabilities ──────────────────────────── ───────────────────────────── In lines 221,223,230,245 you can see undocumented commands for working with junior and senior bytes of the 16-bit register IX as with separate 8-bit registers XL and XH. They are convenient to use when storing Variables lack regular registers. You can do the same work with halves of register IY, however, this is not possible in intro managed to find applications. Although teams working with halves index registers, one byte longer and 4 clock cycles slower compared to commands working with conventional 8-bit registers (due to the prefix byte), their use is stillmore beneficial than storing variables in memory. And, between by the way, in later modifications of the Z80 processor (Z380 and etc.) these commands are no longer undocumented. In lines 666-677 (writing values to AY registers) for Checking the condition for exiting the loop is used in an undocumented manner. A special feature of the OUTD command: after its execution, the carry flag is set if the value of register H has changed and at the same time the value output to the port is not zero; otherwise flag reset. Actually, in the process of writing the intro I discovered this feature - those interested can read about this in [3]. The cycle is constructed in such a way that writing to the AY registers occurs in in reverse order - from R10 to R0. After writing to R0 the value of HL decreases from #8000 to #7FFF. So that the carry flag turned out to be installed (and this serves as a condition for exiting the cycle), It is necessary that the value displayed in R0 is not equal to zero. This value represents the low byte of the frequency divider in channel A. Throughout the entire melody, this byte is never becomes equal to zero - this happened, in general, by accident. If it suddenly turned out that at some moments this byte resets to zero, then you would have to change the melody - say, by selecting a different tone. When using undocumented features, The question is how will all this work when running the program underemulator. Commands that work with index halves registers are supported in all emulators known to me, and about the correct setting of flags when executing a command OUTD I have no such confidence. However, as it shows practice, the authors of emulators are constantly improving their creation, and the apparent inoperability of any program only serves as an additional incentive to increase emulation quality. :-) A few more tricks ─────────────────────── The image in the "plasma" effect moves along a trajectory, described by equations of the form x=sin(t), y=sin(t+fi). Such the trajectory is called a Lissajous figure, and its appearance in this case depends on the value fi - phase difference. If fi=0 or Pi, the figure degenerates into a straight line segment; if fi=Pi/2 - turns into a circle, and for other values it is an ellipse (which is exactly what is needed). In the program fi values from 0 to 2*Pi correspond to numbers from 0 to #100. Thus, it is clear that The phase difference can be any, but not a multiple of #40 (i.e. Pi/2). Taking advantage of this freedom, I chose it equal to #AA - this allowed on line 227 instead of the ADD A,N command (2 bytes/7 clock cycles) use the ADD A,H command (1 byte/4 clock cycles). The point is the fact that the H register already contains the number #AA at this point - high byte of the table address TABLE_SIN. That's how it worked shorten the program by another byte.The initial value t (located in the XL register) has influence only on the point from which the movement begins, and the view the trajectory does not depend on it. So I don't set this value and This way I don't waste extra bytes. So if you will run intro at different XL values, then every time you'll see him a little different. :-) ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_10.W ══════════════════ When playing music, the fact that the number of notes in the pattern (8) is a power of two. This allows you to optimize the program fragment where the increasing the note number and checking to see if the end of the pattern has been reached (lines 465-471). After the note number is increased, instead of comparison commands (CP 8) the AND 7 command is used. Both another command will set the Z flag if after increasing the number became equal to 8, so for now there seems to be no winning in sight. But after the AND command will also reset the battery to zero, and this is exactly what we need, because after the 7th note of one pattern there is 0th note of the next one. That's a win - no special requirement reset the note number. See how a 256-byte table is formed PALETTE. It should consist of 12 equal parts filled values taken from the 12-byte COLORS table. But 256 by 12 is not completely divisible, and the parts are as follows: 11 by 22 bytes and one is 14 bytes long. The filling procedure (lines 191-197) is very simple. DE contains the address of the beginning of the PALETTE table. From (HL) is taken next value and written to (DE) 22 times (after writing each byte E increases). If after recording the next 22 byte reaches the end of the PALETTE table (i.e. E=0), fillingends. If not, then HL is increased and added to the table The next 22 bytes are entered. If you trace this procedure, you will find something far from obvious at first glance. After the table PALETTE will be filled for the first time, the procedure will not end. The table will be filled “in a circle” again and again - until until the last byte of the next 22-byte section matches with the last byte of the table. By this point, how easy it is calculate, 2816 bytes will be written to the table (this is the smallest the common multiple of the numbers 256 and 22), and HL will increase by 2816/22=128. Therefore, the address set in HL before starting filling (see line 184) is less than the address of the beginning of the COLORS table, by 128-12=#74 bytes, so after padding is complete, the PALETTE table will be filled with data from the table COLORS. It would seem, why such difficulties - after all, you can simply add a check for reaching the end of the table in the inner loop. Yes, you can - but it will take two more bytes of memory. By the way, similar tables are used when implementing many graphic effects, so hopefully outlined the principle of their optimal formation will be useful to someone. * * * Well, we didn’t expect that you needed to know so much to write such a small program? :-) Try your hand atoptimizations - shorten the intro by at least a byte without loss functionality! I’ll say right away that this is possible and one of the I know the ways... ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_10.W ══════════════════ When playing music, the fact that the number of notes in the pattern (8) is a power of two. This allows you to optimize the program fragment where the increasing the note number and checking to see if the end of the pattern has been reached (lines 465-471). After the note number is increased, instead of comparison commands (CP 8) the AND 7 command is used. Both another command will set the Z flag if after increasing the number became equal to 8, so for now there seems to be no winning in sight. But after the AND command will also reset the battery to zero, and this is exactly what we need, because after the 7th note of one pattern there is 0th note of the next one. That's a win - no special requirement reset the note number. See how a 256-byte table is formed PALETTE. It should consist of 12 equal parts filled values taken from the 12-byte COLORS table. But 256 by 12 is not completely divisible, and the parts are as follows: 11 by 22 bytes and one is 14 bytes long. The filling procedure (lines 191-197) is very simple. DE contains the address of the beginning of the PALETTE table. From (HL) is taken next value and written to (DE) 22 times (after writing each byte E increases). If after recording the next 22 byte reaches the end of the PALETTE table (i.e. E=0), fillingends. If not, then HL is increased and added to the table The next 22 bytes are entered. If you trace this procedure, you will find something far from obvious at first glance. After the table PALETTE will be filled for the first time, the procedure will not end. The table will be filled “in a circle” again and again - until until the last byte of the next 22-byte section matches with the last byte of the table. By this point, how easy it is calculate, 2816 bytes will be written to the table (this is the smallest the common multiple of the numbers 256 and 22), and HL will increase by 2816/22=128. Therefore, the address set in HL before starting filling (see line 184) is less than the address of the beginning of the COLORS table, by 128-12=#74 bytes, so after padding is complete, the PALETTE table will be filled with data from the table COLORS. It would seem, why such difficulties - after all, you can simply add a check for reaching the end of the table in the inner loop. Yes, you can - but it will take two more bytes of memory. By the way, similar tables are used when implementing many graphic effects, so hopefully outlined the principle of their optimal formation will be useful to someone. * * * Well, we didn’t expect that you needed to know so much to write such a small program? :-) Try your hand atoptimizations - shorten the intro by at least a byte without loss functionality! I’ll say right away that this is possible and one of the I know the ways... ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_11.W ══════════════════ * * * A few words about the intro source text below "Start". The text is provided with detailed comments. Program lines are numbered only for ease of reference. If you need to type a program to compile it and run, then there is no need to specify line numbers. The program is designed to be compiled in ZXASM assembler. If you are using any other assembler, please note attention to the syntax features characteristic of ZXASM. Lines 221, 223, 230, 245 contain commands that work with halves of register pair IX. ZXASM accepts them designations XH and XL. In other assemblers these halves may designated as HX and LX, or not supported at all (after all, this is an undocumented feature of the Z80) - in this In this case, you will have to correct XH to H, XL to L, and before each such command put the prefix byte #DD. For example, the INC XL command on line 221 would look like two commands: DB #DD and INC L. In lines 402-430 and 484 you can see the operator "" - calculating the remainder of division. It is used there to get the low byte of a double-byte value. If in the used your assembler does not support this operator, then perhapsa special operator for calculating the low byte is provided (for example, in ALASM it is "."), or the assembler itself discards high byte, when the result must be single-byte (by the way, and ZXASM has this ability). As a last resort, you can use the equality A256=A-((A/256)*256). 001 ┌─────────────────── ───────────────────┐ 002 │ │ 003 │ "START" │ 004 │ 512 bytes intro │ 005 │ for │ 006 │ Chaos Constructions 000 │ 007 │ │ 008 │ (c) 2000 by Ivan Roshin, Moscow │ 009 │ │ 010 └─────────────────── ───────────────────┘ 011 012 ;Data structures: 013 014 TABLE_SIN EQU #AA00 ;this address 015 ;not chosen by chance- 016 ;but! 017 018 PALETTE EQU TABLE_SIN+#100 ;and this one 019 ;too! 020 021 ;Hardware constants of the video controller: 022 023 black EQU 0 ;color numbers 024 blue EQU 1 025 red EQU 2 026 magenta EQU 3 027 green EQU 4 028 cyan EQU 5 029 yellow EQU 6 030 white EQU 7 031 032 paper EQU 8 ;coefficient multiplication 033 034 ;Example of setting an attribute: 035 ;white*paper+black 036 037 border EQU 254 ;control port038 ;border color 039 040 ;Addresses of procedures for work 041 ;with calculator: 042 043 A_TO_STACK EQU #2D28 044 FROM_STACK EQU #2DD5 045 046 ;Calculator commands used: 047 048 multiply EQU #04 049 add_EQU #0F 050 sin EQU #1F 051 stk_data EQU #34 052 end_calc EQU #38 053 stk_one EQU #A1 054 055 056 ;The program itself went: 057 058 ORG #8001 059 060 ;C #8000 hereinafter the first 11 bytes 061 ;will be used as AY dump. 062; 063 ;Currently the following values are set: 064 ;#8007=%00111000 (AY mixer); actually, 065 ;most significant 2 bits are not important and can be 066 ;any, depending on convenience. 067 ;#800A - right channel - 9 (volume 068 ;ornament). The most significant 3 bits can be 069 ;any, depending on convenience. 070; 071 ;Now this data is “built-in” 072 ;program and are interpreted as 073 ;commands. 074 075 ;Build table sample with #8201 (more precisely, 076 ;we complete it - the first 4 values 077 ;already located with #81FD): 078 079 LD HL,#8201 080 LD BC,#0C40 081 082 ;B - current volume (decreases 083; every 6 ints); 084 ;C - constant #40 used for 085; setting the frequency offset in 086; according to up_2, up_0, dn_2. 087 088 ;At address #8007 we have %01111000 - this is 089 ;mixer value and code at the same time 090 ;required here commands LD A,B: 091════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_11.W ══════════════════ * * * A few words about the intro source text below "Start". The text is provided with detailed comments. Program lines are numbered only for ease of reference. If you need to type a program to compile it and run, then there is no need to specify line numbers. The program is designed to be compiled in ZXASM assembler. If you are using any other assembler, please note attention to the syntax features characteristic of ZXASM. Lines 221, 223, 230, 245 contain commands that work with halves of register pair IX. ZXASM accepts them designations XH and XL. In other assemblers these halves may designated as HX and LX, or not supported at all (after all, this is an undocumented feature of the Z80) - in this In this case, you will have to correct XH to H, XL to L, and before each such command put the prefix byte #DD. For example, the INC XL command on line 221 would look like two commands: DB #DD and INC L. In lines 402-430 and 484 you can see the operator "" - calculating the remainder of division. It is used there to get the low byte of a double-byte value. If in the used your assembler does not support this operator, then perhapsa special operator for calculating the low byte is provided (for example, in ALASM it is "."), or the assembler itself discards high byte, when the result must be single-byte (by the way, and ZXASM has this ability). As a last resort, you can use the equality A256=A-((A/256)*256). 001 ┌─────────────────── ───────────────────┐ 002 │ │ 003 │ "START" │ 004 │ 512 bytes intro │ 005 │ for │ 006 │ Chaos Constructions 000 │ 007 │ │ 008 │ (c) 2000 by Ivan Roshin, Moscow │ 009 │ │ 010 └─────────────────── ───────────────────┘ 011 012 ;Data structures: 013 014 TABLE_SIN EQU #AA00 ;this address 015 ;not chosen by chance- 016 ;but! 017 018 PALETTE EQU TABLE_SIN+#100 ;and this one 019 ;too! 020 021 ;Hardware constants of the video controller: 022 023 black EQU 0 ;color numbers 024 blue EQU 1 025 red EQU 2 026 magenta EQU 3 027 green EQU 4 028 cyan EQU 5 029 yellow EQU 6 030 white EQU 7 031 032 paper EQU 8 ;coefficient multiplication 033 034 ;Example of setting an attribute: 035 ;white*paper+black 036 037 border EQU 254 ;control port038 ;border color 039 040 ;Addresses of procedures for work 041 ;with calculator: 042 043 A_TO_STACK EQU #2D28 044 FROM_STACK EQU #2DD5 045 046 ;Calculator commands used: 047 048 multiply EQU #04 049 add_EQU #0F 050 sin EQU #1F 051 stk_data EQU #34 052 end_calc EQU #38 053 stk_one EQU #A1 054 055 056 ;The program itself went: 057 058 ORG #8001 059 060 ;C #8000 hereinafter the first 11 bytes 061 ;will be used as AY dump. 062; 063 ;Currently the following values are set: 064 ;#8007=%00111000 (AY mixer); actually, 065 ;most significant 2 bits are not important and can be 066 ;any, depending on convenience. 067 ;#800A - right channel - 9 (volume 068 ;ornament). The most significant 3 bits can be 069 ;any, depending on convenience. 070; 071 ;Now this data is “built-in” 072 ;program and are interpreted as 073 ;commands. 074 075 ;Build table sample with #8201 (more precisely, 076 ;we complete it - the first 4 values 077 ;already located with #81FD): 078 079 LD HL,#8201 080 LD BC,#0C40 081 082 ;B - current volume (decreases 083; every 6 ints); 084 ;C - constant #40 used for 085; setting the frequency offset in 086; according to up_2, up_0, dn_2. 087 088 ;At address #8007 we have %01111000 - this is 089 ;mixer value and code at the same time 090 ;required here commands LD A,B: 091════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_13.W ══════════════════ 199 ;PLASMA - register distribution: 200 ; 201 ;DE - address in the attribute file 202; C - y coordinate on screen 203; (the x coordinate is obtained as E AND 31) 204; H - high byte of the sin table address 205; or palettes (differ by 1) 206; L - used to access the table 207; B - x offset 208 ;XH - y offset 209 ;XL - offset in the SIN table (therefore 210; offset takes offsets along x,y), 211; the initial value XL is not important 212 213 PLASMA HALT 214 CALL PLAY 215 216 ;We take x,y offsets from the sin table, 217 ;then the plasma will move along the figure 218 ;Lissajous (in this case, an ellipse): 219 220 LD H,TABLE_SIN/256 221 INC XL 222 223 LD A,XL 224 LD L,A 225 LD B,(HL) 226 227 ADD A,H ;phase shift = #AA 228 LD L,A 229 LD A,(HL) 230 LD XH,A 231 232 LD DE,#5800 233 LD C,24 234 XOR A 235 236 ;sin (2*x+sm_x) + sin (2*y+sm_y) + sm_x 237 238 LOOP ADD A,A ;2*x 239 ADD A,B ;2*x+sm_x 240 LD L,A 241 LD A,(HL) ;sin(2*x+sm_x) 242 EX AF,AF' 243 LD A,C ;y 244 ADD A,A ;2*y 245 ADD A,XH ;2*y+sm_y246 LD L,A 247 EX AF,AF' 248 ADD A,(HL) ;sum of sines 249 ADD A,B ;+sm_x 250 LD L,A 251 252 INC H ;= LD H,PALETTE/256 253 254 LD A,(HL) 255 LD (DE),A 256 INC E 257 LD (DE),A 258 INC DE 259 260 DEC H ;restored H 261 262 LD A,E 263 AND 31 264 JR NZ,LOOP ;loop in x 265 266 DEC C 267 JR NZ,LOOP ;cycle along y 268 269 JR PLASMA ;all over again 270 271 ;--------------------------------------- 272 ;Color set for plasma: 273 274 COLORS DB yellow*paper+yellow 275 DB yellow*paper+red 276 DB red*paper+yellow 277 DB red*paper+red 278 DB red*paper+magenta 279 DB magenta*paper+red 280 DB magenta*paper+magenta 281 DB magenta*paper+green 282 DB green*paper+magenta 283 DB green*paper+green 284 DB green*paper+yellow 285 DB yellow*paper+green 286 287 ;--------------------------------------- 288 ;Control is passed here when 289 ;melody ends: 290 291 END POP AF ;after this A=0! 292 LD (TRIGGER),A ;offset 293 ;for JR=0294 POP AF ;return address 295 296 CALL CLS 297 298 ;Final chord: 299 300 LD B,#4D 301 ACCORD PUSH BC 302 LD DE,freq_e5-freq_shift 303 CALL NE_NOTE_F 304 HALT 305 POP BC 306 DJNZ ACCORD 307 ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_13.W ══════════════════ 199 ;PLASMA - register distribution: 200 ; 201 ;DE - address in the attribute file 202; C - y coordinate on screen 203; (the x coordinate is obtained as E AND 31) 204; H - high byte of the sin table address 205; or palettes (differ by 1) 206; L - used to access the table 207; B - x offset 208 ;XH - y offset 209 ;XL - offset in the SIN table (therefore 210; offset takes offsets along x,y), 211; the initial value XL is not important 212 213 PLASMA HALT 214 CALL PLAY 215 216 ;We take x,y offsets from the sin table, 217 ;then the plasma will move along the figure 218 ;Lissajous (in this case, an ellipse): 219 220 LD H,TABLE_SIN/256 221 INC XL 222 223 LD A,XL 224 LD L,A 225 LD B,(HL) 226 227 ADD A,H ;phase shift = #AA 228 LD L,A 229 LD A,(HL) 230 LD XH,A 231 232 LD DE,#5800 233 LD C,24 234 XOR A 235 236 ;sin (2*x+sm_x) + sin (2*y+sm_y) + sm_x 237 238 LOOP ADD A,A ;2*x 239 ADD A,B ;2*x+sm_x 240 LD L,A 241 LD A,(HL) ;sin(2*x+sm_x) 242 EX AF,AF' 243 LD A,C ;y 244 ADD A,A ;2*y 245 ADD A,XH ;2*y+sm_y246 LD L,A 247 EX AF,AF' 248 ADD A,(HL) ;sum of sines 249 ADD A,B ;+sm_x 250 LD L,A 251 252 INC H ;= LD H,PALETTE/256 253 254 LD A,(HL) 255 LD (DE),A 256 INC E 257 LD (DE),A 258 INC DE 259 260 DEC H ;restored H 261 262 LD A,E 263 AND 31 264 JR NZ,LOOP ;loop in x 265 266 DEC C 267 JR NZ,LOOP ;cycle along y 268 269 JR PLASMA ;all over again 270 271 ;--------------------------------------- 272 ;Plasma color set: 273 274 COLORS DB yellow*paper+yellow 275 DB yellow*paper+red 276 DB red*paper+yellow 277 DB red*paper+red 278 DB red*paper+magenta 279 DB magenta*paper+red 280 DB magenta*paper+magenta 281 DB magenta*paper+green 282 DB green*paper+magenta 283 DB green*paper+green 284 DB green*paper+yellow 285 DB yellow*paper+green 286 287 ;--------------------------------------- 288 ;Control is passed here when 289 ;melody ends: 290 291 END POP AF ;after this A=0! 292 LD (TRIGGER),A ;offset 293 ;for JR=0294 POP AF ;return address 295 296 CALL CLS 297 298 ;Final chord: 299 300 LD B,#4D 301 ACCORD PUSH BC 302 LD DE,freq_e5-freq_shift 303 CALL NE_NOTE_F 304 HALT 305 POP BC 306 DJNZ ACCORD 307 ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_14.W ══════════════════ 308 ;--------------------------------------- 309 ;Screen cleaning procedure: 310 311 CLS XOR A 312 OUT (border),A 313 314 LD HL,#5AFF 315 CLS_1 LD (HL),A 316 DEC L 317 318 ;Expand the loop - the code is 2 longer 319 ;bytes, but when calling CLS after HALT 320 ;attributes will be cleared before they enter 321 ;under beam. 322 323 LD (HL),A 324 DEC HL 325 BIT 3.H 326 JR NZ,CLS_1 327 328 RET 329 330 END_MAIN 331 332 ;--------------------------------------- 333 334 ORG #80C6 335 336 ███ Player 337 338 ▒▒▒ Constants 339 340 tempo EQU 12 ;interval between 341 ;notes in 1/50 s 342 343 length EQU 24 ;melody length 344 ;in patterns 345 346 patt_size EQU 8 ;number of notes 347 ;in pattern 348 349 note_g4 EQU 0 ;note numbers 350 note_b4 EQU 1 ;(not by chance notes 351 note_a4 EQU 2 ;go in this 352 note_c5 EQU 3 ;okay - this is 353 note_e5 EQU 4 ;allows combination 354 note_g5 EQU 5 ;read tables 355 note_b5 EQU 6 ;FREQ_TABLE and 356 note_d5 EQU 7 ;ORNAMENTS) 357 note_f5 EQU 8 358 note_a5 EQU 9 359 360 freq_g4 EQU #114 ;divisor values361 freq_a4 EQU #F8 ;frequencies for 362 freq_b4 EQU #DD ;each 363 freq_c5 EQU #CF ;notes used 364 freq_d5 EQU #B8 ; 365 freq_e5 EQU #A5 ;(used 366 freq_f5 EQU #9B ;pure gamma, and 367 freq_g5 EQU #8A ;not tempered- 368 freq_a5 EQU #7C ;naya) 369 freq_b5 EQU #6E 370 371 freq_shift EQU freq_g4-#C9 372 ;such a meaning, 373 ;so that for each 374 ;dogo freq number 375 ;freq-freq_shift 376 ;was 0..#FF 377 378 code_up EQU #00 ;with these values 379 code_dn EQU #40 ;coded, will 380 code_max EQU #80 ;is the volume in 381 ;pattern raise- 382 ;sya, go down or 383 ;to be maximum 384 385 code_pack EQU #20 ;packed pattern 386 ;(2 notes repeated 387 ;4 times) 388 389 ornament_0 EQU 1 ;offsets used 390 ornament_1 EQU 5; 391 ornament_2 EQU 2 ;ORNAMENTS table 392 ornament_3 EQU 0 393 394 395 ▒▒▒ Variables 396 397 ;Table positions 398; 399 ;The low byte of the beginning of each 400 ;pattern. 401 402 POSITIONS DB PATTERN_0256 403 DB PATTERN_1256 404 DB PATTERN_2256 405 DB PATTERN_3256 406407 DB PATTERN_0256 408 DB PATTERN_1256 409 DB PATTERN_2256 410 DB PATTERN_4256 411 412 DB PATTERN_5256 413 DB PATTERN_6256 414 DB PATTERN_2256 415 DB PATTERN_3256 416 417 DB PATTERN_5256 418 DB PATTERN_7256 419 DB PATTERN_8256 420 DB PATTERN_9256 421 422 DB PATTERN_10256 423 DB PATTERN_11256 424 DB PATTERN_12256 425 DB PATTERN_12256 426 427 DB PATTERN_10256 428 DB PATTERN_11256 429 DB PATTERN_12256 430 DB PATTERN_12256 431 ════════════════════════════════════════════════ С уважением, Иван Рощин.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_15.W ══════════════════ 432 ▒▒▒ Procedures 433 434 PLAY 435 436 ;Checking: we need to move on to the next one 437 ;note? 438 439 LD HL,QUARK 440 DEC (HL) 441 JR NZ,NE_NOTE ;no 442 443 LD (HL),tempo 444 445 ;When moving, we immediately copy the values 446 ;frequency and volume from channel A to B: 447 448 LD HL,(FREQ_A) 449 LD (FREQ_B),HL 450 451 ;And set the position to SAMPLE for 452 ;channel A: 453 454 LD HL,SAMPLE 455 LD (POS_IN_S_A),HL 456 457 ;But this installation is only useful 458 ;at the end... 459 460 LD (POS_IN_S_B),HL 461 462 ;Check: need to move on to the next one 463 ;position? 464 465 LD A,patt_size-1 466 NOTE EQU $-1 467 INC A 468 AND 7 469 PUSH AF ;remembered! 470 LD (NOTE),A 471 JR NZ,NE_POSIT 472 473 ;Move to next position: 474 475 LD D,A ;=LD DE,0 476 LD E,A ;(useful later) 477 478 LD HL,POSITIONS-1 479 POSITION EQU $-2 480 INC HL 481 LD (POSITION),HL 482 483 LD A,L 484 CP PLAY256 485 JR Z,END 486 487 LD L,(HL) 488 INC H ;=LD H,PATTERN_0/256 489 LD A,(HL) 490491 ;In the two most significant bits of the accumulator 492 ;the value characterizing 493 ;change volume in pattern: 494 ;%00 for up, %10 for dn, %01 for max. 495 ;Depending on this value 496 ;set to address CODE_I_D_N 497 ;one of the commands INC (HL), DEC (HL), NOP, 498 ;and also set the current volume 499 ;ALL_AMP 500 501 ADD A,A ;code_max=#80 502 JR C,SET_PAR ;with DE=#0000 503 504 LD DE,#34FF 505 ADD A,A ;code_dn=#40 506 JR C,SET_PAR 507 508 LD DE,#3505 ;code_up=#00 509 510 ;Now in D is what needs to be written down 511 ;in CODE_I_D_N, in E - ALL_AMP. 512 513 SET_PAR LD A,D 514 LD (CODE_I_D_N),A 515 LD A,E 516 LD (A_AMP),A 517 518 ;If the pattern is packed (i.e. in the pattern 519 ;2 notes are repeated), place the code 520 ;NOP (#00), otherwise the code is INC HL (#23): 521 522 BIT 5,(HL) 523 LD A,#23 524 JR Z,SET_COD 525 XOR A 526 SET_COD LD (ADR_CODE),A 527 528 ; Low 5 bits - ornament number 529 530 LD A,(HL) 531 AND %00011111 532 LD (ORNAMENT),A 533 534 ;Set the pattern start address: 535 536 INC HL 537 LD (POS_IN_PAT),HL 538 ════════════════════════ ════════════════════════Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_16.W ══════════════════ 539 ;Processing another NOTE. 540 ;Place a new position indicator 541 ;in sample for channel B (for A there was 542 ;placed earlier): 543 544 NE_POSIT LD HL,SAMPLE+12 545 LD (POS_IN_S_B),HL 546 547 ;Here we will take the older ones one by one 548 ;4 bits or low: if low. NOTE bit 549 ;=0, then high, if 1, then low. 550 551 LD HL,0 552 POS_IN_PAT EQU $-2 553 554 POP AF ;NOTE 555 RRCA 556 LD A,(HL) 557 JR C,NOTE_LOW 558 559 RRCA 560 RRCA 561 RRCA 562 RRCA 563 564 ;Changing the volume: 565 566 LD HL,A_AMP 567 CODE_I_D_N NOP ;or INC (HL), DEC (HL) 568 569 JR NOTE_TO_AY 570 571 NOTE_LOW INC HL ;or NOP for PACKED 572 ADR_CODE EQU $-1 573 LD (POS_IN_PAT),HL 574 575 NOTE_TO_AY AND %00001111 576 ADD A,FREQ_TABLE256 577 LD L,A 578 579 ;FREQ_TABLE/256 is already set in H 580 581 LD A,(HL) 582 LD (FREQ_A),A 583 584 NE_NOTE 585 586 ;Processing channel A (central): 587 588 ;At the very beginning, channel A contains the note g4 589 ;with volume -9 (see below). At the first 590 ;calling PLAY these values will be 591 ;copied for channel B, and there will be 592 ;quiet (amplitude 2->1->0) sound this593 ; note (exactly this one - because further it 594; used there). Actually on the channel 595 ;B at the very beginning there should be silence - 596 ;but it’s easier to do this: 597 598 LD DE,9*256+(freq_g4-freq_shift) 599 FREQ_A EQU $-2 ;E 600 A_AMP EQU $-1 ;D 601 602 NE_NOTE_F LD HL,0 603 POS_IN_S_A EQU $-2 604 LD A,(HL) 605 INC HL 606 LD (POS_IN_S_A),HL 607 608 LD L.8 609 CALL IZM_FRQ 610 611 ;Processing channel B (left): 612 613 LD HL,0 614 POS_IN_S_B EQU $-2 615 LD A,(HL) 616 PUSH AF ;for fin. chord 617 INC HL 618 LD (POS_IN_S_B),HL 619 620 LD L.9 621 LD DE,0 622 FREQ_B EQU $-2 ;E 623 B_AMP EQU $-1 ;D 624 CALL IZM_FRQ 625 POP AF 626 627 ;Processing channel C (right): 628 629 JR C_NORM 630 TRIGGER EQU $-1 631 632 ;At the end of the melody (final chord) 633 ;0 is written to TRIGGER, which means 634 ;go to next command. 635 636 LD L,#A 637 LD E,freq_g5-freq_shift 638 CALL IZM_FRQ 639 JR OUT_AY 640 641 C_NORM LD A,2 ;past 642 SM_IN_ORN EQU $-1 ;offset 643;in orn. 644 INC A 645 CP 3 646 JR NZ,SAVE_SM647 XOR A 648 SAVE_SM LD (SM_IN_ORN),A 649 ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_17.W ══════════════════ 650 ADD A,ornament_0 ;N current orn 651 ORNAMENT EQU $-1 652 ADD A,ORNAMENTS256 653 654 LD H,ORNAMENTS/256 655 LD L,A 656 657 ;HL indicates the value of the divisor 658 ;frequencies. 659 660 LD A,(HL) 661 LD HL,#8004 662 CALL IZM_FRQ_2 663 664 ;Output dump AY: 665 666 OUT_AY LD L,#0A ;H=#80 667 OUT_AY_1 LD BC,#FFFD 668 OUT (C),L 669 LD B,#BF 670 OUTD 671 672 ;After OUTD flag C=0, if the value of H is not 673 ;changed after reduction, and C=1, 674 ;if changed (and if (HL) is not 0). 675 ;You didn’t know? ;-) 676 677 JR NC,OUT_AY_1 678 RET 679 680 ;--------------------------------------- 681 682 IZM_FRQ SUB D 683 LD H,#80 684 LD (HL),A ;volume 685 686 ;Now in register L register number AY 687 ;for the channel amplitude, transform it 688 ;to register number AY for low byte 689 ;frequencies of the same channel: 690 691 SLA L ;*2 692 RES 4,L ;-16 693 694 RLCA 695 RLCA 696 RLCA 697 AND 7 698ADD A,E 699 SUB 2 700 701 IZM_FRQ_2 ADD A,freq_shift 702 703 ;After adding freq_shift to A there will be704 ;low byte of the divider, and in flag C - 705 ;high byte. 706 707 LD (HL),A 708 709 INC L 710 LD A,0 ;get the value 711 ADC A,A ;high byte 712 ;and output it 713 LD (HL),A 714 715; RET (see below) 716 717 ;Frequency table (each note in the table 718 ;corresponds to the value of the frequency divider 719 ;minus the offset freq_shift, i.e. for one 720 ;note one byte is spent, not two). 721; 722 ;The value of freq_shift is chosen as follows 723 ;so that the first byte of the table 724 ;equal to #C9 and served simultaneously 725 ;by the RET command, which reduces the total 726 ;program length. 727; 728 ;freq_shift min = #15 729 ;freq_shift max = #6E 730; 731 ;1st digit in table (g4): m.b. #A6-#FF 732; 733 ;Combined with the ORNAMENTS table. 734; 735 ;Ornaments table (8 bytes) 736; 737 ;Freq values ​​are stored. For use 738 ;you need to know the offset in this table 739 ;tse, then ornament is 3 bytes, from 740 ;read from this offset. 741 742 FREQ_TABLE DB freq_g4-freq_shift 743 DB freq_b4-freq_shift 744 ORNAMENTS DB freq_a4-freq_shift 745 DB freq_c5-freq_shift 746 DB freq_e5-freq_shift 747 DB freq_g5-freq_shift 748 DB freq_b5-freq_shift 749 DB freq_d5-freq_shift 750 DB freq_f5-freq_shift751 DB freq_a5-freq_shift 752 ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ════════════════════ start_18.W 753 ;Table not in every pattern 754; 755 ;1 byte - volume + ornament 756 ; 4 bytes - notes, in bytes 2 notes 757 758 PATTERN_0 DB code_dn+code_pack+ornament_0 759 DB note_c5*16+note_g4 760 761 PATTERN_1 DB code_dn+code_pack+ornament_1 762 DB note_d5*16+note_a4 763 764 PATTERN_2 DB code_dn+code_pack+ornament_0 765 DB note_e5*16+note_b4 766 767 PATTERN_3 DB code_up+code_pack+ornament_0 768 DB note_e5*16+note_b4 769 770 PATTERN_4 DB code_up+ornament_0 771 DB note_e5*16+note_c5 772 DB note_d5*16+note_e5 773 DB note_a5*16+note_g5 774 DB note_f5*16+note_e5 775 776 PATTERN_5 DB code_dn+code_pack+ornament_1 777 DB note_a5*16+note_d5 778 779 PATTERN_6 DB code_max+code_pack+ornament_2 780 DB note_g5*16+note_d5 781 782 PATTERN_7 DB code_dn+code_pack+ornament_2 783 DB note_b5*16+note_e5 784 785 PATTERN_8 DB code_dn+code_pack+ornament_0 786 DB note_f5*16+note_c5 787 788 PATTERN_9 DB code_up+ornament_0 789 DB note_g5*16+note_c5 790 DB note_g5*16+note_c5 791 DB note_a5*16+note_c5 792 DB note_a5*16+note_c5 793 794 PATTERN_10 DB code_max+ornament_1795 DB note_a5*16+note_d5 796 DB note_f5*16+note_d5 797 DB note_a5*16+note_d5 798 DB note_f5*16+note_d5 799 800 PATTERN_11 DB code_max+ornament_3 801 DB note_e5*16+note_a4 802 DB note_c5*16+note_a4 803 DB note_e5*16+note_a4 804 DB note_c5*16+note_a4 805 806 PATTERN_12 DB code_max+ornament_0 807 DB note_g5*16+note_c5 808 DB note_e5*16+note_c5 809 DB note_g5*16+note_c5 810 DB note_e5*16+note_c5 811 812 END_PLAY 813 814 ORG #81FD 815 816 ;Constants added to a value 817 ;amplitudes in sample and indicating 818 ;frequency offset: 819 820 up_2 EQU #80 821 up_0 EQU #40 822 dn_2 EQU #00 823 824 ;First 4 bytes of table sample: 825 826 SAMPLE DB #F+up_0 827 DB #E+up_0 828 DB #D+up_0 829 DB #D+up_0 Literature ────────── 1. I. Roshchin. "Z80: optimization of loading constants into registers." "Radio amateur. Your computer" 9/2000, 2/2001 (under alias BV_Creator). 2. I. Roshchin. "More about programming arithmetic operations." "Radio amateur. Your computer" 12/2000, 1-3/2001. 3. I. Roshchin. "The effect of the OUTD command on the carry flag." "Radio Amateur. Your Computer" 5/2001 (under the pseudonym BV_Creator).════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_12.W ══════════════════ 092 MAKE_SAMPL LD A,B 093 LD (HL),B 094 INC L 095 096 ;At address #800A - #A9 (volume 097 ;ornament in the right channel = 9) and 098 ;simultaneously the code of the command needed here 099 ;XOR C: 100 101 XOR C 102 LD (HL),A 103 INC L 104 LD (HL),A 105 INC L 106ADD A,C 107 LD (HL),A 108 INC L 109 SUB C 110 LD (HL),A 111 INC L 112 LD (HL),A 113 INC L 114 DJNZ MAKE_SAMPL 115 116 LD (HL),B ;=LD (HL),0 117 118 HALT 119 CALL CLS 120 121 ;Draw the mesh (HL=#57FF): 122 123 ;This procedure is specifically written this way, 124 ;so that there is a fragment JR $+3, because the offset 125 ;in JR equal to 1 will be used 126 ;then as data (variable 127 ;QUARK). 128 129 GRID_1 LD D,%10101010 130 BIT 0.H 131 JR Z,GRID_2 132 LD DE,%0001000101000100 133 BIT 1,H 134 JR Z,GRID_2 135 QUARK EQU $-1 ;this byte is 1 136 LD D,E 137 GRID_2 LD (HL),D 138 DEC HL 139 BIT 6,H 140 JR NZ,GRID_1 141 142 ;After drawing the mesh HL=#3FFF, D=#AA, 143 ;A=0 (this is still after CLS)! 144145 ;We form a sin table: 146 147 LD E,A ;DE=#AA00=TABLE_SIN! 148 149 LOOP_SIN PUSH DE 150 151 ;If you use register pair BC 152 ;instead of DE, you can save a byte 153 ;(instead of LD A,C: CALL A_TO_STACK we put 154 ;CALL A_TO_STACK+1). Unfortunately, in this 155 ;program to use BC instead of DE 156 ;it’s impossible (otherwise we’ll lose more later)... 157 158 LD A,E 159 CALL A_TO_STACK 160 161 ;Calculation 162 ;int ((1+sin((2*Pi/255)*COUNTER))*127) 163 164 RST #28 165 DB stk_data ;2*Pi/255 166 DB #EB,#49,#D9,#B4,#56 167 DB multiply 168 DB sin 169 DB stk_one 170 DB add_ 171 DB stk_data ;127 172 DB #40,#B0,#00,#7F 173 DB multiply 174 DB end_calc 175 176 CALL FROM_STACK 177 POP DE 178 LD (DE),A 179 INC E 180 JR NZ,LOOP_SIN 181 182 ;Form the palette (DE=TABLE_SIN!): 183 184 LD HL,COLORS-#74 185 186 ;Offset -#74 is selected specifically. 187 ;And all because 256 is not divisible by 12. 188 189 INC D ;DE:=PALETTE! 190 191 MAKE_PAL_1 LD A,(HL) 192 INC HL 193 LD B,0+(256+11)/12 194 MAKE_PAL_2 LD (DE),A 195 INC E 196 DJNZ MAKE_PAL_2 197 JR NZ,MAKE_PAL_1 198════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 26 January 2002

Hello, All! ═══════════════════ start_12.W ══════════════════ 092 MAKE_SAMPL LD A,B 093 LD (HL),B 094 INC L 095 096 ;At address #800A - #A9 (volume 097 ;ornament in the right channel = 9) and 098 ;simultaneously the code of the command needed here 099 ;XOR C: 100 101 XOR C 102 LD (HL),A 103 INC L 104 LD (HL),A 105 INC L 106ADD A,C 107 LD (HL),A 108 INC L 109 SUB C 110 LD (HL),A 111 INC L 112 LD (HL),A 113 INC L 114 DJNZ MAKE_SAMPL 115 116 LD (HL),B ;=LD (HL),0 117 118 HALT 119 CALL CLS 120 121 ;Draw the mesh (HL=#57FF): 122 123 ;This procedure is specifically written this way, 124 ;so that there is a fragment JR $+3, because the offset 125 ;in JR equal to 1 will be used 126 ;then as data (variable 127 ;QUARK). 128 129 GRID_1 LD D,%10101010 130 BIT 0.H 131 JR Z,GRID_2 132 LD DE,%0001000101000100 133 BIT 1,H 134 JR Z,GRID_2 135 QUARK EQU $-1 ;this byte is 1 136 LD D,E 137 GRID_2 LD (HL),D 138 DEC HL 139 BIT 6,H 140 JR NZ,GRID_1 141 142 ;After drawing the mesh HL=#3FFF, D=#AA, 143 ;A=0 (this is still after CLS)! 144145 ;We form a sin table: 146 147 LD E,A ;DE=#AA00=TABLE_SIN! 148 149 LOOP_SIN PUSH DE 150 151 ;If you use register pair BC 152 ;instead of DE, you can save a byte 153 ;(instead of LD A,C: CALL A_TO_STACK we put 154 ;CALL A_TO_STACK+1). Unfortunately, in this 155 ;program to use BC instead of DE 156 ;it’s impossible (otherwise we’ll lose more later)... 157 158 LD A,E 159 CALL A_TO_STACK 160 161 ;Calculation 162 ;int ((1+sin((2*Pi/255)*COUNTER))*127) 163 164 RST #28 165 DB stk_data ;2*Pi/255 166 DB #EB,#49,#D9,#B4,#56 167 DB multiply 168 DB sin 169 DB stk_one 170 DB add_ 171 DB stk_data ;127 172 DB #40,#B0,#00,#7F 173 DB multiply 174 DB end_calc 175 176 CALL FROM_STACK 177 POP DE 178 LD (DE),A 179 INC E 180 JR NZ,LOOP_SIN 181 182 ;Form the palette (DE=TABLE_SIN!): 183 184 LD HL,COLORS-#74 185 186 ;Offset -#74 is selected specifically. 187 ;And all because 256 is not divisible by 12. 188 189 INC D ;DE:=PALETTE! 190 191 MAKE_PAL_1 LD A,(HL) 192 INC HL 193 LD B,0+(256+11)/12 194 MAKE_PAL_2 LD (DE),A 195 INC E 196 DJNZ MAKE_PAL_2 197 JR NZ,MAKE_PAL_1 198════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.