Thread Rating:
  • 1 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Exploring QB64-PE default soundfont patches
#1
I was curious as to what all the default MIDI soundfont patches sounded like, and I also wanted to put together a way of grabbing those sounds somehow in code to use, so this function is the result.  It just grabs 1 midi note.  I don't think I'm calculating the ticks and duration right, but at least I can hear the sound patches available.  There's some real neat ones.  I'm probably not making a valid MIDI data file here, but at least it loads/plays.

- Dav

NOTE:  Get the lastest version, now called MidiNotes, HERE

Code: (Select All)
'Exploring QB64-PE default soundfont patches.
'Makes MIDI a note and play's it from memory.
'Dav, SEP/2024

'Cycles throuh all default sound patchs 127-0

$Unstable:Midi
$MidiSoundFont: Default

'cycle through all sounds, press any key to quit.

For patch = 127 To 0 Step -1
    Print "Patch#"; patch
    note$ = MidiNote$(60, patch, 60, 1)
    midisound& = _SndOpen(note$, "memory")
    _SndPlay midisound&
    _Delay 1
    _SndStop midisound&
    _SndClose midisound&
    If InKey$ <> "" Then End
Next

Function MidiNote$ (tempo&, patch, note, duration&)

    TicksPerQuarterNote = 96

    'Make MIDI Header Chunk (MThd)
    MThd$ = Chr$(77) + Chr$(84) + Chr$(104) + Chr$(100) ' "MThd"
    'Make Header size
    MThd$ = MThd$ + Chr$(0) + Chr$(0) + Chr$(0) + Chr$(6)
    'Make format type (1 = single track)
    MThd$ = MThd$ + Chr$(0) + Chr$(1)
    'Make number of tracks (1)
    MThd$ = MThd$ + Chr$(0) + Chr$(1)
    'Make division = 96 (ticks per quarter note)
    MThd$ = MThd$ + Chr$(0) + Chr$(96)

    'calculate microseconds per beat from tempo& in BPM
    MicroSecsPerBeat& = 60000000 \ tempo& '(converts BPM to microseconds per beat)

    'Get msb/mb/lsb from MicroSecsPerBeat& for saving tempo
    '(midi requires 3 bytes for this info)
    msb = (MicroSecsPerBeat& \ 65536) And 255 'most Significant Byte
    middle = (MicroSecsPerBeat& \ 256) And 255 'middle Byte
    lsb = MicroSecsPerBeat& And 255 'least Significant Byte

    'Make the tempo data + the 3 bytes
    TrackData$ = TrackData$ + Chr$(0) + Chr$(255) + Chr$(81) + Chr$(3) + Chr$(msb) + Chr$(middle) + Chr$(lsb)

    'Set Program number (patch) to use
    TrackData$ = TrackData$ + Chr$(0) + Chr$(192) + Chr$(patch)

    'Set Note On: play note value (with velocity 127)
    TrackData$ = TrackData$ + Chr$(0) + Chr$(144) + Chr$(note) + Chr$(127)

    'convert duration& in beats to ticks& (how long to play the note)
    ticks& = duration& * TicksPerQuarterNote

    'Set Note Off: Stop playing note after specified duration& in ticks&
    TrackData$ = TrackData$ + Chr$(ticks&) + Chr$(128) + Chr$(note) + Chr$(64)

    'Make track end event
    TrackData$ = TrackData$ + Chr$(0) + Chr$(255) + Chr$(47) + Chr$(0)

    'Make the MTrk header
    MTrk$ = Chr$(77) + Chr$(84) + Chr$(114) + Chr$(107) ' MTrk

    'Make the track data length (4 bytes)
    TrackLen& = Len(TrackData$)
    TrackLength$ = Chr$((TrackLen& \ 16777216) And 255) + Chr$((TrackLen& \ 65536) And 255) + Chr$((TrackLen& \ 256) And 255) + Chr$(TrackLen& And 255)

    'Put it all together
    MidiNote$ = MThd$ + MTrk$ + TrackLength$ + TrackData$

End Function

Find my programs here in Dav's QB64 Corner
Reply
#2
Well, I was not expecting anyone to do this.

But this is brilliant. Good one @Dav.
Reply
#3
(09-10-2024, 06:42 AM)a740g Wrote: Well, I was not expecting anyone to do this.

