📜 ⬆️ ⬇️

Game development for NES in C. Chapters 14-16. Working with sound

In this part, basic information about working with sound. The audio subsystem of the NES is very low-level, its description is very complicated and uses specific terminology, so the description in some places may not be very clear.
<<< previous next >>>
image
A source


Getting started with sound


An overview of the tools that the NES platform presents to us. However, we will go further to a higher level and will use the Famitracker library.


The easiest way to feel the sound capabilities of the console is through the Sound Test demo , developed by SnoBrow. It is not compatible with all emulators, but FCEUX is supported.


The select button switches sound channels, Start includes them. 4 channels are available:
1 - meander 1
2 - meander 2
3 - triangular signal
4 - noise


The sound coprocessor (APU) is controlled through the registers $ 4000- $ 4017.


$ 4000- $ 4003 = Meander 1
$ 4004- $ 4007 = Meander 2
$ 4008- $ 400B = Triangle Signal
$ 400C- $ 400F = Channel Noise
$ 4010- $ 4013 = DMC, delta modulated channel
$ 4015 = Channel Management
$ 4017 = Frame Count


Meander 1
The channel is controlled by bits in the $ 4000 register using the DDLC VVVV scheme.


D - duty cycle. 10 - smooth sound, 01 or 11 - tolerable, 00 - nasty
L and C - channel operation modes
V - volume


Channel Modes

L = 0; C = 0:
We get the envelope generator. The volume will be attenuated, and V corresponds to the duration of the sound.


L = 1; C = 0:
Now V adjusts the interval between the repetitions of a note at full volume.


L = 0; C = 1:
V controls the channel volume. The length of the note is governed by the L bits in the $ 4003 register.


L = 1; C = 1:
V also controls the volume, the note plays continuously until the new record in the register $ 4000. Usually the duration of the sound is tied to the frame counter, the transitions are made the same frame-by-frame attenuation of the volume.


The channel is turned off by zeroing the volume - we write 0x30 to the register $ 4000.


$ 4001 - Frequency Swing Register, bits: EPPP NSSS
E - includes effect
P - swing period
N is the direction. 0 - down, 1 - up
S - another period control, but another


If this effect is on, the note is played only until it ends. In combination with L = 1 in the $ 4000 register, interesting effects can be obtained. Bit N affects low frequencies, even when the effect is off. Some games use this hack.


$ 4002 - 8 low bits of the timer that sets the note frequency
$ 4003 - LLLL LTTT - 5 bits of the note duration timer (only works if at least one of the parameters L and C is zeroed in the $ 4000 register) and 3 high bits of the frequency timer. The smaller the duration timer value, the longer the note will sound. For some reason, the frequency of the note is limited to the value 000-00001000. Higher sound - that is, with a shorter period of oscillation - will not work. However, and so it will be nasty squeak.


At the addresses of $ 4004- $ 4007 there are absolutely similar control registers of the second channel.


$ 4008- $ 400B - triangular channel


$ 4008 - CRRR RRRR. C - the flag of the constantly switched on note. R is an incomprehensible register. Theoretically, it should affect the duration of the note - 0xFF for always on and 0x80 for off. But when set to 0x7F here, the duration will be adjusted via the L bits in the $ 400B register.
$ 4009 - not used.
$ 400A - low notes frequency timer bits
$ 400B - LLLL LTTT. 5 bits of duration and 3 high bits of the frequency timer. The logic is the same as the channel meander.
Channel volume is not adjustable. In addition, it plays a sound 1 octave lower than the meander with the same setting. The frequency limit is not here, so you can get a very high squeak.


$ 400C- $ 400F - channel noise
$ 400C - xxLC VVVV - just like the meander, but not the duty cycle.
$ 400D - not used
$ 400E - ZxxxTTTT. Z is the type of noise. 0 gives white noise, and the unit gives a metallic clank. T is the noise frequency timer; the smaller the value, the higher the tone.
$ 400F - LLLL Lxxx. Duration of the note. Bits L and C from the $ 400C register work for both the meander.
To turn off the channel, you need to reset the volume ($ 400C = 0x30)


The DMC channel allows you to play uncompressed sound from memory. The sampling frequency is regulated, and here it is necessary to compromise. The high frequency gives a decent quality, but it requires an indecently large amount of memory to store sound. Low frequency greatly spoils the quality and adds an unpleasant whistle. This tool is good for short sounds such as individual words ("Fight!") Or drum lines.


$ 4010 - ILxx RRRR. I includes channel interrupts, L - sample looping, R - frequency.
$ 4011 - xDDD DDDD - volume. If you pull this register at the right time, then in principle you can get a decent sound quality.
$ 4012 - address of the beginning of the sample. 8 bits are supplemented to 16 like this: 11AA AAAA AA00 0000. So we get the range of available addresses from $ C000 to $ FFC0.
$ 4013 - the duration of the sample in bytes. Also supplemented, but up to 12 bits: LLLL LLLL 0001. We get the allowed size from $ 11 to $ FF1 bytes. If this is not enough, then you can usually hook several samples and lose them in a row.


$ 4015 - xxxDNT21. Includes feeds.
D - DMC
N - noise
T - triangular channel
2 - second meander
1 - the first meander


$ 4017 is theoretically a frame counter, but it is rarely used. Most games write here 0x40 at the start and this is limited.


The DMC turns on when the corresponding bit is written to the $ 4015 register and turns off after the sample is finished. To play it again, you have to pull the bit again.
The remaining channels are turned on by recording the eldest to the address of their registers ($ 4003 for the first meander, etc.). If you write there every frame, then there will be unpleasant clicks.


