Some Hints for BUZZ machine writing
Some Hints for BUZZ machine
writing (version 0.3)
by vII
Contents
Introduction/Purpose
Generators:
Generator note pitches
Alternate tunings
MIDI playability
Effects:
Several separate inputs
General:
Own Dialogs
DirectX usage
Maintaining version
compatibility
the Save/Init functions
Positions
Stop->Start?
Event mechanism
Accessing wave data on
startup
Credits
Contacting the author
Introduction/Purpose
I've just started writing some machines for BUZZ, the first and only
published being vMidiOut and vGraphity. In the BUZZ mailing list there
are often questions regarding the machine programming and maybe the desire
to have some kind of tutor. I'm not that expert (yet?) and don't want to
or can cover every detail of programming the machines, especially I'm not
experienced with DSP stuff and haven't written an effect machine yet nor
used the dsplib of BUZZ. But I've found some things that aren't in the
supplied sources or don't come into sight easily, which I think are worth
to be summed up here (some of them are pure assumptions, marked with an
"?"):
Starting the thing seems quite easy, just go as Jeskola explains and
use the supplied sources as a start. Visual C++ 6.0 seems to work o.k.,
but the resulting machine DLL's have double the size than compiled with
VC 5, so I'm mostly staying with the older version for the releases (any
settings to cure this?).
Generators:
-
Generator note pitches: You need to know the
note format of BUZZ. They are stored as bytes with the higher 4 bits as
octave and the lower 4 bit the note value. The notes are from 1 for C to
0x0c (12) for B (or H in german), the octaves range from 0 to 9. So the
least possible note is 1 for C-0, than it goes 2 for C#0,... 9 for G#0,0x0a
for A-0, 0x0b for A#0 (german B), 0x0c for B-0, jumps to 0x11 for C-1 and
so on. The formula for a continuous note range from 0 to 96 is
Note = (tv.note>>4)*12+(tv.note&0x0f)-1;
if tv.note is the name for your BUZZ track note. Special values are 0 for
no note and 0xff for a note off command.
The resulting note may be shifted by octaves (multiples of 12) to get
the correct MIDI note, the corresponding frequencies would be
freq = base_freq* pow(2.0, Note/12.0);
with a base_freq = 16.3516 Hz for C-0 (-> A-4 = 440 Hz).
Alternate tunings: A bit more complicated
are alternate tunings. For vMidiOut I implemented them via pitchwheel commands
(which may sound very bad and only allows monophone MIDI channels), but
I've also found an archive with over 1000 scales,
which could be used as standard format for any machines experimenting with
this topic. The basic idea is to fill a float array of all 127 possible
MIDI notes (or for BUZZ some less) with the according frequencies. The
scale file contains comment lines (starting with an '!'), the scale name,
the number of steps s for a whole scale (for the standard chromatic
scale or many others this is 12) and following the intervall between the
single steps and the base note of the scale either in cents (100 cents
is 1 half tone step) or as ratio (an octave is 2/1). To fill the frequency
array you need additionally a root frequency f0 to start the scale
with and associate a MIDI note m0 with this frequency. Now just
set all frequencies with an index less or equal to m0 to f0,
and calculate the next s frequencies from the file. Repeat the last
step until you reach the end of your array. Easy, isn't it?
MIDI playability: While I found no way to
add a MIDI record option to a generator, the (at least monophonic) playing
of a generator via MIDI should be very easy to implement. You have just
to add the mi::MidiMote(int const channel, int const value, int const velocity)
to your code, where value is the midinote this time (no translation necessary)
and velocity the "volume" in the range from 0 (for note off) to 0x7f (127),
and do in the function just the same stuff you would do in the mi::Tick()
option, when a track note arrives. The main problem is to switch this feature
on and off and assign a track to listen to. For a new machine you could
easily add an attribute, but for existing ones with compatibility in mind,
you should add an extra dialog for this.
Effects:
Several separate inputs for an effect machine:
BUZZ
2 shall bring the possibility of using several inputs for one effect without
simply mixing them together first. This is needed for a real vocoder (not
using the wavetable samples) or similar machines. But we don't have to
wait for BUZZ 2 to achieve this and we also don't have to restrict to a
limited bandwidth as in Ynzn's Multiplexer/Demultiplexer (which nonetheless
is a nice idea to solve this basic task completely according to the BUZZ
1 machine interface; and it gave me the idea to avoid the interface for
this one):
All machines of the same kind can share global data of the machine
dll. So it would be easy to have several ringbuffers (several -> to allow
more than one pair of inputs) of let's say 1k samples. Every machine now
has a parameter, telling which buffer to use, and another, if it shall
write to the buffer. A second machine can now use it's own input and the
input from any of the ringbuffers (even from more than one) to do what
it wants...
For something like a vocoder this would look like:
(output
is set to zero,but also written into ringbuffer N)
input 1 -------> vocoder 1 -------------|
|
input 2 ------------------------------vocoder
2---------> output
(has
access to input 2 and ringbuffer N)
General:
-
Own dialogs: For communication with the machine
(input) you have the attributes, the global and track parameters, the Init/Save
functions and the machine menu (with the command function). As output the
supplied MessageBox() just isn't enough, as aren't the menus in case you
want to switch some more parameters. So it's very soon you want to add
a DialogBox, but to access resources in the DLL you have to add something
like
HINSTANCE dllInstance;
mi *p_mi;
BOOL WINAPI DllMain( HANDLE hModule,
DWORD fdwreason, LPVOID lpReserved )
{
switch(fdwreason) {
case DLL_PROCESS_ATTACH:
// The DLL is being mapped into process's address space
// Do any required initialization on a per application basis, return FALSE if failed
dllInstance=(HINSTANCE) hModule;
break;
case DLL_THREAD_ATTACH:
// A thread is created. Do any required initialization on a per thread basis
break;
case DLL_THREAD_DETACH:
// Thread exits with cleanup
break;
case DLL_PROCESS_DETACH:
// The DLL unmapped from process's address space. Do necessary cleanup
break;
}
return TRUE;
}
so you can create your dialog with
p_mi=this;
DialogBox(dllInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, ConfigDialog);
in the Command() function and access your data in your ConfigDialog() message
handler with the p_mi function.To find the BUZZ main window I used the
EnumWindows() function, looking for a "- Buzz - " in the title.
Using DirectX: To use any components of DirectX
(as vGraphity for instance uses DDraw) you usually need to give a window
handle for initializing. If BUZZ itself already uses DirectX because of
the DirectSound driver, you are forced to use the handle of the main BUZZ
window (see last point), or the initialization will fail. With vGraphity
for instance this causes some inconsistencies with the associated window
being BUZZ but the really targeted window being that of vGraphity...
Compatible machine versions: To allow future
versions of your machines to be compatible with older, there are some things
to concern just from the beginning. Of course the global and track data
size must not change anymore, also the default values if possible (but
changing them doesn't cause BUZZ crashes). It is possible to add attributes
later to a machine without, but once the machine has some of them, they
can't be changed anymore (really?), so best you generally add some more
for future use.
Another thing is to implement things and data sizes a bit generous,
even if this increases the pattern and song size. A good idea may be a
"method" attribute (as in vMidiOut) or a similar byte/integer in the patterns
(as in Geonik's Omega). This allows you to redesign your machine a lot,
without losing compatibility. But always keep in mind, that you also just
can give your machine a new name.
Save/Init: If you begin to use the Save and
Init functions, you should establish your own mechanism to deal with all
data sizes (of current, former and maybe even future versions), especially
check for an input pointer of zero, to allow loading of older songs without
data at all. I always read and write the data size first, followed by some
kind of version indication for the machine a song was written with. This
way it would be even possible to say: "Hey, you should use at least version
xxx of this machine, to play this song correctly."
Positions: Unfortunately the machine interface
doesn't give you any access to song positions (yet?). The GetPlayPosition()
and GetWritePosition() commands just start if you press the "Play" button
the first time after loading a song and won't never stop again (with the
pause button). Both count samples and wrap around at 0x800000. The write
position always is a bit ahead, so I guess it's something like the actual
position of data written to the wave driver, while the play position tells
you, what you're listening to. For the DirectSound and the silent driver
they seem to run smoother (or even continuous?) with a constant difference
(depending on driver settings, especially latency), while with the standard
wave driver they increase in larger steps and also the difference varies
(why this?).
Stop->Start? If there's a Stop() function, shouldn't
there be a similar start function, too? I guess yes, but there's really
none implemented yet and also nothing to replace this missing one: the
above position functions run through and so do the Tick() and Work() functions.
But I've heard about the possibility of hooking the BUZZ play button ;)
Event mechanism: BUZZ has a built-in event
mechanism (based on a 5ms timer?), which should avoid an own timer for
any machine, but sometimes it seems to be left behind in favour of the
wave output (especially on NT?). The standard usage would be to call ScheduleEvent(GetWritePos(),data
) in the Tick() function and implement the Event(data) method. This would
give you an event triggered when the play position reaches the data processed
in the above Tick()-call. There seems to be a limit on the number of events
waiting (at least I had some crashes with many of them), so I reduced them
to one per Tick() in vMidiOut. You also must not call ScheduleEvent() in
the very first Tick() (where the last active machine parameters, which
always get saved with the song, are written back to the machine). I guess
there's also a problem with waiting events when closing the machine (calling
the destructor), or at least with accessing the machine data when processing
them.
Accessing wave data on song startup: My
vGraphity concept of storing machine data in the wavetable section has
one disadvantage: on startup (for instance for autoshowing an image) the
machines are loaded and initialized before the waves are loaded.
The easy solution: I've used a 2sec timer to delay loading anything from
the waves. This may cause difficulties with machines in the song which
need a longer initialization time for themselves or if the listener is
very quick with pressing the play button...
Credits:
Found out some things myself, but others not:
Thanks to Jeskola for the help, especially concerning the event mechanism
(and of course for the source of all this pleasure -and sometimes trouble-
BUZZ).
And to Hagen: hooking the play button is just a too fine idea
to let it be unmentioned... I'm still thinking about OCR for the song position
;)
contact me or mail contributions to: (
vII)
or look at my
Homepage