But this is brilliant. Good one @Dav.

Thanks!  I'm going to dig into this deeper, and learn more about the MIDI format.  Looks like a MIDI making program is very possible in QB64-PE. Found a nice page of MIDI format info here.


I edited the code so it's not messed up.  Looks like the QB tag needs a space between MidiSoundFont: and Default.

- Dav

Find my programs here in Dav's QB64 Corner
Reply
#4
Here's a new version, it can now can play a string of given notes and you can specify the note duration in a musical way (whole note, quarter note, etc).  I went ahead and named the program because I think it has some potential to expand on.  I put the note duration stuff inside the notes FOR/NEXT loop because I intend to later allow duration calls in the notes string itself later on, like "qn606162hn656362", and not have it a separate parameter.

This version plays notes up and down on every sound patch.  One more baby step.

- Dav

Code: (Select All)
'=============
'MidiNotes.bas
'=============
'Makes MIDI notes and plays it from memory.
'Exploring QB64-PE default soundfont patches.
'By Dav, SEP/2024 (v1.01)

$Unstable:Midi
$MidiSoundFont: Default

'cycle through all sounds, press any key to quit.
For patch = 0 To 127
    Print "Patch#"; patch
    note$ = MidiNotes$(120, patch, "6062646567656462", "en")
    midisound& = _SndOpen(note$, "memory")
    _SndPlay midisound&
    _Delay 2
    _SndStop midisound&
    _SndClose midisound&
    If InKey$ <> "" Then End
Next

Function MidiNotes$ (tempo&, patch, notes$, duration$)
    '--------------------------------------------
    'Returns MIDI data suitable for MIDI playback
    '--------------------------------------------
    'tempo& = tempo of the midi
    'patch = program number (sound) to use
    'note$ = string of note to play
    'duration$ = the duration the note will have
    'The following duration string is allowed:
    '  wn = whole note
    '  hn = half note
    '  dq = dotted quarter
    '  qn = quarter note
    '  de = dotted eighth
    '  en = eighth note
    '  sn = sixteenth note

    TicksPerQuarterNote = 96 ' Set ticks per quarter note

    'make MIDI Header Chunk (MThd)
    MThd$ = "MThd"
    MThd$ = MThd$ + Chr$(0) + Chr$(0) + Chr$(0) + Chr$(6) 'header size
    MThd$ = MThd$ + Chr$(0) + Chr$(1) 'format type (1)
    MThd$ = MThd$ + Chr$(0) + Chr$(1) 'number of tracks (1)
    MThd$ = MThd$ + Chr$(0) + Chr$(TicksPerQuarterNote) 'division (ticks per quarter note)

    'calculate microseconds per beat from tempo& in BPM
    MicroSecsPerBeat& = 60000000 \ tempo& 'Converts BPM to microseconds per beat

    'get msb/mb/lsb from MicroSecsPerBeat& for saving tempo
    '(Midi requires 3 bytes for this info)
    msb = (MicroSecsPerBeat& \ 65536) And 255 'most significant byte
    middle = (MicroSecsPerBeat& \ 256) And 255 'middle byte
    lsb = MicroSecsPerBeat& And 255 'least significant byte

    'make the tempo data + the 3 bytes
    TrackData$ = TrackData$ + Chr$(0) + Chr$(255) + Chr$(81) + Chr$(3) + Chr$(msb) + Chr$(middle) + Chr$(lsb)

    'set Program number (patch) to use
    TrackData$ = TrackData$ + Chr$(0) + Chr$(192) + Chr$(patch)

    'loop through all notes given
    For n = 1 To Len(notes$) Step 2
        note = Val(Mid$(notes$, n, 2)) 'get a note
        'set Note On: play note value (with velocity 127)
        TrackData$ = TrackData$ + Chr$(0) + Chr$(144) + Chr$(note) + Chr$(127)

        'get notes duration in beats
        Select Case LCase$(duration$)
            Case "wn": ticks& = 4 * TicksPerQuarterNote 'whole note (4 beats)
            Case "hn": ticks& = 2 * TicksPerQuarterNote 'half note (2 beats)
            Case "qn": ticks& = 1 * TicksPerQuarterNote 'quarter note (1 beat)
            Case "dq": ticks& = (3 / 2) * TicksPerQuarterNote 'dotted quarter (1.5 beats)
            Case "en": ticks& = (1 / 2) * TicksPerQuarterNote 'eighth note (.5 beat)
            Case "de": ticks& = (3 / 4) * TicksPerQuarterNote 'dotted eighth (.75 beat)
            Case "sn": ticks& = (1 / 4) * TicksPerQuarterNote 'sixteenth note (.25 beat)
            Case Else
                ticks& = 0 'If invalid, set default to 0
        End Select

        '================================================
        'convert ticks& to variable length quantity (VLQ)
        TickData$ = ""
        If ticks& = 0 Then
            TickData$ = Chr$(0) 'safety, in case ticks& = 0
        Else
            Do
                byte& = ticks& And &H7F
                ticks& = ticks& \ 128
                If TickData$ <> "" Then byte& = byte& Or &H80
                TickData$ = TickData$ + Chr$(byte&)
            Loop While ticks& <> 0
        End If
        '===============================================

        'set note off: stop playing note after specified duration
        TrackData$ = TrackData$ + TickData$ + Chr$(128) + Chr$(note) + Chr$(64)

    Next

    'make track end event
    TrackData$ = TrackData$ + Chr$(0) + Chr$(255) + Chr$(47) + Chr$(0)

    'make the MTrk header
    MTrk$ = "MTrk"

    'make the track data length (4 bytes)
    TrackLen& = Len(TrackData$)
    TrackLength$ = Chr$((TrackLen& \ 16777216) And 255) + Chr$((TrackLen& \ 65536) And 255) + Chr$((TrackLen& \ 256) And 255) + Chr$(TrackLen& And 255)

    'put it all together
    MidiNotes$ = MThd$ + MTrk$ + TrackLength$ + TrackData$