Some mappers add their sound channels. VRC7 generally has an FM synthesizer, but it is used only in one game - Lagrange Point.


PCM-sound can be used in principle, but I did not have to. This is critically costly both in memory and in processor time - there will be enough resources except for a static splash screen.


Let's make some simple peep. It can be added to any of the demos in previous lessons. Just check that the audio channels are on.


Simplest effects
*((unsigned char*)0x4015) = 0x0f; //  if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){ *((unsigned char*)0x4000) = 0x0f; *((unsigned char*)0x4003) = 0x01; } //    if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){ *((unsigned char*)0x4000) = 0x0f; *((unsigned char*)0x4001) = 0xab; *((unsigned char*)0x4003) = 0x01; } //    if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){ *((unsigned char*)0x400c) = 0x0f; *((unsigned char*)0x400e) = 0x0c; *((unsigned char*)0x400e) = 0x00; } 

When you play around with this code and SoundTest, immediately forget everything and open Famitracker.


Add music


The famous Nerdy Nights tutorial hinted that you should write your own music engine. It turns out that everything is already written - Famitracker and Famitone2.


Famitracker . The latest binary version will do. Nuance: sometimes you need to import a MIDI file, then you have to restrict to version 0.4.2. Import always worked poorly, and after this version it was broken completely.


Famitracker can do everything, but slowly and cumbersomely. So it is better to use Famitone2 .


It has some limitations, but they can be circumvented. There is no volume control, but this can be simulated by creating additional tools. In some places, the envelope volume is less than the maximum - this also costs through other instruments. There are no effects other than tempo change, looping and fading.


... here is a description of the implementation of any musical subtleties that I generally do not know. Original


The main limitation is a smaller range of notes, from C1 to D6. Limits can be tweaked in the assembly source code. Here is a table for recalculating the frequency of the sound into timer counts.


Good tracker video guide:



... I omit the description of the buttons in the tracker.


The main thing is to pack all the tracks in one file. Further, it is converted to assembler:
text2data TestMusic.txt -ca65
And inserted into the initialization code:


 .include "MUSIC/famitone2.s" music_data: .include "MUSIC/TestMusic.s" 

In the initialization code there is a detectNTSC: tag, which allows you to check the console version - NTSC (US / Japan), or the European PAL standard. This is critical because it affects the timings.


We need to add something to reset.s for the Famiton construction of IF:


Hidden text
 FT_BASE_ADR =$0100 ;   ,        .define FT_THREAD 1 .define FT_PAL_SUPPORT 1 .define FT_NTSC_SUPPORT 1 FT_DPCM_OFF = $C000 FT_SFX_STREAMS = 1 .define FT_DPCM_ENABLE 0 .define FT_SFX_ENABLE 0 ;  DPCM  SFx 

Tags for functions in Famitone2.s:


Hidden text
 .export _Reset_Music, _Play_Music, _Music_Update ... _Reset_Music: lda NTSC_MODE ldx #<music_data ;   ldy #>music_data ;   FamiToneInit: ... _Play_Music: FamiToneMusicPlay: ... _Music_Update: FamiToneUpdate: 

Now you can declare the prototypes of the functions and pull them from the code:


 void Reset_Music(void); void __fastcall__ Play_Music(unsigned char song); //   fastcall    ,     void Music_Update(void); 

So now you can turn on the track by calling Reset_Music () and switch to another via Play_Music (1). Once a frame, you must call Music_Update (). You can also import pause or stop functions, but it is easier to do it manually through volume control:
*((unsigned char*)0x4015) = 0;
You can turn off the music by skipping the Music_Update call. To start again, you need to write 0x0F to $ 4015 and call Music_Update again. The noise channel will need to be restarted manually, because Famitone only turns it on when it has a common initialization.


 *((unsigned char*)0x4015) = 0x0f; //   *((unsigned char*)0x400c) = 0x30; //    *((unsigned char*)0x400f) = 0x00; //    

... or score on it and still use the library functions Stop and Play.


Here is the previous demo with music:
Dropbox
Github


Sound effects


Each effect was a separate track in Famitreker, with import into one file.


Restrictions is perhaps the duration of the effects. Each channel must be terminated with the C00 effect, and save everything to 1 NSF file. It should be put in Famitone2 / tools and converted to assembler:
nsf2data SoundFx.nsf -ca65
and turn on the resulting SoundFx.s in reset.s:


 sounds_data: .include “MUSIC/SoundFx.s” 

and then turn on the effects in the engine:
.define FT_SFX_ENABLE 1
You must also add the appropriate labels for import:


 .export _Play_Fx _Play_Fx: ldx #0 FamiToneSfxPlay: 

Only one SoundFx channel is used, so its number is written in X. If there are more channels, then you need to delete this line and turn on the desired channel before calling FamiToneSfxPlay.


The effect is activated by calling the Play_Fx () function, the argument is the channel number, it is also the track number in the NSF file, numbering is from zero.


 void __fastcall__ Play_Fx(unsigned char effect); 

In our demo there are three effects, the first to jump, the second and third to the Up and Down buttons respectively


 if (((joypad1old & UP) == 0) && ((joypad1 & UP) != 0)) Play_Fx(1); 

The Start button switches background tracks.
Dropbox
Github


The tutorial does not cover the topic of DPCM samples. They are cool for effects, although they require a lot of memory in the cartridge. If you really want to, you can import the wav file into Famitracker and convert it to DMC, and then access it from Famitone2. I think someday I will write a section on this topic


')

Source: https://habr.com/ru/post/349742/


All Articles