MoonSynth Development Diary
Mimu / MoonCore
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
After spending some time listening to midi music using my beloved crusty and
dusty Sound Blaster 16, I came to a conclusion. Proper FM synthesized music
beats up lousy wavetable midi music any day. I've heard a few samples of what
a fabled Roland GS synth sounds like, and I was impressed. I myself have also
a genuine MT-32, courtesy of my kind uncle who had little use for it, and by
this too I was impressed. The reverb is dreamy and the sound is half FM and
half wavetable (if I understood the technology correctly). My only gripe is
that the preset Bell sound is horribly offkey, but then for some reason
disturbingly many sound cards have an offkey bell... or something's wrong
with my ears. In fact, recently I've even imagined hearing some detuned
sounds from the Microsoft GS synth...
Sadly the MT-32 can only play 8 channels and the rhythm channel at any one
time, with a maximum polyphony of 32 voices, I think. Not enough for all midi
pieces I have. Also, the MT-32 isn't built following the General Midi
specifications so some instruments are totally wrong when trying to play GM
files. The SB16 likewise has restrictions on the polyphony, although thanks
to the neat Voyetra Super Sapi FM driver the card behaves rather well under
Windows environment otherwise.
Next, I have this Sound Blaster PCI64 card. It was cheap, and it gives nice
and clear wave output under Windows. The midi instruments are bearable too,
although the percussion is too loud. It has legacy emulation for an Ensoniq
Soundscape, and the older SBPro. Ironically the Sound Blaster emulation
sucks. I don't know what kind of chip they have on that thing but the FM
sounds it produces could almost be midi instrument approximations of what an
OPL-chip is supposed to sound like.
The Yamaha XG SoftSynth that came with Final Fantasy 7 produces very
credible sounds. The balances feel good and instruments believable. This
would be my choice for realistic midi music production.
And I've got Timidity++ too, with EAW Patches. A software wavetable midi
synthesizer, using freely choosable GUS patches. The general feel is almost
as good as with the XG synth. However, I'm sort of fond of surreal,
synthesized sounds. Seeing as I'm not aware of any simple midi player that
can produce neat sounds with high polyphony and an oldskool interface, I
figured I'll make one myself.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
While working on this project, I found the following sites which had some
useful or at least interesting information on DirectSound.
If you are in need of programming information on DirectSound, this handful
of links might prove handy.
Streaming Wave Files with DirectSound
the DirectX eXperience
The Enilno project used to have nice tutorials on lots of things including
DirectSound, but those seem to have been taken down.
tom@work ; two Free Pascal programs with sources, demonstrating DirectSound
MicroSoft's own DirectSound stuff... I wish it was more useful.
Exploring DirectX 5.0, DirectSound... kinda like this.
Searching on Google for "windows ddk" gives information on how to do device
drivers... useful for later date.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
First attemps
Rummaged around Win32 API documentation and the internet, found some
information on using waveOutEx for sound output. My first tests finally
resulted in some noise from the speakers and Windows crashing; not the
optimal outcome. Wrote down a scheme on channel management and planned
features. Moved to new projects soon after.
191102, Tuesday
Picked project up again, starting from scratch. The plans should be lying
around somewhere, I may need to clean the room a bit... Searched the net for
more information, decided to use DirectSound this time. Found an example
program for Free Pascal that can play .WAV files using DS. Trying to make
sense of the code and an article about Streaming Wave Files with DS...
201102, Wednesday
Tried not to sleep late, did so anyway. Then continued research and
development powered by a plate of fries.
211102, Thursday
03:00 - Wondering where El is. Where will I scavenge inspiration from when
she's not around? Managed to put together a test program that outputs
random noise at the desired format. Windows hasn't even crashed
a single time! :o
13:00 - After a nourishing breakfast and some relaxation I'm starting on this
again. I'll see if I can get the read/write pointers and display them
now.
14:00 - The thing only returns a 0:0 pair for the pointers. For some reason
now the program won't run at all, claiming a problem in creating the
primary sound buffer. Same thing with secondary buffer. The returned
error code is apparently "Invalid Parameter". I don't think I changed
anything in that part of the code though, to cause this. I did change
the DirectX compile version to 3, from 7. I've only got 4 installed on
this comp anyway so I'm not sure why 7 worked in the first place. Now I
think I'll reboot and see if it works better then. Btw, Winamp also
reports inability to create a sound buffer when trying to use DS
instead of waveOut. The other example program still works like always.
14:20 - Ok, scratch that. Nothing works now. A recompile of the example
program at every DX version gives the same result, an invalid parameter
error. The error Winamp reports is the same. I think I broke something
in this system... and for some reason installing DX5 does nothing at
all, the old drivers remain even after reboots.
14:50 - Hmm, so I had DX6 after all. The version numbers just said 4 so I
thought... anyway, some drivers were from DX5 while others from DX6, so
just to be sure I installed DX7. Now Winamp actually manages to play
using DirectSound, and the example program works too. I set the DX
compile version to 5. My own test program became mysteriously muted,
but at least it doesn't give any errors.
15:00 - Actually it does give an error, I just forgot to check for it. Says
we don't have the priority level needed to use the Play function. :p
17:55 - After an afternoon nap and a warm shower I feel refreshed and ready
to make noise. Except that now El appeared and has had two rough days.
Honor priorities dictate programming must be secondary for now.
221102, Friday
19:00 - Slept through all day, El disappeared as her comp broke down. Not
feeling like programming now, so I'll do some Final Fantasy instead. ;)
19:30 - Or I would, if this comp had a few more MHz in it. Not enough power
for smooth SNES emulation... Fine. As an avid reader of the 8-Bit
Theater I'll go for FF1 instead of 6. :p
21:55 - Saved Mysterious Princess Sara from the clutches of the archnemesis
Garland (you impertinent fools! I, GARLAND, will knock you all down!)
and rescued a town from pirates... that's enough for now. It also
appears that the SNES emulation didn't work properly because of the
sound card, not because of lack of processing power. I guess my SB16
never was intended to be used with DirectX so some modern DX init
algorithms will result in the sound quality getting messed up and the
comp slowing down to a crawl. Apparently creating the primary buffer is
required to prevent this, while for more modern cards the buffer is
already there and ready for use.
051202, Thursday
22:50 - Whoops, forgot about this project for a while. But well, I was doing
another audio project, still not finished. Anyway, I'll try to see if I
can figure out why the secondary buffer play function dislikes my
priorities...
061202, Friday
01:40 - Aha! As I thought. I'm setting the priority level using the wrong
handle. That's what I get for being novice in win32 programming. And I
had the command for getting some sort of usable handle in that other
program I had, but the sources for that are on the other HD which I
can't access now, which is why I did this mistake. Now, I'll see if I
can somehow get the read/write pointers from the buffy...
02:10 - Gave the test program to El, she says it works too. Produces
unbecoming noise in full stereo. I guess I'll see more about this later
on, now I'm feeling too tired.
20:00 - After sleeping way too late and a refreshing bout of WarCraft 2, I'm
feeling like getting into programming again. Let's see then, where was
I this time?
20:10 - There we go. Now I'm getting the pointers, hopefully properly. The
test program shows the play and write pointers of the secondary buffer
when I press enter. I am slightly confused by that if I get the values
a few times with only a short delay in between, the pointers remain the
same, as if no sound was played between the keypresses. I'm supposing
this is due to the messaging nature of the OS, it maybe queues a bunch
of keypresses and processes them only every once in a while, and
possibly the pointers are also updated only along with the primary
buffer a few times per low-level buffer loop, or something. I guess the
next thing to do is to see if I can begin filling the buffy while
playing it, instead of the pre-calculated noise.
071202, Saturday
03:00 - Ok, I have a functioning callback function. Or at least it doesn't
crash the comp. I used the timesetevent Windows multimedia thingy, and
told the system to call my procedure every 250 milliseconds, while the
sound buffer is consistently a second long. I read somewhere that
during a multimedia timer call, calling most functions will result in
a crash. DirectX calls are exempt, likely. I'll see if I can alter
a variable in the tentatively named MIXER function...
03:10 - Whee! I tap enter about once a second and it shows numbers
incrementing by 3..5 each time depending on my personal timing
resolution. :D
03:30 - Wowie, still no crashes. The output is 16-bit stereo at a juicy 44100
frames a second. This means the buffer size is, um... 176400 bytes. The
amount of audio to mix to keep up with the playing is pretty
consistently 49k bytes, occasionally 32k, and at some hiccups the
program gave even 16k and 64k but it evens out nicely. No errors appear
to occur now, and the mixer procedure hasn't been called a single time
if it still was mixing, which means the procedure is sleek and swift.
It only calculates how many bytes of audio need to be mixed so it
figures, really. ;)
03:50 - Weird... I tried to lock the buffy for writing sound to it but the
program gave an access violation error after making some noise. This,
even though I included error checking all along so it should've exited
gracefully in case something went wrong. But now as I try it again, it
seems to work. I'll see if uncommenting the actual noise-writing lines
causes another violation...
04:00 - Hmm. I must be tired. I thought an audio size was an offset. It still
doesn't work any better though.
04:05 - Finally it works properly. I must be really tired. I thought the
pointer dumbly referenced the beginning of the buffer while of course
it pointed directly at the offset I had already specified...
20:00 - Hacked up a preliminary version of the QfG4 finale audio show. I'll
need to redo two or three of the hero's lines and pester El into
recording the Erana lines. And the midi music needs tweaking so it'll
fit in perfectly. At any rate, I think I've done enough work on that
today so now (after another refreshing WarCraft 2 session) I think I'll
see about my cutesy little synth program.
20:40 - Ok, proceeding nicely. A few more lines of code and I think I should
be able to play some monotonous music with square waves...
22:15 - I can now synthesize a square wave at runtime and at varying
frequencies although the melodic ranges aren't defined. However for
some reason the output isn't smooth, there are pops now and then. The
same routine is capable of producing smooth noise though so the problem
must be in my square wave generator code.
23:20 - Yes, I am dumb. Noise is always smooth because it's nothing but pops
and crackling. Anyway, still not sure what's causing the unevenness in
the sound output.
081202, Sunday
01:30 - Can't figure it out now. I guess something might be wrong in the
memory copy routine, will have to test that using a typed array memory
referencing and pull the procedure apart.
261202, Thursday
22:30 - Lack of inspiration. Now I am taking a new look at the program again
though. It appears that the break in the sound happens every time the
secondary directsound buffer loops. I deduced this by changing the
buffy size from 8k to 32k and witnessing a noticeable change in the
audio crackles.
271202, Friday
00:05 - I appear to have also done an estimation error which caused the
intended triangle wave to become a 1Hz wave instead of a few hundred or
so. At any rate, preliminary tests produced clean sound again.
00:40 - Next tests didn't show as positive results. Presetting the buffer
worked fine and gave smooth sound, and cutting the sound seemed
to work perfectly in realtime. Trying to produce a little triangle wave
in realtime using a steady tick counter worked smoothly. However as I
try to generate square waves in a bit more complex loop, the sounds
become as if there were several layers playing. The distortion got
worse as I made the buffer timer polling less frequent, so that's
probably somehow connected... maybe a rollover bug? Changing the
playback frequency and stereo/mono had no discernible effect.
01:10 - Nope, producing anything results in audio artifacts. Still can't
figure out why. No rollovers seem to happen either.
01:15 - In fact I'm beginning to suspect the problem is a DirectSound
compatibility one. There was mention that older sound cards don't work
perfectly with that thing. I'd need to stick in some other card than my
trusty SB16 to make sure this isn't the problem...
13:00 - Ah, now I'm getting somewhere. The program appears to mostly do just
about 300-byte mixes, occasionally getting a 20000-byte mix. The sound
that is being played changes accordingly. Something is clearly wrong
with either the pointers that are processed or my calculations on how
much to mix.
13:20 - Yep. For some reason or another, Directsound happily hogs huge chunks
of the buffy for playback, leaving my program little room to maneuver
in. I still assume it's because of my older soundcard. DS probably uses
a default mixing scheme which means internally using fixed-size double
buffering or something. If I use a buffer size of a second, the
playback becomes garbled. Two seconds gives smooth sound. My timer
routine's been ticking away at 100 milliseconds all the time anyway.
But this really sucks! I mean, two seconds?? We can forget about
playing a synth instrument in realtime on the keyboard for free
improvising... way too much lag for that. I wonder if there's a way to
reduce the mixing size to something below half a second...
13:25 - Latency. It's called playback latency. Ok.
16:00 - And for some reason the getcaps function of the Directsound buffer
interface doesn't want to work, so I can't even get info on the size of
the created buffers. Invalid Parameter, it says, which could mean
absolutely anything is wrong. Also no help was found on the net, the
best relevant advice I could find was, if sound skips or is garbled,
increase the buffer size, which is exactly what I did. Supposedly
Directsound should be capable of 20ms latency, or in the worst case it
should still be able to deliver below 200ms. But it's not actually
about my computer not being fast enough, I think. It's about DS using
too large transfer blocks. If I could tweak it to use smaller blocks, I
certainly could still keep filling the buffer smoothly enough and
everyone'd be happier. *grumble grumble*
16:20 - That was extremely stupid on behalf of whoever made DS. I had to fill
in the size of the DSBCAPS structure myself before asking the getcaps
method to fill in the information... I mean, seriously. That is just
plain idiotic. And the overly helpful MSDN Library resources made no
mention of this whatsoever.
16:30 - Right, I made a tiny mistake. A one second buffer is enough for
smooth audio output. Due to the doubling caused by 16 bits the bitwise
shift left of 1 doesn't make it two seconds. But even a second-long
delay is too much for keyboard improvising. Halving the buffer size
causes problems already, and it'd still be half a second long, which is
still too large. Of course, I'm creating a music synth that primarily
plays midi files so this is alright. I'm just a bit annoyed, since I
like playing tunes myself.
16:50 - Tweaked the buffer size to two thirds of a second, and a 50 ms timed
callback. Seems to be the lowest latency I can get.
17:55 - Upon virtual memory swapping this buffer size may be a bit too small.
Oh well. I painstakingly calculated a bunch of frequencies for free
improvising using the keyboard, used 32-bit fixed point precision and
did hexadecimal conversion on 29 numbers... and they went in the wrong
way around, or something. Didn't sound quite in tune either, in
addition to the keyboard being inverted. I guess next I'll have to come
up with a way to represent note and instrument info... now where were
those original plans of mine?
18:35 - My ingenious plans are forever lost... since they weren't in any of
the more obvious paper stacks, I'll assume it must've been one of those
things I wrote on chocolate bar wrapper paper and threw away somewhere
along. So... let's start over.
::: moonsynth SECRET PLAN rerevision X :::
Since I'm aiming for midi playback, how do I represent the notes? Midi has
note values from C-0:0 to G-10:127. The pitch wheel goes from 0 to 0x03FFF,
with recommended melodic range being from -2 semitones to +2 semitones,
though that's adjustable. Curiously note A-5:69 is the famous 440 Hz.
I guess I'll use a similar system as I did in MoonTracker, only a little
finer. Each playing instrument will be given a single plain frequency and
it's up to the instrument definition to produce something fitting. There will
only be a limited number of frequencies available, and the total number of
different frequencies must fit in 16 bits, though the less frequencies are
allocated, the less memory will be spent on just the precalculated frequency
table. Not that it matters much.
I'll give 32 finetunes per semitone. Internally C-0 will be note 16, and
C#0 will be note 48. I'll calculate a hardcoded table by hand with the
frequencies of one full octave, and at program initialization will use that
table to calculate the other octaves into a full frequency list, with all the
128*32=4096 frequencies. 16 bit precision won't be quite enough, since G-10
is defined as 12543.9 Hz which requires 14 bits, leaving only 2 bits for the
fractional part which isn't enough for the lowest notes... so each frequency
will be stored in 32 bits with the lower 16 bits being the fractional part.
The midi note numbers are easily translated using the equation ...
Internal Note = MIDINOTE * 32 + 16.
There are 16 physical channels. I'll allow up to 128 samples playing at the
same time. Each channel has a pointer to the first sample playing on it, and
each sample has a pointer to the next sample on the same channel. The last
playing sample has value 0xFF. If the channel has nothing playing, it already
points to 0xFF. The chain is easy to change when notes are cut off in the
middle.
Seeking a free sample number for new notes is done by keeping a counter
that goes from 0 to 127, picking the first free sample slot for the new note.
The counter never resets, it only loops. This way there's always an almost
guaranteed free sample slot quickly found for use. If over 128 sounds are
to be playing at once, some sort of advanced routine should be written to
choose one of the channels to cut off.
Each instrument can have, call it, 8 oscillators. Each has its own variables:
Waveform, frequency, amplitude, frame, value, FMO, AMO.
Waveform can be sine, saw, triangle, square, or some other more creative
type that I will precalculate. Also available should be a realtime noise
for which you can also define a delta.
Frequency is the frequency of the oscillator in hertz. The precalculated
waveform will be linearry interpolated to fit this. For the noise, this
value will be the minimum delta for each frame of noise. Deltas below
minimum will be averaged with the given minimum.
Amplitude of the oscillator, 0 to 65536. This allows overamplifying to 2x,
which will result in some clipping.
Frame is the number of frames that have passed while this sample has been
playing. Useful for attack/decay things.
Value is the variable where the value of the oscillator is stored. This is
used for modulating the following oscillators, mixing the oscillators for
the instrument sound, and checking the deltas for noise generation.
FMO points to the oscillator whose value modulates the frequency of this
oscillator. Can only point to an earlier oscillator, so oscillator 1
can't be modulated. Every 32 amp in the oscillator is a semitone's
modulation in the frequency.
AMO points to the oscillator whose value modulates the amplitude of this
oscillator. Can only point to an earlier oscillator, so 1 can't be
modulated. Is a percentage modifier, with -32768 being 0% and 32767 200%.
The instrument has an action string. It comprises of short commands that
define how to modify the variables of the oscillators. The string is in plain
ASCII probably, though I'll need to use a special pointer for it to make it
bigger than 256. 65000 sounds like enough chars for any instrument.
Commands beginning with 1..8 alter the oscillators.
1w* : sets waveform to *.
1f***** : sets base frequency to *****. Internally the number will be
converted to a straight word.
1a***** : sets base amplitude to *****. Internally a straight word.
Other commands are instrument-global.
S : sets the mixing string of the instrument. This is what is used to add
together the desired oscillators into an output stream.
050203, Wednesday
23:00 - Been a while since I wrote here. I haven't just rested on my laurels,
though. I wrote another revision of the moon synth plan and I think
I'll need to write it down coherently here soon enough. I also did more
work on the code, and found out something unsettling. It started nicely
enough... I calculated some frequencies and changed the mixer routine
to work a little simpler. I also made it produce a saw wave instead of
a square wave. Even more importantly, I added the capability to play
multiple channels. Thanks to the frequencies I calculated, I could then
play some simple songs with chords, though the playback latency was
annoying. But this also revealed that even the highly simple routine
that is in place now, takes too much processing time. I don't quite
understand why, since it doesn't even do anything much. But, trying to
play several notes at the same time resulted in sound breaks due to the
mixer not keeping up, and reducing the mixing frequency to 22kHz or
below cured the breaks again. Of course the routine isn't
hand-optimized in assembler yet, but it shouldn't be that heavy even
so... perhaps some memory is swapped back and forth by the operating
system and I need to lock it to reduce overhead? Also, I've been using
a multimedia timer to call the mixer, but that could be changed to
a properly DirectSound-based buffer position notification thingy. I'm
not sure how much it would help but it's worth a try. :)
100203, Monday
00:00 - So, feeling listless and desperate, I dug up the DirectSound
information sources again and tried to make sense of the notification
system. In fact, I managed to incorporate the thing surprisingly
easily. I removed the multimedia timer, and happily also can now remove
the mmsystem unit from the program completely. I also created
a separate thread to run in the background of the actual program, that
handles the position notifications. It does nothing, except sit still
and wait for DirectSound to say one of the magic offsets has been
reached. Then it calls the mixer routine. The amount of blocks the
buffer will be divided into can be set from 1 to 8 currently, although
that could be increased. And what was the result?
00:05 - The result is that the sound still skips as it did before. Now it
just does so less chaotically, since the breaks come at even intervals
thanks to the position notification system. However, and this is very
interesting... when I added one extra line in my main program loop,
that just prints a ! on the screen every time the loop finishes, the
sound became almost perfectly smooth. Removing the line made the sound
choppy again. I do not understand this. Unless it's that accursed CRT
unit... if spending more time in my main loop helps, I could suspect
that the less often one time-vacuum function is called, the more
processing time is left for everything else!
00:10 - I have a line that says... if keypressed then com:=readkey; in my
main loop. Due to the stupid nature of Windows, translating the
keypresses takes a lot more time than the far better implementation
that was available under DOS, namely two simple assembler instructions.
Of course it still seems weird how that could eat up so much time, but
it's the first thing to blame I can think of! And this is easy enough
to test. I'll just comment out the call and add a counter to allow
a timed exit from the program instead of user-invoked.
00:20 - Fine, so that wasn't the culprit. Printing the !'s got almost twice
as fast without the keypressed/readkey combo, but there still was
a tiny break in the sound. Furthermore, removing the !'s kicked the
sound back into sushi slices anyway. Let's see if some debugging output
will shed any light on what is going on...
00:25 - Never mind debugging output for a moment longer. I simplified the
notification handler procedure a bit, which had no effect, and then
gave a try to something that had been in my mind for a while. In my
mixer procedure, you see, I do the following things:
1. Call DirectSound to see which part of the buffer is being played
2. See how far ahead it is from our mixing offset, and if there are
over 16 bytes, then proceed, otherwise break right off.
3a. Loop through a temporary mixing buffer, doing 3b for every frame
3b. Check every channel, and if the channel is playing, generate the
saw wave and mix it into a variable; after all the 128 channels
have been checked, write the variable into the temporary buffer.
4. Lock the actual DirectSound secondary buffer which is played, and
do a memory transfer from the temporary buffer to the real one, also
remembering to wrap around properly, then unlock.
Simple enough, right? There's also a flag that is set when the mixing
routine is crunching, and it creates an overcall error if the routine
is called while the previous call is still being processed. This error
has never occurred despite the choppiness. We can deduce from this that
if overly long is spent in the mixer procedure, then for some reason
the procedure just doesn't get called again even if a notification time
passes.
But output had always been nearly perfectly smooth, until I made one
add, which was step 3a. It still doesn't make sense though. All that
that step actually does, is check 128 times if a "playing" flag is set.
However, making the loop go through only one or two channels again
reduced the chops to the bare minimum. This despite that the actual saw
wave generating and mixing is in both cases done only once.
Besides, if adding a line in the code that actually wasted processing
time for printing exclamation marks made the sound smoother, I'm pretty
sure that processing time isn't the issue here. What it actually is,
then, I have no idea whatsoever. I doubt it's the DirectSound locking
and unlocking calls either, since those are only called a few times
every second. I don't, I just don't get it...
00:45 - Ok, now for debugging information. So to speak. That's just
1337-speak for checking a few variables at runtime, you understand. So
when I speak of checking debugging information, I actually mean
analyzing a mindless string of variable snapshots. With that out of the
way, I can make a brilliant observation from looking at the figures.
The otherwise smooth sound only says a chop when over 30k or so bytes
have been mixed at once. 40k and 50k resulted in a nastier aural
artifact. My buffer is 88200 bytes large, so this means that as long as
the program can keep up and at most only a quarter of the second-long
buffer is mixed, the output will remain smooth. But why is it like
this, since the amount to mix should be almost constant? I'm going to
bed... if this turns out to be an amateur error on my part, I'll
dislike me and punish the mangy critter appropriately.
18:05 - Reformatted this diary thingy a bit, making it prepared for
publishing. I also did a little more testing with the program to see
how the mixer is being called. Every time DirectSound makes
a notification, it calls my short handler procedure. The procedure
writes a symbol on the console screen every time it is called, and it
then calls the mixer procedure to make noise. The mixer procedure then
calculates the amount of audio to generate, and prints the number on
the screen before proceeding to the mixing.
=|=22658=|=33072=|=66144=|=21984=|=33072=|=55120=|=88192=|=77104 ...
The buffer is still 88256 bytes large. The notification points are at
22060, 44120, 66180 and 88240. Therefore the mixer should be called
four times a second, and the mixing size should be a constant about
22060 bytes. Notice also that =|= is never repeated without the mixing
size, so the handler procedure just is not called often enough.
18:20 - Changing the channel amount from 128 to 1 again, to smoothen the
sound, and running the same test resulted in a steady string of calls,
with the mixing sizes being 33k at most. The somewhat varying sizes are
likely explained by the chunk-style input of DirectSound.
18:25 - Changing the channel amount back to 128, and disabling the mixer
calls from the handler, instead calling the mixer in the main loop,
gave the results I'm trying to aim for. Since the main loop rolls
through at the highest speed the computer can muster anyway, this means
the mixer is called as often as possible. The output was a steady
stream of a bunch of zero-length mixes, dotted now and then by a 11024
mix. The sound was perfect. The extra zero-length mixes that were
abundant also suggest that plenty of processing power is not used.
Making the system play as many channels at once as possible, the mixes
jumped to a steady 32k and the sound was broken, but this is already
understandable in terms of processing power. Comparing to my
non-optimized code for MoonTracker, this comp can't play more than
maybe 8 channels simultaneously smoothly. Optimizing the mixer would
probably triple this, or even better.
18:40 - Keeping the channels at 128 and replacing the mixer calls in the
handler, but adding printing the !'s made the sound pretty smooth, but
not perfectly without breaks. The exclamation marks overwrote the
mixing size numbers but from what I managed to see, the mixing seemed
to be around 22k, with the breaks probably occurring if the size went
notably above that.
18:45 - Doubling the notification offset amount from 4 to 8 had little
effect. Also, adding a conditional statement to only print !'s when the
program is not doing mixing resulted in as many breaks as ever. It
appears that if the main loop is printing !'s -while- mixing is
happening, things happen somehow faster and the next notification call
comes on time.
18:50 - Yes, altering the conditional statement to only print !'s when the
mixing is happening in the background restored smoothness. I wonder
what the priority level of the callback procedure thread is...
19:15 - Priority levels are normal. Maybe I could tweak the thread priority
upwards a bit. Also, I notice that the function TerminateThread is
apparently dangerous to use and I should use ExitThread instead. Too
bad that gives an access violation when I try to use it.
19:35 - Now the handler thread which calls the mixer is at "highest" priority
level. It actually helped a bit, the sound got smoother even without
the exclamation marks and at 128 channels. It still did around 40k
mixes on average, which is too much. More disturbing is that the
program has mysteriously started giving access violation errors
randomly upon exiting. I suspect that either printing stuff on screen
from the mixer procedure causes this, or possibly my thread isn't
terminated properly. I suppose the thread should exit first, and
terminate afterwards, and only then release the thread handle. I'll
take a look at this.
19:45 - I removed all the screen writes after the initialization information
ones, and an access violation still occurred after a few tries. Then I
added a line at the shutdown procedure, commanding it to wait until the
"mixing" flag is clear before murdering and burying the handler thread.
Two dozen tries later still no violations. I think what's happening, is
that when the mixer procedure is called, it gets the normal return
address to which it should jump after finishing. The return address is
within the codespace of the handler procedure from where the mixer was
called. Since the thread is terminated, the procedure is also lost, and
trying to jump there to continue program execution is a bad thing,
because there's nothing left there to execute. See? I'm not totally
ignorant anymore! :p
19:50 - Also worth noting... nothing is printed at runtime from any routine,
and now sound playback is almost smooth, even with 128 channels. So I'm
assuming that using writeln functions while I'm supposed to be mixing
is disliked by the system. I know, under GO32 in DOS I couldn't do it
either but I figured Windows might be smarter. Goes to show, never do
positive estimations of Windows. :p
19:55 - I increased the notification point maximum amount to 16 and tried it
out right away. It gave faultless audio. I'm sooo happy. ^_^
20:00 - I also restored the capability to play using the keyboard. Of course
the buffer is a second long so latency is heavy, but it still worked.
Playing a single channel at a time gives good results, three or four
may result in a break, and five or more break often. I suspect that the
mixer calls still don't take place immediately enough. But now I'll go
to sauna, and after that I hope El's back so I can make her test this
thing too. :D
21:20 - Back from sauna, and trying to make El test the program. :)
21:30 - Weird... having the program run in the background makes typing in an
IM window very lagged. In fact it makes all other actions notably
slower. I guess that's what happens due to the higher priority level.
I suppose I'll have to make it normal again, this lag is a bit too
heavy for normal use.
21:40 - Normal priority level again, and other tasks run almost as smoothly
as without the program. The sound breaks up again though.
21:55 - El finished testing. For her the sound apparently worked smoothly all
over, although using the higher priority level version of the program.
No wait... she noticed a break of some sort? *interrogates* Oh, it's
just that the main loop only processes one keystroke at a time and
sometimes even if you press keys simultaneously, some are only
processed later and the chord isn't played properly. I think it's
linked to the sound break-up problem, since the main loop should be
running smoother, instead of going in large pulses like that. The mixer
procedure isn't supposed to eat up so much processing time, and as far
as I can tell it doesn't.
110203, Tuesday
17:00 - Winamp and Modplug player both use second-long DirectSound buffers,
and they manage to play very smoothly. If MP3 decompression can be done
on this comp, as well as playing 8 module channels at under 10% CPU
processing time, I don't see why my mixer could take so much more time.
I think I'll try to disable interrupts for the duration of the mixing,
and see if that helps. Under DOS it would be necessary, under Windows
it may not even be possible...
17:30 - Actually now I made the maximum channel amount a variable, and
setting it to 16 gave smooth sound as it is, unless several notes were
to play at once. I also tried playing at the same time as Modplug
player is playing one of my module songs... it actually worked.
Although Modplug started breaking the sound and the latency of my
program made it only an interesting experience, not to be repeated.
17:55 - Apparently it's not possible to block interrupts as it used to be.
Let's see if another test explains the slowness any...
18:10 - I'm not sure what to say about this. Making the mixer call from the
main loop again and displaying the mixing sizes, disabling the mixer
call from the notification handler, gave about 4 to 6 zero-mixes when I
had 128 maximum channels. Reducing that to 8 resulted in 14-20, that is
triple the amount.
18:25 - Playing 8 channels simultaneously didn't give any trouble, as the
mixer is called from the main loop. The zero-mixes didn't even lessen
noticeably. I'm going to assume a for-loop is extremely slow to use,
and see if it helps to use a repeat loop instead. :D
20:55 - Also, 8 simultaneous channels using notifications still gives no
trouble. Randomly, rarely, a tiny break will occur, but I think that
has to do rather with the OS than my program, since I was only playing
two notes simultaneously at that.
21:10 - Well, I guess any loop is slow. 16 is still ok, 32 starts being too
much. Optimizing will help this plenty, of course. I wonder why it gave
an access violation error again as I tested it...
21:50 - Probably a coincidence, the violation thingy. I wonder what I should
do next. Maybe decide on the final plan for storing and using the note
frequencies, and add in some code to fill out the rest of the frequency
table using my calculations for the single octave...
130203, Thursday
23:25 - Changed the mixing flag setting slightly, I'm hoping this would
remove the access violations that rarely pop up at exit. Also, changed
the frame counter code in the mixer, so that the volume of a sample
fades at the same speed regardless of the mixing frequency. I'll need
to come up with some exact scheme for the timing counter anyway later.
I also keep wondering what causes DirectSound to cough occasionally and
cause a break in the sound, even at light mixing load. I noticed that
changing the mixer thread priority to above normal (+1) doesn't seem to
induce any noticeable performance hit in other running applications,
yet it seems to help a little in keeping the mixer up to speed. I'll
likely make this a user-definable option later. I took a moment to
think of the frequency calculations, and saw that the current scheme is
good and functional certainly on short-length wavetable data, but I'll
need to see what the highest frequency allows for wavetable length. If
not at least a few thousand, I'll probably scale all the frequencies
down by a multiple of two or even four. Right not I'm not really in the
mental state to calculate such things, having work tomorrow and not
having had enough sleep last night. To top it off, my net connection
doesn't work properly.
040303, Tuesday
22:40 - The same state has continued, I don't get enough sleep, and still
spend upwards of 10 hours at work daily. I tried SNES emulation again
yesterday and indeed, Snes9X gave crackly sound no matter what I tried.
However ZSnes played the sound beautiful and clear. Of course, this was
the DOS version, and the program is the best optimized of its kind
around. Trouble is that ZSnes crashes constantly on me so I can't use
it anyway, even though the music was great for the time it was playing.
I suppose I can't go making too many more conclusions about sound
breakups until I've had a chance to try this MoonSynth test program
myself on some other sound card and on a quicker computer. I guess I'll
have a look at the notes now.
050303, Wednesday
00:40 - Yikes, I should be asleep by now. Long day ahead tomorrow. But on the
bright side, I managed to write in a little code that should make
a nice frequency table. Using it requires me to recalculate by hand the
hard-coded values though, since the values currently are from octave 5,
where 440 Hz resides. For ease of coding, and maximal accuracy, I'll
need to make those into octave 10. I needed to calculate the
intermediary values too anyway, so it's not a huge trouble. So, beside
deciding that, I wrote the frequency mapping code in inline assembler.
It's been a while since I've had the pleasure of using that, and it was
very refreshing and fun to write the little program. It compiles, too,
and I typed all the stuff from my memory. I didn't test it yet, since
I'm saying a long good night to El and if it crashes, the good night
wishes would be abruptly broken. But it should work. I hope I'll be up
to testing it tomorrow, and if it works, I'll make the program capable
of playing more than one octave and then finally upload this whole
thing on my site! Of course it's unlikely anyone will give it a try,
though such testing on multiple computer setups would be important...
23:30 - Exhausting day, indeed... and tomorrow won't be any better. I gave
a try to that code I wrote and it didn't work properly. The new system
of playing sounds seems to be efficient though, so I'll use it and
describe it in a moment. I tried out the system, but got only snaps
from the speakers. Checking the converted note frequency values showed
that something's wrong, and calculating one directly outside my tiny
assembler routine gave a proper result, allowing sweet waves to be
heard. I'll revise the asm routine maybe tomorrow.
23:45 - This is how the frequency system works: Final sound output is mixed
from individual voices. Each voice has a single playback rate value,
which is used while mixing to produce a desired frequency. There are
only 32 frequencies per semitone possible, and octaves range from C-0
to B-10. There's a precalculated set of frequencies for the highest
octave 10 (which I haven't done yet). These are simply the real
frequency values in hertz for their respective tones, converted to
hexadecimals and multiplied by 65536. This means I have fixed point
values with 16 bits for the integer and 16 bits for the fraction parts.
At the start of the program, or if mixing frequency is altered on the
fly, these values are used to calculate the lower octaves. Since each
descending octave is precisely at half the frequency of the previous
one, this is easy. Each frequency value is further divided by the
mixing frequency, resulting in what I call playrates. Adding the
playrate to the wave offset for every frame that is mixed will thus
increment the offset by the real tone frequency in Hertz every second.
When setting the playrate for an individual voice, you just look at
which note is to be played and choose the appropriate playrate from the
frequency table. This playrate is finally multiplied by the desired
wavelength. For example, if you have a 256-frame long precalculated
sine wave, you want the playrate to move 256 times as quickly so that
the number of completed wave cycles gives the same frequency. All too
easy! :D Or, if you have trouble understanding it, then wait until I
write a proper technical documentation of the synth. I'll try to
explain this and many other things more understandably then.
060303, Thursday
21:45 - I made a little pascal-language loop to calculate the playrate table
instead of the assembly routine, and it worked just fine. Except, of
course, the sounds are too low. And for some reason, trying to play the
highest E note by pressing the P on the keyboard, resulted in
ridiculously high and out-of-place playrates. Apparently an incorrect
memory reference, as the playrate kept changing now and then.
23:55 - Yes, that was a little mishap. I thought there were 13 notes per
octave, while in fact there are 13 notes -in- an octave, and 12 per.
The note system works like a dream now, and it's possible to play a bit
over two octaves in lagged realtime using the keyboard. I also typed in
a little constant array with something not entirely like a sine wave,
whose 16-bit signed values I typed in freely improvising. I changed the
good old saw wave to the quasi-sine, which has 32 frames for every
cycle, and liked the smooth sound that was produced.
--[Explanation of the quasi-sine! A normal sine wave has its gradient 0
at two places: one quarter, and three quarters. At two and four
quarters the gradient is at its steepest, 1 IIRC. Imagined that?
Now, my quasi-sine is otherwise the same, but it also has
zero-gradients at two and four quarters, giving 1 at the eighths
between the quarters.]
What is left before I'll upload this thing? I'll do the frequency
calculations, at least, and add some keys to change the octave of the
keyboard. I also want to get that inline assembly routine to work. I
might include a few different waveforms you can choose between at
runtime, it'd be really easy. I feel I'm also close to actually getting
to the synthesizing part... I'm burning to try out some frequency
modulation! But first I'll need to test these on the two or three
people who actually visit my site once a month. :p For now, however,
ruuvaantukaa keskenänne, I'm going to bed. ^_^
070303, Friday
21:30 - Right, pizza and lemoncola-powered, starting frequency calculations.
080303, Saturday
13:30 - Last night I couldn't concentrate on the calculations, trying to
comfort a depressed girl. I did notice that it'll take lots of time to
do these, since I only managed to calculate frequencies to about E-.
And once I have all the frequencies calculated, I'll need to go over
them all again and convert them to 16.16 fixed point hexadecimal
values. Let's get working... this'll take an hour or two.
14:50 - Whew, done! 384 hertz values, and apparently they're all correct. Now
for the second phase... entering the values in the correct format...
18:15 - Finished at last! My head is drowning in hexadecimals... Even with
that twenty-minute break I kept in between. I hope I won't have
nightmares of having to do all this again. I'll do a little frequency
test run now, playing through all the frequencies, and checking if it
does sound properly smooth...
19:00 - Awesome! :D At first it didn't sound very smooth at all, but that was
because of the very low quality of my quasi-sine. Then it started
rising, and at the highest end really weird aural artifacts started
showing up. Extremely scifi sounds! I'm supposing this was also due to
the low quality of the quasi-sine. I have only a little time left now,
so I'll try to include final touches and upload this...
090303, Sunday
21:15 - Just went over the source and added some comments. In case someone
takes a look at it, it should be a tiny bit easier to understand now.
Of course I'll keep insisting well-written instructions are much better
to use than trying to make sense of program code some geek's written.
22:00 - Ah, complication. It appears that actual frequencies have only been
defined for parts of 10 octaves in the midi range. Although this table
that I dug up at the net claims that 440 Hz, midi note 69, would be in
fact A-4 instead of A-5 as I said earlier, the fact remains that it's
possible to use midi notes that go one octave lower from what this
table lists as octave 0. So the span is still 11 octaves, with a few
notes not supported at the highest end since 7 bits ran out for the
midi specifications. MoonSynth of course supports the whole octave,
just for kicks. I also noticed my frequencies were being scaled one
octave too low, and I'll still need to fix this whole mess... just
popped by to say hi to you all. :)
22:45 - Ok, I think it's fine now. 11 octaves and correct frequencies. Or,
almost correct. C-0 should be about 8.175, but my calculations have
some roundoff in them, and it becomes 8.075. C-1 should be around 16.35
and mine is 16.15. This gives a 1.2% error, at least for these low
notes. The higher ones seemed to be about at the same error margins. In
fact I'll check this... I want accurate values!
100303, Monday
00:20 - I introduced a simple rounding scheme to the playrate expanding code.
This means that the largest single error can be +/- 0.5 Hz. I think...
I guess it's accurate enough. Now, for some reason my monitor may be
breaking down... it's old already. The screen size is shivering
somewhat sometimes. Currently it's been bouncing continuously for the
last hour or so.
110303, Tuesday
22:35 - Noticed my quasi-sine sounds awful on my headphones, so changed it to
a twice as accurate real sine wave. Still could use better quality, but
sounds pretty good.
23:30 - Added the octave adjusting. I notice that B-3 is about the lowest
note that doesn't start to suffer from the moderately low quality of my
precalculated real sine wave. Also, I can hear C-3, barely B-2, and
just a tiny bit of A#2 or A-2. Below that the sounds go too low to make
out, at least from among the high frequency aural artifacts that
appear. A really high ticking noise. A-2 is supposedly 55 Hz... Funny,
I would've thought I would be able to hear down to A-1. Perhaps
a little linear interpolation would remove the noise...
120303, Wednesday
23:10 - One thing I forgot to mention: I cut the buffer size in half just to
see if the lower channel count would help with that too. In fact, it
did, and half a second long buffer at 44kHz was rolling along smoothly,
even as I was playing a module on Modplug Player in the background and
playing along. This is starting to seem more like it. And the final
versions will have an assembly-language mixer which will improve the
speed even further.
130303, Thursday
00:15 - Ah, rewrote the frequency table calculations assembly bit, and now it
works like it should.
23:59 - Made up a linear interpolation scheme in assembler, to see how the
sine would sound like then. I wrote the whole thing in one go, then
compiled and tested, and was surprised that it almost worked. And I say
almost because it for some reason refused to play any sound when only
one channel was playing, and it never played the first note that I hit
after silence. Instead some pops could be heard at the start of the
silent note. However, any second, third, or so on notes that I played
simultaneously, came out as intended. The quality was notably improved,
since I used 65536x-interpolation. Now I could actually hear, or
perhaps more like -feel-, notes going even to C-2 or so. An interesting
experiment! I'll remove the interpolation code though and rewrite it to
work properly some other time. Now I guess I'll need to start laying
down the synth framework and planning things...
140303, Friday
23:00 - Fixed the assembly thingy, now all the waves play as should. But I'll
still remove and rewrite it later on.
290303, Saturday
22:50 - I've been sick, busy with work, barely gotten any sleep, and been
deprived of Cola and Sugar Power. But during that time I also came up
with a great improvement idea for MoonSynth! Since checking channels if
they're not playing causes unnecessary jumps several times while
mixing, I'll use a linked list for keeping track of which channels have
something playing. Likewise I'll have linked lists for each of the 16
midi channels for easy tracking of which logical channels are affected
by midi commands on each midi channel.
23:30 - Due to the way frequencies are calculated, I've decided to do two
minor changes... firstly, I'll limit the mixing frequency to 8363 Hz at
the lowest. This'll allow for cool lo-fi output, but will still leave
room for the waveforms. Since the frequency is used to calculate the
playrate, and this is done at maximum accuracy, the playing rate needs
to be divided by the mixing frequency first before multiplying it by
the single pulse size of the used waveform. The accuracy is all used up
already so the waveform pulse size can't be much bigger than the mixing
frequency. Limiting the mixing frequency to 8363 Hz allows for a juicy
8192 frames of waveform, which should be enough... of course, square
wave is just two frames and no interpolation. A triangle wave is also
just two frames, but with linear interpolation. Sine and saw waves, and
some more exotic ones, will need to be sampled in more frames.
23:55 - The second change is that I added three more octaves to the lower end
of the range. The reason is that I'm going to need some low-frequency
oscillators for vibratos and stuff, and the three additional octaves
give enough range to drop the lowest notes below one hertz. The
relative accuracy suffers somewhat, but since these are sub-aural
frequencies, it's not such a big deal. This means I've got a grand
total of 14 octaves at my disposal now. :D Now I'll try to implement
that playing channels linked list before I collapse.
300303, Sunday
01:30 - I've got 400 lines of code so far. :)
And now I'm going to test the linked list channel scheme...
01:35 - Well, worked just fine. Except that when I tried to play more
channels than my channel maximum is, and then one of the already
playing ones was reallocated, it messed up my list and caused an
infinite loop in the mixer thread. I'll just need to add the code to
remove a channel from the list into the reallocation bit.
03:05 - I just lost an hour thanks to the daylight savings thing. Anyway, I
added the code and the thing works beautifully. I even can improvise at
250ms latency with only a few sound breaks. This looks better and
better all the time! Now I ponder how to continue... I guess I should
finish doing that rerevision X. After that... Hum. I've got two areas
that I could work on. Either synthesizing... which would mean working
based on rerevision X. Or, starting the separate tracking part, which
keeps track of playing instruments and varies them according to playing
instructions. If I worked on that, then I could get the program to play
basic midi files and would have lots of fun listening to my midi
collection...
310303, Monday
00:50 - I'm kinda stuck with the timing. I'd prefer a scheme that allows
reading and translating midi files with relative ease, but after trying
to understand the midi timing method I've come to the decision that
it's too complex to bother with. I'll just make MoonSynth use
internally a framerate of 1024 frames every second... corresponds
nicely to milliseconds but is easier to divide. Also it means I won't
have to check the tracking system for every mixed frame. Except for the
instrument internal tracking which I'll do once for every two frames.
This thus because I'll try to make the optimized mixer mix two frames
with a single pass... should reduce memory fetches and stuff. I guess
every mixed frame adds 65536 to a dword counter, and every time the
counter surpasses (mixing frequency div 1024) shl 16... the tracking
frame counter is incremented. Each logical channel will also have
a variable telling how many 1024ths the note has been playing. These
are all incremented by one, as well as the global 1024ths counter,
every time the dword counter thingy does its thing, as detailed above.
Wow, I just solved the stupid time thingy! :D I guess this also means
that trying to get midi to play properly would be a serious agony, so
I'll concentrate on the synth part first. ^_^
280503, Wednesday
01:55 - I've been just writing the rerevision X amendments and stuff that you
can see below, and I prefer not to write stuff here because I'm in fact
already writing beyond this point. Anyway, I noticed one serious bug in
my program that was really easily fixed. The high frequencies sounded
horrible, and 8363 Hz output sounded practically useless. I didn't
quite get what the trouble was and figured I'd add in an averaging
scheme for all the sample frames that are skipped hoping it'd help even
out the artifacts. Actually some of the artifacts sounded extremely
cool, all kinds of neat mathematical sounds. So as I was starting to
write this new scheme alongside the linear interpolation hack, I
noticed something weird. The bit of code that was supposed to increase
the integer part of the sine offset counter didn't work right. Instead
of doing the increasing loop every time there was more than $10000 in
the counter, it did the loop only if bit $10000 was set. As you can
imagine, as the playing rate went past double the mixing frequency, the
sound got really weird. Anyway, it's fixed now, and all sound is so
smooth... well, you just have to hear the highest frequencies to get
your ears to bleed. Whatever floats your boat. :D
02:05 - I guess I'll summarise what the SECRET PLAN below has so far...
1) There are general explanations to start with. 2) There is a bit
where I ponder how FM works. 3) Same thing for AM. 4) Waveforms are
considered. 5) Channels variables are suggested, with further thoughts
on implementing MIDI commands. 6) How to do pitch changes and vibratos.
7) Oscillator properties are suggested. 8) A rough schematic of how the
mixer will work, some points explained in more detail. 9) Internal
instrument commands are mentioned. 10) Instrument definition files are
thought over. 11) Details on how to do frequency, volume, and panning
slides follow.
260603, Thursday
21:35 - I got a new motherboard, and a new processor, a P3 ticking a billion
times a second. This was right at the start of this month. Very soon
thereafter I went to visit El and got back several days later. Then
I've just not been in the state of mind to do more stuff with this,
what with having all my cool old stuff to play with. But let's see...
What am I missing before I get back to coding? Ah yes, the mixing and
tracking stuff is pretty well thought out already, but one core thing
is still missing: the oscillator envelopes! There's of course the good
old way of using an Attack/Decay/Sustain/Release envelope but it's too
constricting. I was thinking along the lines of a little interpreted
programming language to give maximal freedom, but I couldn't decide how
to go about it properly.
22:25 - Now I guess I'll use a scheme alike Impulse Tracker's. Hmmm, yes...
160703, Wednesday
00:35 - Yay, new version of Free Pascal is out! :D I'll still need to go over
this SPrrX to incorporate the envelope stuff. It appears I wrote the
whole thing keeping in mind the more complex pseudo-programmable system
and made numerous references to it. Bleh. :p Have I complained yet
about how there are occasional tiny breaks in the sound output caused
by unknown reason, which hopefully wouldn't persist with bigger buffer
sizes, but which effectively puts a damper on soloing? Oh, I have?
Well, let me cry a bit. ... OK that's enough. Actually it may be that
there is some way to make DirectX respond smoother, I accidentally read
about it somewhere on the net but now I've forgotten all about it. *_*
Hey, I was also struggling with the Win console, hoping to get my super
cozy MWRITE routine to work with it, with very limited success. MWrite
is something I did for DOS text mode work, it's able to write a string
straight into the video buffer, but it also parses the string looking
for the ascii char 255 and if finds that, can change the colors of the
text from that point on. Naturally a Win console requires something
more to work. Just now I happened to find what seems to be exactly the
function I need, in the Windows API! WriteConsoleOutput can be used to,
if I understand correctly, copy an area from my own buffer right into
the console's buffer... _outstanding_. ^_^
18:30 - I also think I managed to snag a console output buffy handle all to
myself. This handle is required for manipulating the bugger. And now,
the randy rascal is tied down by virtue of this handle... let the
frolicking begin! >=D
18:50 - It has been done! ^.^ Now I can write stuff on the console window,
having figured out its inner workings for the important part. It's also
curious to note that as I turned off all dumb options for console
output parsing that Win does, Pascal's own writeln function adds two
weird symbols after every line. Yes, CR and LF, as I figured out. I'm
still not sure what characters this WriteConsoleOutput wants me to use,
since according to the Win API help I've acquired, it uses Char_Info
for each character displayed, and Char_Info has some sort of structure
union of a WCHAR for unicode and a plain CHAR for ascii. WCHAR is
a wide 16 bit char, I'd imagine. In addition there's one WORD for the
character color attributes. How come then Pascal's SizeOf function says
Char_Info is four bytes large? But well, as long as it displays what I
expect it to display, I won't complain. Soon I'll have a whole new
MWrite32 available... woot!
21:10 - The output routine of MWriteX is done. I'll convert some of it to asm
later on, and I didn't bother to add the color parsing yet. I'll get
back to that once the audio side is better under control! And now, I'll
go through SECRET PLAN and hopefully will finalize this draft!
200703, Sunday
21:00 - Hurt my ankle somehow, couldn't walk for a day. Was too tired to
finish the revision back then, but have been working on it still. I've
taken some stuff out, such as pitch envelopes, and added other stuff
that midi and FM standards desire, such as sensitivity, scaling both
amplitude and envelope length according to frequency of note, and some
stuff about adding it all up. Get this: Even a midi Note Off command
has velocity data! I'll incorporate it neatly but there's so many
little things to take into consideration... It's terribly hot in here
and I'm sweating like a pig! -- korrektion: pigs do not sweat.
::: moonsynth SECRET PLAN rerevision X amendments and stuff :::
Each oscillator of every instrument needs to have a unique sensitivity to the
actual velocity of the played note. This is how my DX-11 synth does things,
and it's good for adding some realism to sounds, as the sound's dynamics
change depending on how hard you're pressing on your midi key. For example
a hard piano hit will be much sharper than a soft, melodically harmonious
one. This is not to be confused with aftertouch, which I'm not sure how to
implement, if at all. Also there is a midi thing to do something by varying
the pressure while you're holding a key down that is different from
aftertouch, and I might have a look at that later.
Since the various frequency harmonics change their balance depending on the
height of the note played, a scaling scheme also needs to be implemented.
Consider a high and a low piano hit, the high one is purer and clearer, while
the low one has a more complex structure. Therefore an additional velocity
control needs to be added to scale the volume of an oscillator depending on
how high it is played.
The interesting part in synthesizing would go something like this...
Within a single instrument, you have can have a maximum of eight oscillators.
Each has a frequency, an amplitude and a waveform. Primarily an oscillator
should modulate only the oscillators after itself. An oscillator's frequency
or amplitude or both can be modulated.
Each oscillator has two variables, FMO and AMO. Both have 8 bits, referring
to respective oscillators. If osku 7 has FMO 00001010 and AMO 00110000 then
its frequency is modulated by the sum of 2 and 4, and its amplitude by the
sum of 5 and 6. You could then make oskus 2 and 4 be modulated by 1 and 3
respectively. An oscillator can modulate itself as well.
The instrument output comes from an additional Additive byte. 11000000 for
example would add the values from 7 and 8 for the output. Note to self: keep
this in a memory var, when finishing an oscillator ROR it to the appropriate
oscillator slot, AND by 1, NOT, and DEC. Then AND value of oscillator with
this and add to the final output as you go along!
Each oscillator can start either always at the same offset, or at a random
offset. The instrument definitions set this. This should yield some nice
naturality for sounds. Imagine playing a strings chord with a single
instrument, and they all start their vibration identically. It can easily
cause clipping problems if you have loads and loads of such.
Both the frequency and the amplitude of the modulator affect the amount and
volume of the resulting mess of frequencies in the modulated wave. Apparently
the amplitude should be linear, so that an amplitude of 400 will make a wave
playing at 700 Hz vary between 300 and 1100 Hz. It's difficult for me to
understand why it's linear while pretty much all other sound-related things
are logarhitmic, so I will also include a logarithmic modifier. This will of
course use the note playrate table, so that an amplitude of 64 in the
modulating oscillator will vibrate the frequency up and down one semitone.
12 semitones would mean an amplitude of 768 would vibrate a complete octave
away from the center in both directions. The logarhitmic modifier gives the
total variance, not just in one direction. The oscillator frequency needs to
be over 20 Hz or so to actually get FM instead of a vibrato.
Since the note played is just a single frequency, a way is needed to set
an oscillator up nicely to play any frequencies. If the frequency should be
constant, say a 10 Hz vibrato, then it's easy to just set the frequency by
pointing at your choice in the note frequency table. However, often the tone
of the sound needs to change depending on the note, and then the frequency
for an oscillator may need to be, say, half of the main note frequency.
Coincidentally that is exactly one octave lower. For this purpose you can
also define a number of notes to move up or down from the main note, for any
oscillator. There are 32 mininotes to a semitone, and 12 seminotes to drop
one octave, so playing an oscillator at -1 octave would come from -384.
Amplitude Modulation is simpler in some way. The oscillator's amplitude is
used as a divider for the modulated wave's amplitude. Apparently amplitude
modulation can add frequency sidebands like frequency modulation, but I don't
quite see how that works. In any case, the more obvious use for AM is using
it for a volume envelope. The oscillator values are 16-bit, so a value of
32767 multiplies the modulated frame by 32767/32768, which is more or less 1.
AM by -32768 would mean a multiplier of -1. Negative values in fact cause
something called Ring Modulation, but I'll call it AM anyway just to be
annoying. Values from 0 to 1 should be the primarily useful ones anyway.
You can choose a waveform for any of the oscillators. I'll do
a precalculated high-quality sine wave, and a few similar waves. I'll also do
a square wave, which is just two frames large, and a triangle wave, which is
totally identical to the square wave except uses linear interpolation. Likely
you'll get to choose whether you want to use linear interpolation for each
waveform or just use it as it is. You'll also be able to make your own
waveforms, I think, possibly they'll all be stored in an external file... All
waveforms have to have a size in the powers of two for mixing efficiency.
It's also possible to use a high silent wave, just a single frame of 32767.
When used with internal oscillator volume altering, this is great for
an amplitude envelope. Of course you won't have to waste an oscillator for
it, since there will be envelopes for such stuff. Like if you want a volume
vibrato (tremolo), I might add that as an instrument global variable. It
could also be done using a sine oscillator for AM. :)
The waveforms are stored this way... first there is a header, a single word.
The number of frames in the waveform pulse must be a power of two, that is
1 SHL x, where x is from 0 to 13, so the largest waveform is 8192 frames
large. The x number is stored in the first byte of the word. The second byte
contains the desired interpolation for this waveform. If you want to do
a square wave, you don't want any interpolation, so you set this to 0, and
have a two-frame waveform with values -32768 and 32767. A triangle wave uses
the same waveform but with linear interpolation, type 1. In addition to those
I'd like cubic and Differential interpolations as 3 and 7, to be added later.
While mixing, the user can set a maximum interpolation level to avoid her
computer melting. If you're content with linear, set the max level to 1 and
all waveforms desiring better interpolation are reduced to linear. For
horrible sound quality and messed up instruments use 0. If no special
interpolation is needed for the waveform, it should ask for the highest
available, 7.
Then there's the matter of noise. Producing plain white noise would be
quite easy, but sample and hold or something is likely required to produce
anything suitable for sweet percussion. Applying a lagger filter on totally
random noise would also have a lowpass effect, and using a minimum delta
booster would have a highpass effect... I'll get back to this later. I'll
just need to generate noise waveforms in mixtime.
There are 16 midi channels, but all the midi commands given on those are then
mapped into my 128 logical channels. Usually a midi instrument is mapped to
play as a real instrument, but percussion is further divided from the single
midi percussion to a wide set of stuff. Each logical channel has the
following variables:
playing : boolean - One way of tracking if channel is used currently
instrument : word - which instrument is used for synthesizing sound
volume : byte - reference to 0..32768 volumetable having 0..255 entries
volumeto : byte - volume slide target, vol=(volto+(totime-1)*vol)/totime
volumetotime : byte - how many tracking passes left for slide, 0..100
filter : ???
filterenv : envelope - envelope for filtering
pan : byte - 0 is left, 128 middle, 255 right
panenv : envelope - envelope for panning
note : integer - which frequency is the main one to use, 0..5375 available
enote : integer - effective note adjusted by vibrato and pitchdelta
time : dword - the number of 1024ths of a second this channel has been
playing since 'note on' event
noteoff : boolean - set true when the envelope is released and fade starts
faderate : word - 0..512, speed of fadeout after noteoff
pitchdelta : integer - frequency adjustment is stored here, and always used
when calculating the playrate
pitchdeltato : integer - target which pitchdelta approaches by averaging
pitchrange : word - number of mininotes from min to max of pitchdelta
vibradepth : byte - depth of vibrato, midi modulator wheel
vibrarate : dword - vibe freq multiplied by sinewave pulse size as 16.16
vibraofs : dword - high word is integer offset to sinewave vibra location
oskucount : byte - how many oscillators this instrument uses, 1 to 8
oskuaddout : byte - bit-coded, which oskus to mix into output
osku : [1..8] of oscillator - array of oscillator-related variable records
Possible additions... Portamento Time (midi controller 5) for doing automatic
slides from one note to the next, could be done so that the oscillators of
a new note always start at the note of the previous one and get an immediate
sliding command up to its real note.
also Portamento Flag (65) that is true or false to show if portas are in use.
Modulator Wheel (1 and 33) will yield the depth of vibrato.
Balance (8) for limiting the stereo panning controls of an instrument...
Expression (11) for volume adjustments without velocity commands...
Hold Pedal (64) and other pedals around, but these shouldn't be used much...
Effect Level (reverb? 91), tremolo level (92)...
The pitchdelta value is simply the note difference to be added to the
actual playing note. This is calculated from the midi pitch wheel message.
The midi pitch bend range is 16384, 8192 being the center. The default range
is two semitones up and down, which translates to 128 mininotes. Therefore,
midi pitch values are stored in pitchdelta. This is then decremented by 8192
to make it signed, multiplied by the pitchrange, divided by 16384, and the
result is used for calculating the playrate. All changes in the pitch wheel
are placed in the pitchdeltato variable which drags the real pitchdelta
smoothly along.
VIBRATO is applied on the instrument playrate before synthesizing the sound
from the oscillators. To cut corners, every tracking pass the playrate must
be calculated again, with vibrato and pitch figured in. The pitchdelta
variable is easy, just add it to the note straight out. The vibrato requires
reading an amplitude-adjusted value from the sine waveform and adding that
value.
Every time the tracker is called, each instrument's vibraofs is incremented
by its vibrarate, and the offset is ANDed to fit the waveform size. If the
vibradepth variable is 0, this is skipped and only the pitchdelta is used.
Otherwise a temporary note value becomes sinewave[vibraofs] imul vibradepth
idiv 32768. Vibrarate, or the speed of vibration, is a constant 16.16 freq
multiplied by the sine wave pulse size, and should result in maybe 3-7 Hz
vibration. Experiment on this. Vibradepth is controlled by the midi
modulation wheel, and gives the number of mininotes to move away from the
center. 32 would move one octave up and one down. Likely preferrable values
will be around 16. This resulting temporary note value "enote" is given to
the oscillators when synthesizing.
FADING is fun and easy. Once note-off command is given the noteoff flag is
set and then once every 64 tracking frames, 16 times a second, osku[].volume
is multiplied by faderate, and SHR'd by 9. Faderate has a range of 0..512.
If it's 512 then no volume fading happens and the volume envelopes need to
bring the note to an end to prevent it from hanging. (mathematically, going
from 8192 to 1 would require x multiplications... (511 / 512) ^ x * 8192 = 1 ...
x = (1 / 8192)log / (511 / 512)log ... about 4609 muls) Since there will be
only truncated integers, a more likely amount would come from 512 added to
(512/8192)log/(511/512)log, about 1930, yielding at longest a 2-minute fade.
===NOTE: Noteoff also gives a velocity 0..127, 127 being the quickest fadeout
desired. Faderate must be altered somewhat to respond to this, and possibly
every osku's envelope x-scaling should respond as well.
ENVELOPES.
There are envelopes for controlling the amplitude of oscillators, and the
panning and filtering of the whole instrument. Filtering will only be
implemented later on. Juno effects and such can easily be made anyway by
varying modulator amplitude. I won't probably bother to make a pitch envelope
since you can do that by FM from a modulator of a single high sample and
a nice amplitude envelope.
First variable will contain the current integer value of the envelope, and
it is only updated by the tracker 1024 times a second. This can be grabbed
quickly for applying. All envelopes range from 0 to 2048 for efficiency, but
can be shifted to a desired range afterwards.
Each envelope can also have a sort of horizontal scaling factor depending
on the note of its parent channel. xscalea and xscaleb are the variables,
both dwords and ranging 0..1023:65535, 10.16 bit fixed point. The weighted
average of these is counted at noteon and used as envelope.speed, the number
of frames the envelope advances at each tracking pass.
The offset is stored in 16.16 format. It is increased by the speed on every
pass. When env.ofs reaches env.point[env.progress].next, env.ofs is decreased
by the .next amount, and env.progress becomes env.movingto.
Every time env.progress is set to any value, env.movingto is set to that
value + 1. Unless, cloopend or sustend is equal to env.progress, in which
case env.movingto is set to cloopstart or suststart respectively. The Sustain
Loop only works as long as the channel's noteoff is false. Also, if
env.point[env.movingto].height is $FFFF then this channel must be added to
the kill list and volume must be returned as zero.
The envelope value at each pass is updated to ([prog].height * ofs
+ [movingto]height * ([prog].next-ofs) ) div [prog].next.
It is additionally edited for envelope type!
For volume envelopes ... = (osku[].Volume * env.value) SHR 11
For panning envelopes ... = env.value - 1024 ???
For filter envelopes ... = ???
envelope.value : integer - current value is placed here
envelope.progress : byte - which point we are moving on from
envelope.movingto : byte - which point we are moving to
envelope.cloopend : byte - constant loop end point
envelope.cloopstart : byte - constant loop start
envelope.sustend : byte - sustain loop end point, loops until noteoff
envelope.suststart : byte - sustain start
envelope.speed : dword - 10.16 speed, added to offset every tracking pass
envelope.ofs : dword - 16.16 distance from previous envelope point onward
envelope.point : [0..63] of
.height : word - actual envelope point 0..2048, or $FFFF for end of env
.next : dword - high word only definable, tells when next is due
Each logical channel can be playing an instrument, and each instrument can
have up to eight oscillators running simultaneously, combined in various ways
to synthesize the instrument sound. The instrument definitions tell the
amount of oscillators needed, and the number is stored in the channel record.
The oscillator variable record is:
value : integer - calculated frame is placed here for mixing and modulating
value2 : integer - for asm optimizing, two frames in one pass
offset : dword - high word adds to waveform ptr, loop: AND waveformsize
playrate : dword - freq of oscillator * waveform size, simply added to ofs
waveform : pointer - memory location of oscillator's waveform data, +1 word
waveformsize : dword - 1st byte of data has shl bit amount. Unpack here, -1
waveformbitsize : byte - 1st waveform byte is the shl bit amount, 0..13
interpolation : byte - 2nd byte has desired interpolation for waveform
freqconstant : boolean - constant playrate regardless of pitch?
notix : integer - note number, or note number delta
displace : integer - how much waveform is displaced from centre
volume : word - (0..8192), amplitude for waveform frames
volenv : envelope
ampscalea,ampscaleb : byte - volume balance depending on note, 0..255
FMO : byte - which oscillator values are used to alter this one's playrate
AMO : byte - which osku values alter this one's volume; one bit per osku
FMlinear : boolean - linear or logarhitmic frequency modulation
Upon "note on" this is done...
- Pick next free channel, add to midi channel list and plain channel list
- Reset all vars
- Get some starting values from selected instrument definition
- check all oskus: if FMO = 0 and freqconstant = true, calculate playrate
- osku[].volume = (midinotevelocity^2 - 2048383) * sensitivity / 322580 + 127
* 8192 / 127
* ((ampscaleb * note + ampscalea * (5373 - note)) / 5373) / 255
Each tracking pass does this...
3. Process any "note on"
5. Update all envelopes
6. Kill old notes according to linked hit list!
7. Figure out current effective note
| 7a. Update PitchDelta <-- PitchDelta = (PitchDelta + PitchDeltaTo) shr 1
| 7b. Enote = Notix + (PitchDelta - 8192) * PitchRange iDiv 16384
| 7c. If VibraDepth = 0 then skip to f.
| 7d. VibraOfs = (VibraOfs + VibraRate) AND sinewaveform pulse size
| 7e. Enote = Enote + sinewave[VibraOfs] iMul VibraDepth iDiv 32768
| 7f. For every Osku of this channel, if FMO = 0 and freqconstant=false,
| osku.Playrate = (freq[Enote] SHL osku.wformbitsize) SHR 2
`""""""'
Each mixing pass does this...
1. Loop through active channels!
2. Do oscillators, starting from 1, ending at the oskucount one
| 2a. If FMO > 0, recalculate playrate! Else use playrate in memory.
| | :1. If freqconstant is true, Note=Notix, else Note=Enote.
| | :2. Add up the output of [FMO] oscillators into a temp var.
| | :3. If FMlinear is false, Note=Note+FMtemp.
| | :4. Retrieve the Hz frequency of this note, stored as 14.18. The freq
| | has DIV mixfreq factored in at startup.
| | :5. If FMlinear is true, adjust frequency by FMtemp.
| | :6. SHL this freq by waveformbitsize, and SHR by 2.
| `""""""'
| 2b. Increase Offset by Playrate, AND Offset by waveform wordsize
| 2c. Retrieve waveform frame <- WaveForm[Offset], interpolate
| 2d. tempvol = volEnv. If AMO > 0, do AM:
| go through AMO and for every osku chosen do
| tempvol = tempvol iMul osku.value iDiv 32768
| 2e. frame = frame iMul tempvol iDiv 8192
| 2f. frame -> oscillator's Value variable.
`""""""'
3. Add together appropriate oscillators from oskucount[Value] vars.
4. Perform filtering on channel output.
5. Mix channel output into temp variable, or variables if more than mono.
These all are defined along with the instrument definition, a bunch of more
or less human-readable text lines in an ASCII instrument definition file.
Like those cute Angband data files.
The player can choose from several instrument definition files. One should
have chip instruments (chip.msx), another should be an excellent all-out
best-FM-can-offer (mooncore.msx), and perhaps a simpler version of that for
older comps (moonlite.msx). I'd also love an MT-32 approximation definition,
but the LA synthesis may not be doable so this still won't work as an MT-32
emulating softsynth... maybe if I could find out how exactly it's supposed to
work... The definition of an instrument starts with constant or initial
information:
instrument - instrument ID number, maybe some cool name too
pan - default panning for stereo mixing, 0..255
faderate - 0..512, how quick release does instrument have
oskucount - how many oscillators this instrument uses actively, 1 to 8
oskuaddout - which oskus to add up for the output
sensitivity[] - sensitivity to velocity, 0..31, 31 loudest, 1 softest
ampscalea,ampscaleb[] - for refitting high and low note volumes
waveform[] - which waveform to use for each oscillator
offset[] - where each waveform starts, 0 to 8191 or random
displace[] - how much each waveform is displaced from center, -32768..32767
freqconstant[] - should the playrate be altered by mixtime things
notix[] - note number, or note number delta
FMO[], AMO[] - modulations
FMlinear[] - linear or logarhitmic FM for each oscillator
volenv[] - volume envelopes of oscillators
panenv - for panning the instrument
filterenv - for filtering, or something...
All midi commands, as well as envelopes, are processed in the tracking code
which is called 1024 times a second. Once the system is otherwise working,
implementing the midi playing will be relatively easy. Most common midi
messages will be very easy to patch in, only translating the delta times will
present notable headache.
250703, Friday
01:40 - Finished, finished. I've revised that thing to death. Not only that,
I've also written a bunch of formulas and pseudocode for handling all
the important stuff except the most important stuff that I've no doubt
conveniently forgotten. I'll get cola and candy and pizza tomorrow and
start PROGRAMMING. :D
260703, Saturday
17:35 - After a case of wrist pain I've now incorporated the variables into
the code, and bonked out the previous mixer code in favor of a new one
using the oscillators. It doesn't work, naturally. At first I did this
funny error, forgetting to update the channel counter in the tracking
code, so the mixer thread locked up when trying to play a note. After
fixing that, I do get sound but it's too high and doesn't even seem to
get different frequencies depending on the note. Some playrate
calculation mistake, probably.
270703, Sunday
18:50 - Fixed the playrate thing. I forgot to SHL 16 at some point to account
for the fixed point stuff. I also added the kill list, into which it is
easy to enter channels that are to be turned off. For some reason the
sounds seems to be oddly silent though, I'll check to see if I've used
all the volume things correctly.
19:40 - Yes, due to only half-implementing, some stuff was divided too much.
Then I added a skvare waveform and some other stuff like that and
played with it and it sounded nice. ^_^ Except of course the sound gets
broken now and then still. Must be a DirectSound thing... Next I guess
I'll see if I can do preliminary FM.
20:20 - Wooo! The pseudo code works as it is. :D Now I have two oscillators,
the second one is Frequency Modulated by the first. The second is
a square wave, the first is a sine. I toned the sine wave down into
a lovely vibrato and it simply works! Now... I wonder if it would be
possible to come up with some nice values that would give me an FM
sound, even though envelopes aren't in yet...
20:35 - Some FM indeed is possible. I guess I should make another site
update. Soon enough...
310703, Thursday
11:50 - Me sick today, so not at work. Tried to redo the linear interpolation
thing but it's not working too well. Getting the offsets right took
notable effort and now it's at least not making white noise anymore,
but it still doesn't play the sine waves as what they are.
080703, Friday
23:20 - Turns out I've been enjoying a nice wave of mononucleosis. Anyway,
now I changed the waveforms into unsigned format so the asm code is
easier, the linear interpolation almost works finally except there is
a silly crackling in the sine sound. Delving into it.
090703, Saturday
14:45 - Fixed it, I was doing the fraction inverting from 65536 while I was
supposed to do it from 65535. It happens. So far the FM sounds I'm
getting aren't very exciting, just a spacey sound.
15:00 - Hacked together the linear FM in addition to the logarhitmic one, the
linear one seems to give a more interesting sound. Probably should get
to doing the envelopes next thing. But now me hungry!
21:50 - Patched in basic volume envelope code, it seems to work. I can't say
for sure since now as everything is hard-coded and I turned FM off so
that only the second oscillator which I usually use as the carrier is
mixed into the output while the first is pretty much ignored... and for
some reason the normal sine wave it should output sounds very silent.
Again I'm not sure why this is so, but obviously something in the
envelope code needs tweaking.
130903, Saturday
17:45 - Yes, got disinterested for some time, the envelope stuff seeming
unfun and annoying. I gave a few half-hearted tries in understanding it
all before but gave up. Now I went through the thing again and figured
the problem must come from not having enough variable space in the
arithmetic that calculates the envelope value. Since the offsets use
a 16:16 bit setup, and those are used for a multiply and a divide to
get the linear movement from point to point... the calculations require
over a dword, so I'd best do it by hand. And so I wrote some more asm,
with a screenful of code required just for getting the memory addresses
and variables loaded. The arithmetics themselves were simpler, although
I ran out of registers at one point. A division by zero error was fixed
by making sure nothing is calculated once the end of the envelope is
reached, and... imagine my joy as the sine faded in and out smoothly!
Now I guess I should return to integrating a better solution for using
DirectSound... the current one is not acceptable.
300903, Tuesday
23:30 - I read about this interesting mixing idea which I seriously should
have thought of myself. I'll rewrite the mixer to do one playing
channel at a time for the whole desired mix length and mix the channels
one by one, instead of mixing all the channels for every frame. Since
many of the variables do not change within one mixing period, this
would actually give a notable speed increase, I believe. Naturally
doing some asm conversion would do this as well.
211003, Tuesday
23:30 - The previously described idea has some difficulties to it... namely,
taking care of the tracking. Tracking and updates of the envelopes is
currently done 1024 times a second. (this could be reduced, or
increased, technically...) Anyway, since the mixer is called only like
16 times a second currently (already easily changeable), that means the
tracking code needs to be gone through 64 times per each mix, per
channel. The actual tracking work doesn't change notably when this
mixing technique change is committed, but it means the mixer will use
up an extra check and won't be quite as efficient as I dreamed it would
be. Still, once I get around to the ASM conversion, this new model will
have more efficiency than the previous one. Always a good thing. ^_^
221103, Saturday
00:30 - Well, had a house move. Feeling sleepy.
01:00 - Unable to concentrate. Listening to Final Fantasy music.
01:15 - Oh, did I mention? I finally got Final Fantasy 7 and 8 and have been
playing 7 with elation. That, and some other factors, keep adding to my
sleep debt. Anyway, back on topic... since I'll be mixing one channel
at a time, it means...
01:30 - um, it means, that the channels can't actually be "mixed" in that
single pass. I used to have one mixing buffer into which they'd all be
mixed one frame at a time, every channel being looped through to do it.
But now it's supposed to generate a longer string of audio for each
channel and mix those longer strings together. One solution would be to
have a separate buffer for each. *yawns* But that'd be stupid. Since I
have a maximum of 128 channels available if my memory functions, it'd
mean 128 times as much memory used. The buffer for one second would be
44100 frames, which can be 88200 bytes since all is in 16 bits, and if
you want stereo, double that. The current buffer is 22272 bytes large,
don't ask me why though since I can't think at this hour...
01:45 - So er, it's mono, so in stereo having 128 buffers like that would eat
5.7 megs of memory. Not very nice, I'm sure you agree. But actually
it's much simpler to just use a 32-bit buffer instead for the mixing
and mix each frame into the buffer sequentially, channel at a time, and
only do the clipping once in the end. I'm sure sooner or later I'll get
around to coding this thing. I already did some changes, but I forget
what...
231103, Monday
02:45 - Spent some time going through the program, managed to implement the
clipping thing and I think the mixer should be okay now. Except that
the program didn't work. I think I've got byte and frame sizes mixed up
somewhere, it appears that DirectSound does byte sizes while I was
thinking of frames somewhere along... not sure how it worked before.
I'll have to go over the whole code and make sure everything is
correct. Of course, I'll need to get to work in a few hours' time... so
I'll get back to this sometime soon.
291103, Saturday
02:20 - Right, so I've now gone over the code and fixed the most blaring size
mishaps. Now it seems to initialize okay, but the mixer still doesn't
run. I'm starting to be annoyed again as the program seems to enjoy
crashing if I try as much as to run a bit of code from the start of the
mixer and break off then, that just fills the 32-bit mixing buffer with
zeroes, and then hit ESC to quit. I think I've left something
unfinished with the thread stuff and all. *sigh*
02:25 - *blink* Um. *blink, blink* No. *blink* No. No. No.
02:26 - *blink*
02:27 - No.
02:28 - What's wrong with this code: LEA EDI,pointerthingy; MOV [EDI],stuff
02:30 - Indeed. Now it is fixed and crashes are gone, again. That's what one
gets when one stays away from programming for a while and then returns
and fills the programs variable space with a long string of unsigned
doubleword values of hex 8000 0000. For those of you who aren't up to
speed on coding, I'll tell what is wrong with the code above. Pointers
are variables in memory that contain the memory location of something
else. LEA is a command that gives you the variable's memory location.
This is great if you want to set for example Variable X = 1. But with
a pointer you need to read in the memory location, not get the memory
location of the memory location! The correct code would thus start with
MOV EDI,pointerthingy. No, I don't care about C.
02:40 - Right, the whole mixer routine is online again. Needs some debugging
though. I can hear sound again but it's crackly and some parts are
dropped. In addition it crashed again at exit, but that seems to be
random. :p
02:45 - Yay. One trouble was that I wasn't reformatting my 32-bit mixed
material down to 16 bits before copying it to the output buffer. Now
the sound is somehow SIDish, but something's still wrong.
02:55 - Perfection is attained. ^_^ While switching over to this new method,
I forgot to reset the temporary mixing variable, resulting in a lot of
feedback to the sound. Actually it sounded kind of neat. ;)
181203, Thursday
00:00 - Upon further playing around I detected an annoying scratchiness in
the sound. I wrote in a bit of code that outputs the sound to a raw
binary file, and took a look at it. While the envelope works perfectly
otherwise, as in a totally smooth rise from 0 to maximum amplitude in
a quarter second, then there's also two half seconds where the envelope
descends back to zero. They move at the same speed, that is quarter as
steep as the rise. The latter half is smooth as it should be, but the
first half in the middle of the sound... has these weird jagged edges.
Otherwise it goes down perfectly, but there's 16 pairs of what would be
a fortress battlement, descending lightly. The difference leap is small
but it doesn't change the phase, only the amplitude, so it has to be
an envelope thing. I think it can happen in any part of the sound,
randomly.
22:00 - It doesn't happen in every part, curiously. Seems to only take place
in the middle segments on the envelope. When I used just three points,
0 to 2048 to 0, it's smooth as ever. But when adding a fourth one or
more, the battlement effect happens. If the voice is near maximum
amplitude, the battlement is a slim one, as in has a high frequency.
With a more silent sound there are audibly long gaps in the sound. This
is most odd behavior.
241203, Wednesday
23:15 - Have been trying to figure it out. 2048 to 0 to 1024 is also
flawless. Also, overriding the volume envelope removes the effect, so
it would appear that somewhere in the envelope tracking code,
particularly some bit in the interpolating part, is likely bugged...
190104, Monday
02:55 - Lovely holiday. Me refreshed. ^_^ Now back to this issue... the
jagged edges appear at both rising and falling slopes. However, it
seems that... *checks* yes. The distance between the points affects the
severity of the amplitude cut blocks. With all distances being 256, the
amplitude in the affected parts was reduced by 256. However, with all
distances changed to 128, the amplitude reduction changed to 512 at
every part! Using distances of 512 causes an amplitude reduction of
128. Still, why doesn't it happen between every point? And why such
a battlement effect?
03:00 - The distance between two points directly only affects the battlement
effect between the same points. If the battlement isn't triggered for
some slope, then the distance can be anything without problems.
03:05 - An envelope of 2048-1700-512-1700-2048 resulted in all four slopes
getting battlemented. Looks formidable, sure, but sounds awful. Whereas
an envelope 0-512-2048-512-0 gave battlements only on the two center
ones. This seems like a pattern recognition task...
0 -> 512 -> 2048 <- 512 <- 0
^^^^ ^^^^
2048 <- 1700 <- 512 -> 1700 -> 2048
^^^^ ^^^^ ^^^^ ^^^^
0 -> 512 -> 1024 -> 1536 -> 2048
^^^^ ^^^^ ^^^^
2048 <- 1536 <- 1024 <- 512 <- 0
^^^^ ^^^^ ^^^^
0 -> 2048 <- 0 -> 1024 <- 0 is perfectly smooth.
03:15 - Right. So the interpolation thingy performs as it should as long as
at least one of the points is 0, otherwise it gets confused... whew.
And the higher one of the points is, the more disruption is caused.
Even values under 16 cause problems that are almost inperceptible but
can be seen on the graphic wave output.
04:00 - WOOOOO. I figured it out. My interpolation thingy uses two 32-bit
offsets multiplied by the amplitude, and adds those together. Of course
I was smart enough to make use of the 32:32 register combos
appropriately, but for one tiny detail at the end. When adding to 32:32
values together, the lower dwords may overflow. In this case you need
to Carry The One. You do that simply by checking the Carry Flag by
a LAHF command that copies the basic flags to register AH. Now, whoever
infiltrated into and broke my Perfect ASM Code made it so the program
thought mistakenly they went to AL instead. This was of course easy to
fix with a bit shift, and now the system works as Promised. ^_^
200104, Tuesday
00:45 - I think I figured out one crash... when having another application
playing at the same time, like ModPlug Player, and it controls playback
to some extent, MoonSyn crashes randomly when starting or stopping
playback in that other program. I guess DirectX doesn't handle the
primary buffer stuff for SB16 all that well, it being an older sound
card and all. Not that I know exactly why it crashes, of course. The
buffers must keep running in MoonSynth or it would just stall and look
sheepish. Perhaps then one or another buffer becomes invalid somehow...
01:25 - However, neither ModPlug or Winamp crash when I run MoonSynth and
stop and start it even lots of times. So I guess I'm doing something
not entirely correct. :\
210104, Wednesday
00:25 - Performed an interesting experiment. I ran this system monitoring
program, standard windows sysadmin thingie, and checked how much the
processor gets used. I was slightly surprised to see processor usage
being at 100% when MoonSynth was running. Then I realized that it's the
main program that I've made in good old DOS style, to keep polling the
keyboard while constantly updating some debug info on the screen. So of
course all the time that isn't spent on something else like mixing or
other inferior applications, is fully used by the main loop. The sound
engine itself doesn't actually need the main loop, since it will
contain tracking of midi data as well as the mixing. For now though the
main loop is necessary. I added a 40-millisecond sleep for each loop if
no key has been pressed and witnessed processor usage dropping to
nearly nothing while no notes were playing. Interestingly still playing
some song with merry abandon could cause processor usage to peak at
100% again. I wonder if this would allow me to reduce the buffer size
for lower latency...
01:00 - Indeed it does. Now, I'm not sure how much processing power actually
the mixer eats and how much is eaten by the main loop. Of course, this
should be easy enough to check now that I have a functioning envelope;
I just make a never-ending sound and see how much power generating it
eats over ten seconds or so. Then try with two sounds... since, even as
I'm typing this using good old DOS Edit, the processor keeps peaking at
100%. It must be one of these dos thingies. So...
01:30 - Here's the results. I observed each bit for up to a minute to make
sure. Running without notes playing or anything varied; I turned off
the firewall and every other program that might interfere notably. In
general running dry took 0-3%. Upon one test it was 0-1%, then although
I didn't change much it went to 2-3% on a subsequent try. Strange,
perhaps, but I suppose that could happen from simply making the
instrument a little more complicated, as in turning on linear
interpolation and using a sine instead of square, and not displaying
debug data...
Playing one note: 21% -> 3,5%
Playing two notes: 22% -> 4%
Playing three notes: 23% -> 4,7%
Playing four notes: 23% -> 5%
Playing five notes: 24% -> 5,5%
Hitting + to change octave: 24% momentarily
Playing six notes: 24% -> 6%
Playing seven notes: 25% -> 6,5%
Playing eight notes: 25% -> 7%
Octave up again: 26%
Playing nine notes: 26% -> 7,5%
Playing ten notes: 26% -> 8%
The results speak for themselves. What, you can't hear its voice inside
your head? Fine, I'll explain. :p Pressing any key uses momentarily
about 18,5% of processing power. I did this in three second cycles so
perhaps pressing a key eats 100% for a half second ?.. Apart from that,
stacking simultaneous notes was quite cheap, only 0,5% each. Of course
I'm running a 1GHz system, and at 27000 Hz output, but still it's very
nice for unoptimized code. I wonder if this means that each note eats
5 megahertz... that's about 5 million clock cycles a second. Not
counting overhead, it would mean that each generated frame would eat
about 185 cycles... sounds feasible. :D Besides my SB16 is an ISA card
so a modern PCI one would output stuff lighter too. With optimizations,
this should be quite okay! As long as nobody even thinks of touching
the keyboard... :p
21:10 - Tested again, this time using a 1-second refresh rate instead of
3-second, for the processor load graph. Indeed the keypresses now ate
up to 50% each. In fact, I just noticed that even holding down any key,
like shift or control, will still cause the processor load to peak up
to 100%. Inside the command prompt, that is. I can hit any keys I like
on the desktop, or anywhere like the processor burden program, and it
doesn't affect a thing.
21:30 - Of course, DOS programs are given 100% processor power since they
aren't built for multi-tasking, and will happily use up everything the
OS can spare them. MoonSynth is not a DOS program though, yet it still
eats plenty of power when any keys are pressed. Unless it is caused by
running in a console, which would be sort of weird, a silly keyboard
procedure is to blame. I presume in more recent Windowses this should
be handled better all over.
22:20 - Changed the instrument to do some FM again. Now I thought of
a possible problem... I was hoping to have a single long buffer where
all the NoteOn and NoteOff et al midi stuff are stuffed, and which
would be read to trigger the notes. This triggering needs to be done
in the tracker code, and it would be easy to distribute the sounds to
their channels if all channels were processed simultaneously. This is
no longer the case, of course.
22:30 - I guess I'll keep the single note data buffer concept. I'll just need
to process as much of it as will be needed for each mix before starting
the tracking/mixing loop. Sort of, split it apart for the individual
channels so each has their own minidatabuffy... and starting new notes
would be handled by the premix tracking code, setting the new channel
up appropriately but leaving the "playing" flag false, only enabling it
by a NoteOn in the minidatabuffy for that channel.
220104, Thursday
00:00 - The main data stream is a pointer thingy, the stream consisting of
concurrent entries:
tick:dword; (2048 ticks/second: upon splitting to ministreams will be
converted to mix freq speed which can be checked by the tracking
code 4-2048 times/sec)
com:byte; (midi command or other such);
chn:byte; (if command requires a midi channel it's here)
data:whatever; (any possible data for com)
The tracking code will run once for every frame to mix. Each time it
checks if new channel commands are due for that frame, and increases
a counter. The counter is used to time the channel tracking 4-2048
times a second. The channel tracking bit updates the envelopes. The
ministreams are like the mainstream only without the midi channel, and
the global tick changed to coincide with the mixing frames.
04:00 - Now I have a sort of working player. Of course I have to hardcode the
notes into the megastream first. This version also only puts stuff on
one channel, and uses the hardcoded instrument thing. It should be
relatively easy to expand, of course. ^_^ While exiting on one of the
test tries it again gave an error. I wish I could figure out where
those come from. :p
230104, Friday
02:20 - Now I notice that MoonSynth is offkey... the notes should all be one
seminote higher, apparently.
02:30 - Simple enough. My note calculation was off by one due to using
an array starting from 0 for checking which key was pressed, but to get
proper results, C must start from 1. At least now it's correct, and I
could play some familiar songs just like on a real keyboard, only more
clumsily and with slightly mean latency.
04:05 - I should be asleep tightly by now. Anyway, I made MoonSynth play the
bass line of the first part of the Giana Sisters Theme, and loop it
infinitely. It sounds great, except that it's almost as if the notes do
not fall in place exactly at right times. Will look into this. I also
checked the processor load and as one could guess it was the same old.
Having MoonSynth play a riff on one channel affects very little, and
playing lead over it with the keyboard clogs things up. Oddly the kb
latency varied, now and then a sound break would occur and the latency
would change either to 0.2 or 0.4 seconds, as I'd estimate.
250104, Sunday
05:30 - Not yet dawn. :p Now I fixed the note buffer tracker code so that it
doesn't make every note play on channel 1, but gets the next free
channel instead. The current bass riff has a little overlap between the
notes so it sounds sort of fuller now. I also looked at the slight
mistiming of the NoteOns. Turned out I wasn't correctly calculating the
offset within the current mixing block, but that was easily fixed. Now
there is just a minor timing error when looping but I'll improve the
looping bit later on anyway. Looking great! Next step: leave the thing
running overmorning as I go to sleep and see if it hasn't crashed on
its own when I wake up again. ^_^ 815 lines of code now, btw.
14:00 - Still running. :D
15:00 - Okay, next thing to do would be to adjust the tracking code so that
it's possible to call it a variable number of times a second. Since the
per-channel tracking, doing the envelope updates and maybe something
else, can be kinda heavy to do 1024 times a second, and I like
providing customizability. It would help in improving the speed too
especially for slower computers. And for file rendering it could well
be higher than 1024. Currently I made the internal ticking go at 2048
frames per second.
290104, Thursday
09:05 - Just before leaving for work, I'll write down the ToDo list!
* go over DirectSound code
* adjust tracker speed
* check and finish FM synth
* implement a config file
* write own keyboard handler
* write AM synth code
* add stereo mixing and panning envelope
* implement multiple instruments
* implement midi command converter
* implement midi channel management
* implement more commands in tracker
* make a sound editor, create lots of cool instruments
* implement filters and reverb
020204, Monday
01:55 - I implemented a toggle key for the hardcoded playback, then sent the
program to El for evaluation. Refreshingly it crashed on her. It also
went into some sort of infinite loop, evidently, dragging processor
usage to 100% and making everything sluggish. When running MoonSynth
normally it didn't eat much power. From 5-6%, running MoonSynth brought
it to 8-10%. Turning on the playback increased that to 15-20%. I gave
her a new version that plays about four notes simultaneously, which
caused processor use to go up to 35-40%. So it would seem that one
channel eats about 6-8% on her comp. El has a 300MHz processor, if I
remember correctly. Now, if she went ahead and pressed a key, processor
use of course peaked. Often this would also cause an access violation
and an unhandled exception error. I presume this could be due to the
too high processor load caused by the Free Pascal keyboard reading
function, even though it certainly uses windows functions. The
processor consumption was also higher than I expected, but hopefully
optimization will help with that. There aren't that many heavy things
to add into the mixer/tracker now, anyway.
020304, Tuesday
01:50 - I made some changes in the tracker speed. Now it's changeable from
a measly four times a second to 2048 times a second. Below 512 you
already start noticing a quality degradation though. But well, it sure
should save on processing power if needed. As I tried to move the midi
command processing code (that currently only handles Note Ons) into the
tracking loop proper instead of being run separately at every pass,
some strange lockups started occurring. Otherwise my code was flexible
enough to actually work almost as it was.
20:45 - Okay, I keep making these stupid mistakes. Again, confused
an effective memory address and its contents. With that fixed, I can
again play notes, but trying to get the tracker to trigger the
preprogrammed note sequence causes a hang...
21:40 - Progress. ^_^ I forgot to update the note sequence offset variable in
a loop. Having fixed that, even the sequence plays nicely for a moment,
but hangs inexplicably after a while... and curiously this seems to
depend on the tracking frequency. At high tracking speed hanging
doesn't occur, while at a low speed it hangs straight away. I guess
I've left in some hack that only allows one command per channel or
something...
22:30 - The hangs seem to happen somewhere in the sequence breakdown routine,
the one that splits the data stream to ministreams for each channel. It
appears that a hang only occurs when the last 2048th frame to mix is
close to the 2048th of the command. When the tracking frequency is 1024
times a second, as before, it only seems to hang when the last frame to
mix is just one more than the command. Going down in the tracking
frequency appears to increase the dangerous frames so that at 64 times
a second hangs occur when the last frame to mix is at most 16 frames
past the command frame.
22:50 - Changing the internal note on offset to just zero instead of
somewhere midway removed hangs altogether when tracking at 64 fps or
higher. However, 32 fps or lower will always hang even now.
23:00 - And if the mixer gets to process more frames at once, the hangs are
reduced so that 32 fps doesn't always hang...
030304, Wednesday
00:00 - So I implemented a system where the ministream buffers wrap around
instead of being filled from the start every time. This did not help.
Oddly the whole thing works perfectly as long as the midi command
processing is done once for every mixing frame. If the processing gets
moved into the tracking loop where it runs only 64 times a second,
problems arise. However, there's never more than one command in queue
waiting for handling anyway so the stacking can't really be the
dilemma.
00:40 - The problem appears to be that if the channel isn't set to play the
sound soon enough, the hang happens. I have no idea why this is so,
since as long as the playing flag is false there should be little
happening at all. Therefore, with sufficiently rapid tracking the
channel is set to Play and the hang is avoided. This is proved by
changing the keyboard playing routine to initialize the channel
otherwise but leaving it stopped, which gives one okay sound but the
next one will hang.
01:05 - Alright, I believe I figured it out. The routine that finds the next
free channel makes use of the play flag. Since it decides to re-use
a channel that's already added in the active channel list, the same
channel gets added again pointing to itself.
01:20 - Finally fixed! I'd have a party with myself to celebrate but I'm too
sleepy. Not to mention the added inconvenience of a sugar hangover in
the morning. I'm going to go get a haircut before work anyway. ^_^
Anyway, this means that one item on the To-Do list can now be ticked as
Done! * Adjust Tracker Speed - 100% complete. What next, Massster ?...
08:35 - A full-night endurance test also passed, MoonSynth was still playing.
080304, Monday
00:55 - Now I'm trying to add an instrument type you could use for easily
storing a number of different instruments and copying that into the
channel variables for synthesizing. While typing in the instrument
variables as detailed in MoonSynth Sekret Plan ReRevision X I noted
that the effect of sensitivity on volume was rather unclear. Therefore
I wrote a new version of how volume will be calculated. Here it is...
There are three things to note in calculating an oscillator's volume.
In addition to those there is a channel volume which is the same as the
midi channel volume, and a global volume. For now however, we have the
midi note Velocity 0..127, sensitivity of this particular oscillator
which can be 0 for N/A or 1..31, and the amplitude scale values,
a linear progression from A to B both being 0..255. This last one is
used for adjusting the volume depending on the note being played.
Sensitivity can be 0 and the oscillator uses full volume regardless of
the midi velocity. Otherwise it will either soften or harden the
linearry increasing velocity. The output volume needs to be 0..8192.
If sensitivity is 1..16, let V = (velocity^4 + 15878) / 31756
If sensitivity is 17..31, V = (8192 - ((127 - velo)^4 + 15878)) / 31756
The +15878 is for rounding purposes. V is now 0..8192.
If sensitivity is 1..16, Volume = ( (Sensitivity - 1) * ( [velo shl 13]
div 127 ) + (16 - Sensitivity) * V ) div 15
If sensitivity is 17..31, Volume = ( (31 - Sensitivity) * ( [velo shl
13] div 127 ) + (Sensitivity - 16) * V ) div 15
This makes a balanced average between the exponential curve and
a straight linear increase. Finally, the scaling by note: Volume =
Volume * ((ampscaleb * note + ampscalea * (5373 - note)) / 5373) / 255
01:50 - Also I decided to skip the waveform displacement. It would save some
memory in some cases but it's really more efficient to just make
a separate waveform if you need such. Now I have a nice typecast for
instruments. Maybe tomorrow I'll write some test instruments and the
code to copy that into a channel's own variablespace... and after that,
I could make a piece of code to generate 128 slightly different
instruments... and then, work on midi file reading because it would be
really motivating to be able to play those things. ;)
090304, Tuesday
00:25 - Right, I wrote some instrument initialization code and it appears to
work. Now to write something that copies it into the channel vars...
01:55 - Getting it to work was fairly simple. Technically I now have multiple
instruments working. If only someone created a bunch of cool
instruments for me now... I noted also that, although one channel seems
to eat 1-2% of processing power on my comp, running the hardcoded
sequence hoggles about twice that. I believe the heavily typecasted
instrument copying code I put in place is kinda slow to use, but I'm
reluctant to convert that to asm until I feel more certain I won't be
adding new variables to the instrument definition. Not that it'd cause
major hassle to fix things afterward, though. I guess next I'll try to
create some instrument generation code so I won't need to bother typing
in 128 of those typecast heaps. And after that, why not see if I can
decode midis and play them... :D
180304, Thursday
19:55 - I wrote some code to generate funky FM instruments but it didn't work
quite as well as I hoped. Some very frightening sounds were produced,
including several composed of crackling and a strange rising "whup" at
the end.
20:50 - Okay, rewrote it so it's simpler. Only does two-operator stuff, one
modulates the other, which other is played out. However, in half of the
instruments the modulating operator also modulates itself for some
extra effect. This combined with a small number of waveforms, and eight
modulation amplitudes yields 128 instruments, all subtly alike. Most
annoyingly my headphones can't handle all the sound and have started to
resonate with some frequencies.
190304, Friday
00:05 - Sniffle! It be lonely when El is away, though rest good for her.
Okay, I started deciphering a midi file. So far so good, I read headers
and made sense of the chunk system, and variable length values. The
difficult bit was the delta timing system, but I think I actually
figured that out as well! Technically there's just two things I need to
be concerned about: Pulses per Quarter Note, and Tempo. These two are
related, as Tempo is microseconds per Quarter Note. The delta times are
in pulses. One pulse thus takes Tempo / PpQN millionths of a second.
This is easy to convert to MoonSynth ticks, of which there are 2048 in
each second. An example: Tempo is 400000 microsecs per quarter note, or
0,4 seconds. Pulses per quarter note is 120. Microseconds per Pulse is
400000/120 = 3333,3. Quarter Notes per Second is 1000000/400000 = 2,5.
Pulses / Second = Pulses / Quarter Note * Quarter Notes / Second = 300.
MoonSynth Ticks per Pulse = 2048 / 300 = 6,82666.
00:25 - So... that means... Ticks per Pulse is...
32 divided by ( PpQN * 15625 / Tempo ).
01:55 - Okay again, now I'm implementing fake midi command processing so that
I could get even one simple song to play. I'm ignoring most commands,
just reading them in, except Note On events. Of course, I couldn't get
very far before crashing into that infamous Running Status trick.
Briefly, all midi commands use seven bits, the highest bit is always
set and the lowest four tell the channel to use. So there are only
three bits' worth of commands, but to make it more interesting and
efficient, you can insert a new command data byte without the actual
command if it's the same command as the previous one... because all
data except metadata has the highest bit always clear. This isn't too
difficult to implement of course, but it's getting too late to deal
with this kind of aggravation! Night-night...
270304, Saturday
14:10 - My eyes grew moist as success flooded my immediate surroundings.
Getting running status to work was a piece of cake. After that I needed
to recall that a note on with a velocity of 0 is actually a note off
and I can ignore it for the time being... and, I spent an hour trying
to figure out what is wrong with my delta times. The reason was of
course that I'd forgotten to keep a running count and instead sort of
used the delta time itself as the time offset. And then... it finally
worked! MoonSynth actually can now play a song! I picked Matoya's theme
from FF1 as the suitable midi, in part due to it being the only type 0
midi file which is the simplest to read.
280304, Sunday
18:45 - Oh dear, there seems to be a problem. For some reason, after extended
playing, all channels were stuck and the music was only playing on one
channel. That must mean the Playing flag is stuck on some channels...
080404, Thursday
21:20 - The reason was of course the hacky datastream looping. I moved the
stream end check inside the main stream dividing loop and after a few
trial runs and fixes that fixed it. Superb. ^_^
260404, Monday
20:20 - What I need now is to be able to read more midi file types. Most are
probably type 1, and as such contain multiple tracks. I can read
a single track already, but multiple ones need to be combined somehow.
I guess I'll do it so that each track is read into a temporary
datastream, which is then mixed into the main datastream. One problem
arises from my handling of the tempo, since I currently do the tempo
changes while reading and not while playing. Probably that must be
changed so that the tempo changes are effected as technically they
should have been all along.
290404, Thursday
00:15 - Okay, the inner data streams are now going to be built as follows...
The whole stream is qword-aligned. Each entry starts with the trigger
time in MoonSynth internal ticks, of which there are 2048 to a second.
Then there is the command. If the command requires a midi channel, that
comes next. Otherwise there's two or three data bytes available. If
more data space is needed, the following qword block is available by
setting the first byte to FF. A normal tick count must always have the
highest bit clear. (so max song length is one megasecond, or 12.1 days)
tick : dword;
command : byte;
[channel \ data] : byte;
data : word;
[more data : qword;]
Since midi files can have multiple tracks, they are read in one at
a time into a ministream, which is then mixed into a superstream. Since
there are some midi commands that can be simplified for internal use,
that superstream is then parsed once more while copying to the final
playback datastream.
The commands available go as follows...
9x: 2: Note On: Channel, Note, Velocity. Note becomes a word value at
playtime. Zero-velocities are converted to noteoffs while
pre-processing. If the same note is already playing, it gets
an automatic noteoff before the new noteon.
8x: 3: Note Off: Channel, Note, Faderate/Velocity. A noteoff will scan the
desired chn and issue fadeout+sustoff for the voice playing this
note.
Ax: Aftertouch - not implemented for now.
Bx: Controller commands - in midi two data bytes: controlnumber and value.
121: 0: Controller Reset. No data. Puts all controllers to default state.
1: 1: Vibrato: Channel, coarse value. Fine adjustment from controller 33
is ignored. This might affect vibrarate as well as vibradepth.
Usually modulation only affects depth, and around values of
half-octave up and down. Vibradepth is internally stored as maximum
value to move away from the center note in mininotes. Since the
coarse midi values go from 0 to 127, that can be divided by 7,
resulting in a vibration of 0..18 mininotes. Vibrarate should be
around 3-7 Hz, and maybe that could also increase depending on the
depth.
6: 6: Data Entry Coarse. Channel, Coarse Number, Fine Number.
38: 6: Data Entry Fine.
When reading the midi tracks, the value half that isn't entered gets
the highest bit set. Compressing into datastream allows for both
coarse and fine data entry to be combined into one if they are
consecutive commands and on the same channel.
101: 5: Set Data Parameter Number Coarse. Channel, Coarse Number, Fine Num.
100: 5: Set Data Parameter Number Fine.
When reading the midi tracks, the value half that isn't entered gets
the highest bit set. Compressing into datastream allows for both
coarse and fine parameter number setting to be combined into one if
they are consecutive commands and on the same channel.
7: 7: Channel Volume Coarse. Channel, Volume:byte. The fine volume from
#39 is ignored. The midi volume is 0..127, but the internal channel
volume uses a juicy 0..32768 range. Linearity won't be good, so the
0..127 are mapped first into 0..255 for datastream storing, and then
translated to 0..32768 with a nicely curved lookup table.
10: A: Panning Coarse. Channel, Pan. Fine Panning from #42 is ignored. The
midi pan gets SHL 1 to range 0..254, and values >= 129 get +1.
123: F: All Notes Off. No data.
120: F: All Sound Off. No data. These both issue noteoffs to all playing
notes on all channels.
Cx: C: Program Change. Channel, New Instrument number.
Dx: Channel Pressure - Not implemented.
Ex: E: Pitch Wheel. Channel, Pitch [word]. This value is placed in the
channel's PitchDeltaTo variable. PitchDelta draws closer to the
target at each tracking pass: Delta = (3*DeltaTo+Delta+2) / 4. Pitch
wheel range is set with command 6, using parameter number 0.
PitchRange gets the number of mininotes the total range should be;
coarse data entry is in multiples of 32 for one seminote each, and
fine entry should be cents, whatever they are. The default range is
4 semitones, or 128 mininotes. The actual pitch is calculated at
every tracking pass into ENote from (Delta*Range)/16384 - (Range/2).
F0: SysEx - Not sure what to do with all these. For now this can be
skipped, reading past data until the end value F7.
010504, Saturday
03:45 - Yes, finally! MoonSynth compiles and runs again, although midi
playback is half broken. Technically it should now be able to read in
type 1 midi files. I made some stupid novice mistakes as usual and that
drew out this programming effort into late night. But hey, it's the
first of May! Since I changed the datastream format slightly I'll need
to update the tracker as well to handle the new quadword format. One
more note before I collapse in bed: some instruments, the very first
one in particular, start out with an annoying pop. Must find out why.
10:30 - Now it plays the same old Matoya midi again.
14:10 - And now it does so using a separate instrument for each channel! :D
What I've accomplished is a basic midi channel scheme. The instrument
thing is actually a hack since I was too lazy to make it work properly,
so now it only sets the channel instruments as the song is loaded.
However, it still doesn't load multiple tracks. Crashed as I attempted
that. The chunk length appeared to not be read correctly.
040504, Tuesday
00:40 - Wowie, I even got it to load type 1 midi files with multiple tracks!
Just forgot to read in a signature before the chunk length. And it can
play them... however, the annoying pop is a bit more weird. It appears
as though the volume envelope still has a bug, as the very first value
it gives seems to be a ridiculously high one, and the desired fade in
only works from the second value on...
01:00 - Easy enough to fix, just needed to preset the envelope value to 0 at
instrument initialization. Not sure why as I thought the envelope value
would always be calculated before it got used, the tracking code being
before the mixing code, but well... it works so I won't complain. :p
01:30 - It plays midi files really nicely now! ^_^ Of course, still using
just the lousy hardcoded instruments, and there's some nasty sound
overlapping or something causing clipping. And for some reason, a small
number of midi files appear to hang the tracking code. Like right at
the start of the third Orc music of WarCraft 2. Oh, and it might just
be my instruments but it's almost as if all the voices are an octave
too low... didn't I fix something like this at some point?
02:50 - The strange hangups with some pieces were due to my broken hack that
was supposed to ignore the percussion channel. Fixed that. I carefully
checked my frequencies and octaves and: MIDI has 11 octaves specified,
the lowest C-0, note 0, being about 8 Hz. Since I wanted extra room
under 8 Hz I added three octaves long ago, bringing my C-0 to around
one Hertz. This is not a problem as I calculate the frequencies
starting from the highest, which for me is the same frequency as for
midi. I listened carefully to some midis and it would seem that the
sounds are at the right height, the bass sounds just aren't as audible
since they all have the same amplitude but low and high sounds should
be bigger to be perceived as loud relatively. I'll have to give another
listen still, trying what if it sounds better at an octave higher. I'm
pretty exhilarated that even the Quest For Glory 4 soundtrack midi from
Quest Studios loaded and played, though it fell out of synch during the
title theme.
23:05 - The processor usage is also not excessive. The falling out of synch
that happens with some songs is likely due to tempo changes, since I'm
not handling those properly. Guess I should try to fix that now.
060504, Thursday
01:45 - Trying to implement a new scheme for the tempo stuff. Tempo changes
will now be implemented during playing and the datastream uses midi
pulses which will be converted to MoonSynth ticks only while playing.
The tempo change becomes internal command #4, with a tri-byte value.
02:50 - Functions like a dream! Which is appropriate since I'm in need of
sleep. Now songs seem to stay in good synch. I am slightly confuzzled
by an anomaly in the QfG4 soundtrack though. As I understand, the
default tempo should be 500000, and midi files should change the tempo
to whatever they like right at the start of the song. The Space Quest 3
midi files I have for example affirm the tempo at pulse 0. The QfG1
soundtrack only affirms the tempo after a few seconds, but it actually
sounds like playing at the right speed with the default value before
setting the tempo. However, the QfG4 title starts notably too fast, and
the tempo is only set properly a few seconds into the file! Oddly other
midi players seem to start out with the right tempo too.
03:30 - Need sleep even more. But well, while many songs play correctly,
a few do not. Notably, a midi transcription of Bach's Toccata and Fugue
has some incorrect tempos, mainly too slow. In addition I wrote a quick
bit of code that adjusts oscillator volume linearry according to the
note velocities but that started muting some notes. Perhaps it was just
because of the linearity as analogically volumes must be exponential...
070504, Friday
02:05 - Agh. Implementing dummy handling of SysEx messages solved the tempos;
simply ignoring those messed up the delta times at the start of the
song a little. Strangely two major midi files still won't load. One is
the QfG3 soundtrack, which works just fine until the file is loaded -
then it states that an overcall has happened and freezes. This means
that for some reason the mixer is called before the previous call has
finished. I don't see why this is so. Another file is the Silpheed GM
soundtrack, which crashes the whole loader after reaching a "subliminal
message" in the file. *rushes back to code, returns in a few minutes*
Okay, it was just a string variable overrun, easy to fix. Now Silpheed
loads up properly too. Still, I'm unsure of the QfG3 anomaly.
080504, Saturday
01:30 - As I thought, the QfG3 problem was likewise a silly memory overflow.
With that fixed the file loads glitchlessly. I'll implement a few
things now as I'm on a roll, starting with instrument changes...
01:35 - Done. Now to add some sort of volume. I'll let global volume wait
until later, but that can be applied at the final mixing phase of the
cycle. Apart from that we've got a channel volume, 0..255, default 180,
mapped to a nice 0..32768 curve, applied after mixing oscillators
together... the volume envelope already works, being linear values,
affecting each oscillator... and there's the velocity, which is
calculated at note on as specified earlier, mapping 0..255 to the same
32768 curve. One site suggested this formula for the curve:
40 log (Volume/127). I think this means 40*ln(vol/127). The result is
in decibels, each ten of which halving the amplitude. I'll precalculate
a table for these, though using (Volume+2)/129 instead so the lowest
volumes are not all zero. The range used is higher than midi volumes,
0..255, since volume sliding must be used to make sure no nasty pops
happen.
15:35 - Fixed a bug in the oscillator velocity formula that made some
velocities way wrong... and now I'm calculating the volume table. This
is going to take a few hours probably. I can't think of any better way
of doing this while retaining perfect values. I use the windows
calculator... ln(volume/259)... swap +/-... *4... Memory Set... 0.5 to
the power of Memory Recall... *32768. Check the hex value, write it in
my table. Repeat 250 times.
16:00 - Ah, actually there is a better way. It's called QBasic. :p
17:25 - Well, that was done quickly. The volumes work nicely too. Now to see
about the pitch wheel.
21:45 - I recalculated the sine wave so it uses the whole 16-bit range. Much
better now. Also rebalanced the instrument generation volumes, as it
appears that a sine wave has an average amplitude of 20844, while the
skvare has 32768 and the triangle is maybe 15360. I also think the
pitch wheel functions, even with the range setting. Perhaps I should
implement the note offs now...
23:00 - Easier than I thought, actually. Of course, even my best instruments
sound like organs. The worst ones just break the ears. And percussion
is ignored. Still, it sounds surprisingly good! ^_^
090504, Sunday
15:15 - I implemented a new layer in the midi file reading, now it reads
bigger chunks into memory rather than reading from the file byte by
byte. This of course made loading ten times quicker. Also added a pause
button. :)
100504, Monday
21:55 - Then I added a file loading command, and that worked just fine too
except that the QfG4 soundtrack crashed on loading. Now I figured that
out and it was caused by a hacky handling of running status, which
worked when reading from a file but not when reading from memory.
110504, Tuesday
02:05 - Now I added a nicer file loading system: it uses the standard windows
file open box. Very handy for a console application. For now it only
allows opening one file at a time though, no playlists. I think I need
to add more waveforms too, so I changed the system so that technically
a large number can be supported easily. However, there was a strange
and noticeable detuning of instruments that have a large amount of FM
on them, as generated by my creation code. There shouldn't be, since
I'm not using linear FM that modulates the Hz, but logarhitmic FM that
moves along my mininotes. I'll need to write some code again that would
dump the output into a file, and take a look at the graphic result...
Ah, and it seems that one Super Mario 3 midi file doesn't load
properly. I guess it's some sort of command I'm not handling with the
right length or stuff like that.
120504, Wednesday
01:45 - Weird but true. This site talks about FM in a useful way, and it says
that Linear FM is the true form, while exponential FM - modulation by
musical intervals, the way I primarily used - makes the sound drift
toward higher frequencies as the modulation gets louder. Anyway, I
removed the square wave from my instrument generator since it was too
loud and unbalancing. I also removed a cut-up sinewave that was too
soft. Now it appears I have a few nice instruments that actually sound
like kind of brassy in a surprisingly good way.
02:00 - I guess I'll upload this. Will have to work the midi loading bug out
later since most seem to load just fine.
190504, Wednesday
15:00 - Caught a slight cold. :( I noted that the pitch wheel isn't supposed
to affect notes that have received noteoff and are fading out. That was
quick to fix, and songs like the QfG4 intro and the Attack music from
Princess Maker 2 sounds much nicer now. Also the Mario midi file loads
just fine now. I must've fixed that at some point. A number of FF7 midi
files appear to have an unusual header though and crash upon loading.
22:00 - El tested and said even running the program made her comp very
sluggish. This at 44kHz output and a 0.25 second mixing buffer, called
six times a second. I made the buffer 0.5 seconds long, called four
times a second, and output 33kHz, and it ran much smoother for her. I
assume mixing too little too often wastes the resources causing this.
I added three new modulation ratios for the instrument generation code,
now there are 1:1, 1:2, 1:3.5 and 2:1. I also put in some dummy
handling of various controllers whose effects I haven't implemented.
The All Controllers Off command was important enough so I made it work
properly. That makes Quest Studios soundtracks play better.
210504, Friday
19:20 - I introduced a tiny bug while implementing the new channel status
bitflag: a note was played even if it hadn't properly got a note on if
a note off was received within the same mixing length. Fixed it. Now
songs like the WarCraft 2 music play properly again.
220504, Saturday
18:10 - Mixing seems somehow heavier now? I went through the mixing loop to
add some proper comments for clarification, and while doing that noted
I could improve the calculations by combining a few. That seems to have
given a 5% speed increase. Now each playing channel appears to eat
about 2% processing power on my 1GHz system. Not very refreshing. Let's
see if changing mixing settings helps.
18:30 - Lowering mixing frequency is the biggest help now, I'm afraid. I'll
try to get to the optimization part soon...
20:30 - So I tried profiling the program using gprof but as people who've
attempted this might guess, of course it didn't work. Piece of junk
profiling program.
181105, Friday
22:45 - Just revising a bit for the website update. ^_^ The program is almost
exactly the same as before. In fact, few of my programs even agree to
compile now that I got Free Pascal 2.0, since it doesn't allow
tampering with a FOR..THEN variable inside the loop, and some other
stuff. So I need to go over the errors and change FOR-loops to
WHILE-loops... anyway, I'm quite happy to let MoonSynth rest as it is
until I need a sound system for one of my too numerous other projects.
Then I'll get around to putting in correct AM, percussion support, and
hand-coded instruments.