End Function

Find my programs here in Dav's QB64 Corner
Reply
#5
That’s really cool! I wonder if PLAY could eventually evolve to take advantage of the new MIDI capabilities.
Reply
#6
That would be cool, @FellippeHeitor.  And HEY! Smile

Looks like all MIDI data opened with _SNDOPEN(filedata$, "memory")  loop forever for me when played in any way.  Is this on purpose?  It doesn't do that when playing other audio file data from memory, like ogg's,  Only MIDI. 

- Dav

Find my programs here in Dav's QB64 Corner
Reply
#7
(09-10-2024, 07:47 PM)Dav Wrote: Looks like all MIDI data opened with _SNDOPEN(filedata$, "memory")  loop forever for me when played in any way.  Is this on purpose?  It doesn't do that when playing other audio file data from memory, like ogg's,  Only MIDI. 
This does not happen to me in v3.14.1.

We changed the entire MIDI engine in v3.14. Which version are you using?

Update: I was able to recreate the bug in v3.13.1 and older. So, we fixed it!  Smile
Reply
#8
I’m still using in 3.13 on the laptop I’m using to make it.  I will upgrade today. Glad to hear that it’s fixed!   Now I can add some cool sound effects for my ballshoot game.   Will post a new version of this program today probably.   Fully implemented a PLAY-like system for making MIDI songs.  

- Dav

Find my programs here in Dav's QB64 Corner
Reply
#9
Wow nice selection of sounds like different instruments playing.
b = b + ...
Reply
#10
Yeah, the sounds are a good resource that should be tapped.  The resulting midi data is small and can be added to code fairly easy.  I wanted to do this mainly to get some better sound effects that can be used in code.  

Here's a laser or siren sound.  Just an example of how it can be put/used in code..

- Dav

Code: (Select All)
$Unstable:Midi
$MidiSoundFont: Default

'make laser& sound
a$ = "4D546864000000060001000100604D54726B0000001600FF51030F424000C07B00903C7F60803C4000FF2F00"
laser$ = ""
For i = 1 To Len(a$) Step 2
    laser$ = laser$ + Chr$(Val("&H" + Mid$(a$, i, 2)))
Next

laser& = _SndOpen(laser$, "memory")

_SndPlay laser&: _Delay 1: _SndStop laser&


Find my programs here in Dav's QB64 Corner
Reply




Users browsing this thread: 5 Guest(s)