|
Bay Six Software Beyond the Basics
|
View previous topic :: View next topic |
Author |
Message |
JosephE Junior Member
Joined: 01 Nov 2007 Posts: 21
|
Posted: Aug 3rd, 2009, 4:29am Post subject: MusicLib |
|
|
I've been working on a basic MIDI output library called MusicLib for LB.
Since Brent's way of writing functions has really influenced me, I thought I might post what I have so far here. Critiques, addons, suggestions - all our welcome.
Basically, to play a note, you specify the letter of the note:
A, B, C, D, E, F, or G
All notes are measured from middle c. To specify a note in a different octave, use the + and - signs.
For example, ++E plays an E note two octaves above the middle c.
-F plays a an F note an octave below middle c.
To sharp a note (raise it a half-step), use the hash sign. To flat a note (lower it a half-step), use the percent sign.
+C# - plays the note c sharp an octave above middle c.
--G% - plays the note g flat two octaves below middle c.
My wrapper includes a lot of error checking. But it helps to know what went wrong.
Sources include: MSDN, http://www.computermusicresource.com/MIDI.Commands.html, http://www.harmony-central.com/MIDI/Doc/table2.html
Code with a very tiny example:
Code: | '--------------------------------------------------------------------------------------------'
'Filename: MusicLib.bas
'Description: Legacy Midi Input Wrapper for Liberty BASIC
'File Creation: Saturday, August 1st, 2009
'Author: Joseph Essin
'--------------------------------------------------------------------------------------------'
hMidi = Music.Open(e$)
If e$<>"" Then Print e$
'Set our instrument to #50 - strings.
Call Music.SetInstrument e$, hMidi, 1, 50
If e$<>"" Then Print e$
'Play D flat on the octave above middle c.
Note$ = Music.Note.Play$(e$, hMidi, 1, "+D%", 127)
If e$<>"" Then Print e$
'Stop our note after a delay of 2 and a half seconds.
CallDLL #kernel32, "Sleep",2500 As Long, Null As Void
Call Music.Note.Stop e$, Note$
If e$<>"" Then Print e$
'Give it time to let the note fade out (now that we have turned it off):
CallDLL #kernel32, "Sleep",1000 As Long, Null As Void
'Close our midi output device:
Call Music.Close e$, hMidi
Print "Finished."
If e$<>"" Then Print e$
End
Function Music.Open(ByRef Error$)
'Parameters:
'Error$ - this holds the description of any errors that occur.
' Error$ is untouched if no errors occur.
'----------------------------'
'CONSTANTS: (From mmsystem.h)
MMSYSERR.NOERROR = 0
MMSYSERR.BASE = 0
MIDIERR.BASE = 64
MIDIERR.NODEVICE = MIDIERR.BASE + 4
MMSYSERR.ALLOCATED = MMSYSERR.BASE + 4
MMSYSERR.BADDEVICEID = MMSYSERR.BASE + 2
MMSYSERR.INVALPARAM = MMSYSERR.BASE + 11
MMSYSERR.NOMEM = MMSYSERR.BASE + 7
MIDI.MAPPER = -1
'---------------------------------------------'
Error$ = ""
uDeviceID = MIDI.MAPPER
Struct HMIDIOUT, lphmo$ As PTR
CallDLL #winmm, "midiOutOpen",_
HMIDIOUT As Struct,_
uDeviceID As Long,_
dwCallback As uLong,_
dwCallbackInstance As uLong,_
dwFlags As uLong,_
MMRESULT As uLong
Select Case MMRESULT
Case MIDIERR.NODEVICE : Error$ = "No MIDI port was found. This error occurs only when the mapper is opened."
Case MMSYSERR.ALLOCATED : Error$ = "The specified resource is already allocated."
Case MMSYSERR.BADDEVICEID : Error$ = "The specified device identifier is out of range."
Case MMSYSERR.INVALPARAM : Error$ = "The specified pointer or structure is invalid."
Case MMSYSERR.NOMEM : Error$ = "The system is unable to allocate or lock memory."
Case MMSYSERR.NOERROR
Music.Open = HMIDIOUT.lphmo$.struct 'Return the handle to the opened midi output if successful.
End Select
End Function
Sub Music.Close ByRef Error$, hmo
'Parameters:
'Error$ - this holds the description of any errors that occur.
' Error$ is untouched if no errors occur.
'hmo - handle to the midi output obtained from Midi.Open()
'----------------------------'
'CONSTANTS: (From mmsystem.h)
MMSYSERR.BASE = 0
MIDIERR.BASE = 64
MMSYSERR.NOERROR = 0
MMSYSERR.INVALHANDLE = MMSYSERR.BASE + 5
MMSYSERR.NOMEM = MMSYSERR.BASE + 7
MIDIERR.STILLPLAYING = MIDIERR.BASE + 1
'---------------------------------------------'
Error$ = ""
'Reset the midi device before we close it (to avoid errors):
'(this turns off all the notes that are playing if there are any)
CallDLL #winmm, "midiOutReset",_
hmo As uLong,_
MMRESULT As uLong
If MMRESULT = MMSYSERR.INVALHANDLE Then 'Trap the one and only error that
'midiOutReset can produce:
Error$ = "The specified device handle is invalid."
Exit Sub
End If
CallDLL #winmm, "midiOutClose",_
hmo As uLong,_
MMRESULT As uLong
Select Case MMRESULT 'Check for possible errors from midiOutClose:
Case MIDIERR.STILLPLAYING : Error$ = "Buffers are still in the queue."
Case MMSYSERR.INVALHANDLE : Error$ = "The specified device handle is invalid."
Case MMSYSERR.NOMEM : Error$ = "The system is unable to load mapper string description."
End Select
End Sub
Function Music.Note.Play$(ByRef Error$, hmo, Channel, Note$, VelocityOn)
'Parameters:
'Error$ - this holds the description of any errors that occur.
' Error$ is untouched if no errors occur.
'hmo - handle to the midi output obtained from Midi.Open()
'Channel - A number from 1-16 of the channel you want the note to play on.
'Note$ - A string containing the notation of the note you want to play:
' example: ++F# -< This plays the note f-sharp two octaves above the middle c scale
' example: C <- this plays middle c
' example: -E% <- this plays the note e-flat an octave below the middle c scale
'VelocityOn - how hard and loud the note plays. Must be 0-127
'Music.Note.Play$ returns the handle to a playing note that is required by Music.Note.Stop()
'----------------------------'
'Out of range exceptions:
If Channel<1 Or Channel>16 Then
Error$ = "MIDI Channel #";Channel;" is out of range. Use 1-16."
Exit Function
End If
If VelocityOn <0 Or VelocityOn>127 Then
Error$ = "VelocityOn #";VelocityOn; " is out of range. Use 0-127."
Exit Function
End If
'CONSTANTS: (From mmsystem.h)
MMSYSERR.NOERROR = 0
MMSYSERR.BASE = 0
MIDIERR.BASE = 64
MIDIERR.BADOPENMODE = MIDIERR.BASE + 6
MIDIERR.NOTREADY = MIDIERR.BASE + 3
MMSYSERR.INVALHANDLE = MMSYSERR.BASE + 5
'---------------------------------------------'
Error$ = ""
Note$ = Lower$(Note$)
NoteNumbers$ = "60 61 62 63 64 65 66 67 68 69 70 71"
Notes$ = Lower$("C C# D D# E F F# G G# A A# B")
Status = 143 + Channel 'Turn the note on.
NoteNumber = 0
OctaveOffset = 0
SemitoneOffset = 0
DidPickNote = 0
For c = 1 To Len(Note$)
char$ = Mid$(Note$,c,1)
Select Case char$
Case "c","d","e","f","g","a","b"
For w = 1 To 12
If char$ = Word$(Notes$,w) Then
DidPickNote = 1
NoteNumber = Val(Word$(NoteNumbers$,w))
Exit For
End If
Next w
Case "#" 'sharp
SemitoneOffset = SemitoneOffset + 1
Case "%" 'flat
SemitoneOffset = SemitoneOffset - 1
Case "+"
OctaveOffset = OctaveOffset + 12
Case "-"
OctaveOffset = OctaveOffset - 12
End Select
Next c
If Not(DidPickNote) Then 'Failure to specify what note...
Error$ = "A note letter must be specified. {A, B, C, D, E, F, G}"
Exit Function
End If
'Now calculate the final value of the note:
NoteToPlay = NoteNumber + SemitoneOffset + OctaveOffset
If NoteToPlay > 127 Or NoteToPlay < 0 Then 'Note out of range exception:
Error$ = "The value ";NoteToPlay;" is out of range. Note values for the Win32 API must be 0-127."
Exit Function
End If
'Let's package all the info for the note together to send it to windows.
LowByte = (NoteToPlay * 256) + Status
HighByte = VelocityOn * 256 * 256
dwMsg = LowByte+HighByte
CallDLL #winmm, "midiOutShortMsg",_
hmo As uLong,_ 'Midi Output Handle
dwMsg As uLong,_ 'What note to play and how to play it all packed together.
MMRESULT As uLong
'Finally, let's check to see if the midiOutShortMsg produced an error:
Select Case MMRESULT
Case MIDIERR.BADOPENMODE : Error$ = "The application sent a message without a status byte to a stream handle."
Case MIDIERR.NOTREADY : Error$ = "The hardware is busy with other data."
Case MMSYSERR.INVALHANDLE : Error$ = "The specified device handle is invalid."
Case MMSYSERR.NOERROR
Music.Note.Play$ = Str$(hmo) + "_" + Str$(Channel) + "_" + Str$(NoteToPlay) + "_" + Str$(VelocityOn)
End Select
End Function
Sub Music.Note.Stop ByRef Error$, ByRef hNote$
'Parameters:
'Error$ - this holds the description of any errors that occur.
' Error$ is untouched if no errors occur.
'hNote$ - this is the handle to a note returned by Music.Note.Play$()
'----------------------------'
Error$ = ""
If Trim$(hNote$)="" Then
Error$ = "hNote$ must be a valid handle to a note returned by Music.Note.Play()."
Exit Sub
End If
hmo = Val(Word$(hNote$,1,"_"))
Channel = Val(Word$(hNote$,2,"_"))
Note = Val(Word$(hNote$,3,"_"))
Velocity = Val(Word$(hNote$,4,"_"))
'Out of range exceptions:
If Channel<1 Or Channel>16 Then
Error$ = "MIDI Channel #";Channel;" is out of range. Use 1-16."
Exit Sub
End If
If Velocity <0 Or Velocity>127 Then
Error$ = "Velocity #";Velocity; " is out of range. Use 0-127."
Exit Sub
End If
If Note > 127 Or Note < 0 Then
Error$ = "The Win32 API value of the note must be from 0-127."
Exit Sub
End If
'CONSTANTS: (From mmsystem.h)
MMSYSERR.NOERROR = 0
MMSYSERR.BASE = 0
MIDIERR.BASE = 64
MIDIERR.BADOPENMODE = MIDIERR.BASE + 6
MIDIERR.NOTREADY = MIDIERR.BASE + 3
MMSYSERR.INVALHANDLE = MMSYSERR.BASE + 5
'---------------------------------------------'
Status = 127 + Channel 'Turn off note
LowByte = (Note * 256) + Status
HighByte = Velocity * 256 * 256
dwMsg = LowByte+HighByte
CallDLL #winmm, "midiOutShortMsg",_
hmo As uLong,_
dwMsg As uLong,_
MMRESULT As uLong
Select Case MMRESULT
Case MIDIERR.BADOPENMODE : Error$ = "The application sent a message without a status byte to a stream handle."
Case MIDIERR.NOTREADY : Error$ = "The hardware is busy with other data."
Case MMSYSERR.INVALHANDLE : Error$ = "The specified device handle is invalid."
Case MMSYSERR.NOERROR : hNote$ = "" 'nullify the handle to the note.
End Select
End Sub
Sub Music.SetInstrument ByRef Error$, hmo, Channel, Instrument
'Parameters:
'Error$ - this holds the description of any errors that occur.
' Error$ is untouched if no errors occur.
'hmo - handle to the midi output obtained from Midi.Open()
'Channel - A number from 1-16 specifying the channel.
'Instrument - A number from 1-128 that specifies which instrument/sound to
' use.
'----------------------------'
Error$ = ""
Instrument = Instrument - 1
'Out of range exceptions:
If Channel<1 Or Channel>16 Then
Error$ = "MIDI Channel #";Channel;" is out of range. Use 1-16."
Exit Sub
End If
If Instrument>127 Or Instrument<0 Then
Error$ = "Instrument is out of range. Please specify a number from 1-128."
Exit Sub
End If
'CONSTANTS: (From mmsystem.h)
MMSYSERR.NOERROR = 0
MMSYSERR.BASE = 0
MIDIERR.BASE = 64
MIDIERR.BADOPENMODE = MIDIERR.BASE + 6
MIDIERR.NOTREADY = MIDIERR.BASE + 3
MMSYSERR.INVALHANDLE = MMSYSERR.BASE + 5
'---------------------------------------------'
Status = 191 + Channel 'Change instrument
LowByte = (Instrument * 256) + Status
HighByte = 127 * 256 * 256
dwMsg = LowByte+HighByte
CallDLL #winmm, "midiOutShortMsg",_
hmo As uLong,_
dwMsg As uLong,_
MMRESULT As uLong
Select Case MMRESULT
Case MIDIERR.BADOPENMODE : Error$ = "The application sent a message without a status byte to a stream handle."
Case MIDIERR.NOTREADY : Error$ = "The hardware is busy with other data."
Case MMSYSERR.INVALHANDLE : Error$ = "The specified device handle is invalid."
End Select
End Sub |
|
|
Back to top |
|
|
Brent Site Admin
Joined: 01 Jul 2005 Posts: 800
|
Posted: Aug 3rd, 2009, 9:16pm Post subject: Re: MusicLib |
|
|
Hi Joseph,
It looks pretty good so far.
Personally, I would refrain from using strings for error handling. The users of your library might want errors in their own language--not necessarily English.
Also, requiring the library's users to pass a variable to every procedure could annoy them. It is much more common to return error codes, but the setting of a global variable is also pretty common. If I do the latter, I usually wrap it in a function, similar to the way the GetLastError API in Windows.
BTW, have you looked at the QBasic PLAY Challenge from a few years ago?
http://libertybasic.conforums.com/index.cgi?board=contests&action=display&num=1075263936 _________________ Brent |
|
Back to top |
|
|
JosephE Junior Member
Joined: 01 Nov 2007 Posts: 21
|
Posted: Aug 4th, 2009, 9:56pm Post subject: Re: MusicLib |
|
|
Hmm...Those are good thoughts. I'll have to smooth it out a little bit.
That gets me thinking... |
|
Back to top |
|
|
JosephE Junior Member
Joined: 01 Nov 2007 Posts: 21
|
Posted: Aug 27th, 2009, 3:12pm Post subject: Re: MusicLib |
|
|
You know, I actually have looked at the QBASIC Play challenge. However, my idea was to create music on the fly (slightly lower level than playing a string of data). Although that has it's ups and downs.
What you said about error handling makes a lot of sense. I really should go back over it and add a global variable (as you mentioned) for error handling. That would smooth things out a bit. |
|
Back to top |
|
|
|
|
|
|
|
You can post new topics in this forum You can reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum You cannot attach files in this forum You cannot download files in this forum
|
|