#!/usr/bin/perl -w # # Copyright 2000 by Michael Coyle # Released under GPL. # # Call it with: # [an error occurred while processing this directive] # # Get the file name from the browser... $file_name = $ENV{'QUERY_STRING'}; # Open the file... open (EP, $file_name); # Print to the browser... print "Content-Type: text/html \n\n"; # Load the file and keep spitting it out to the browser... while () { chomp; print "$_ "; } # Close the file and go home... close EP #!/usr/bin/perl -w # # Copyright 2000 by Michael Coyle # Released under GPL. # # Call it with: # [an error occurred while processing this directive] # # Get the file name from the browser... $file_name = $ENV{'QUERY_STRING'}; # Open the file... open (EP, $file_name); # Print to the browser... print "Content-Type: text/html \n\n"; # Load the file and keep spitting it out to the browser... while () { chomp; print "$_ "; } # Close the file and go home... close EP

3D
3D Photo Gallery (Part 1)
3D Photo Gallery (Part 2)

Audio
Poor Man's MIDI
Make A Metronome
iPod Tricks (Part 1)
iPod Tricks (Part 2)
iPod Tricks (Part 3)
Laugh Track Machine
Audio Player with Reverb
Shepard Melody
RB Phone Home
Build a Drum Machine

Custom Controls and Windows
Double Click Listbox
Draggable Metal Window
Double Click Canvas
Custom Buttons
Custom Buttons Part II
iTunes-style Listboxes
Custom Controls


General RB
Scrolling Windows
Using Mesage Dialogs
Case-Sensitive Word Finder
Introduction to Stacks
Wiggle Window
JPEG in PDF
Listbox Checkboxes
Background Applications
Listbox Auto-Find
Virtual Volumes
Time Tracker
Software Distribution (Part 1)
Software Distribution (Part 2)
Software Distribution (Part 3)
Software Distribution (Part 4)
Exceptions
Tips and Tricks
Text Clippings Made Easy

Graphics
Drawing a Simple Gradient
The SpriteSurface: Space Game
Image Spinner
Cropping Graphics (Part 1)
Cropping Graphics (Part 2)
Cropping Graphics (Part 3)
Cropping Graphics (Part 4)
Shimmer Graphics
Lissajous Figures
Simple Screen Capture
Vector Graphics
Kaleidoscope Images
Stegonography
Spirals!
Image Table
RB Magnifying Lens
Screen Capture
Color Picker Tutorial

Hacks
Ghost Grab
Speedy Mouse Extension
iTunes Plugins
iTunes Skinner

Mac OS X
Global Hot Key Event (Carbon Events)
Login Welcomer (Carbon Events)
Add/Remove Buttons
Resizable Sheets
Mac OS X Preferences Window
Using Sheets in REALbasic
Build a Bundle (Part 1)
Build a Bundle (Part 2)
Dock Your Passwords
Mac OS X Debugging
REALbasic Mac OS X Icon Tutorial
Animate Your Dock
RB and the Command Line

Menus
Window Menu
Templates Menu
Listbox Menu

Novelty
Guessing Game
Calendar Trivia
Tile Mixer
Zip Code Finder
Happy Valentine's Day
Merlin Simulator (Part 1)
Merlin Simulator (Part 2)
Merlin Simulator (Part 3)
Buzzword Machine
AppleSoft BASIC

Printing
Print to PDF

Registration
Registration Code Validation
Network Registration Codes

Resources
Picture Extractor (Part 1)
Picture Extractor (Part 2)

Serial
Caller ID (Part 1)
Caller ID (Part 2)
Caller ID (Part 3)

Speech
Speech Recognition

Socket Communication
Easy Peer-to-Peer File Sharing
MacPAD Version Checking
Display Web Image In Canvas
HTML IMG Tags
Version Tracking
Even Smarter Instant Messaging
Web Tiler
JavaScript and REALbasic
Stock Ticker (Part I)
Stock Ticker (Part 2)
AIM Mate

XML Manipulation
Simple XML Introduction

Video
Big Brother Video Capture

Note: All articles without a byline were written by Erick Tejkowski. When cleaning the site I removed them because the code differed from page to page, and I have yet to put them back in.

resexc2.gif (20k)




REALbasic for Dummies
by Erick Tejkowski

$19.99 @ Amazon





Files are in Stuffit 6.5 or earlier, or ZIP format.
Download Stuffit Expander

Tell us about a bad link.

Poor Man's MIDI by Seth Willits
05-01-04




What We're Making
To make an actual MIDI application that reads, writes, and plays MIDI files would be quite involved and would have to span over several weeks. Instead of starting with that (because I may just evolve this project into an actual MIDI application), we're going to create a "Poor Man's" version which doesn't do anything with files, only uses a piano, and has one full octave starting at middle C, but it does record what you play, and can play it back. All in all, it's a pretty neat little application that can spawn a million ideas. It did for me.

The Interface
The interface for the application includes one EditField named "EFNotes", one NotePlayer ("NotePlayer1"), and two buttons, one which toggles for "Record" (and "Stop") and the other "Play".



The Design
There are two ways our application represents notes. There is the user's representation which is a single line of text in the format: NewNote(Pitch=x,Time=y,Velocity=z), and then there is the internal representation which is a dictionary with three keys, Pitch, Time, and Velocity. In both representations, the Velocity parameter is actually optional.

Recording
The way our user will record a song, is to use the home row and top row keys. "A" is middle C and "L" is the D an octave higher. You can play a complete C scale with the ninth by pressing ASDFGHJKL. The top row keys are used just as the sharps and flats on a piano (the black keys). "W" for example is C sharp. The space bar makes a "silence" note which simply stops the last note that was played.

After clicking on the Record button, when the user presses a key, our program will respond by doing two things. First it will create a new line of text in the EditField for that note, and second it will play the note. To stop recording, the user will again click on the Record button which is labeled "Stop".

Playing
When the Play button is clicked, the contents of the EditField will be parsed and new dictionaries will be created for the notes. These notes are stored in the Song array. Then, after the parsing is complete, the song will begin to play, by looping through each note in the Song array, determining if it is time to play yet, will stop the previous note from playing, and then will play the current one.

Properties
We're going to need a couple of properties to accomplish this. They are as shown below and will be described in more detail when necessary.



Playing a Song
In a nutshell, what this code does, which is inside of the Play button's Action event, is exactly what is stated two paragraphs above here. Here's the code:

dim note as Dictionary
dim line, count as integer
dim noteLine as String
dim currentNote as Integer
dim startTime as Double

// Clear Old Song
Redim Song(-1)


// Create Notes
count = CountFields(EFNotes.Text, EndOfLine)
for line = 1 to count

First we declare our variables (duh), then we clear out the old song be redimming the array. Next we're going to loop through every line of text in the field.

  // Get Line Data
  noteLine = NthField(EFNotes.Text, EndOfLine, line)
  
  // If It's Creating a Note, then Do So
  if Left(noteLine, 7) = "NewNote" then
    noteLine = Mid(noteLine, 9)
    noteLine = Left(noteLine, Len(noteLine) - 1)

Here we're just making sure that the line isn't empty and contains a semblance of a valid NewNote() line. We then strip the line down to its parameters for easy reading. (ie "Pitch=60,Time=0,Velocity=80") Below, we then parse the parameters and create a new note dictionary. Remember that the Velocity parameter is optional which is why we first check to see if it contains a Velocity parameter. Also, notice that the order of the parameters can't be changed since this code is dependent on the order. After we parse it, we append the note to the Song array.

    // Create Note
    note = New Dictionary
    note.Value("Pitch") = Val(NthField(NthField(noteLine, ",", 1), "=", 2))
    note.Value("Time") = Val(NthField(NthField(noteLine, ",", 2), "=", 2))
    if InStr(noteLine, "Velocity") > 0 then
      note.Value("Velocity") = Val(NthField(NthField(noteLine, ",", 3), "=", 2))
    end if
    
    // Add Note To Song
    Song.Append note
  end if
next

The way our recording system works, is that it records the length of time that passes from the beginning of the song until the note is supposed to play. So when playing back a song, we need to know when the song starts, which is what the startTime variable is for. And since each note of the song needs to be played and is only played once, our outer for loop can simply loop through each index of the array.

// Start the Clock
startTime = Microseconds


// Play Each Note of the Song
for currentNote = 0 to UBound(Song)

This small loop simply churns until it is time for the next note to play. While this tutorial could have been written to read directly from the field rather using a dictionary to store note information, the extra overhead of parsing the text between playing notes might cause the song to drag, causing notes to be played at inconsistent times. This will really only happen if the notes are close together, but hey, it can happen (wink).

  // Wait until it's time to play the note 
  do
  loop until Song(currentNote).Value("Time").DoubleValue <= Microseconds - startTime

Below is where we hand off the note to the PlayNote method which actually plays the note. The reason we have a test for whether currentNote is greater than 0, is because if it is, we want to pass the previous note (Song(currentNote - 1)) to the PlayNote method as the second optional parameter, so that it knows which note to stop playing. (Remember that our application will automatically stop the previous note before the "current" one begins to play.) If there was no previous note, then it simply doesn't pass a second parameter at all.

  // Play Note
  if currentNote > 0 then
    PlayNote Song(currentNote), Song(currentNote-1)
  else
    PlayNote Song(currentNote)
  end if
next

Playing a Note
So what does this PlayNote method look like? Well first of all, the declaration goes a little something like this: PlayNote(CurrentNote as Dictionary, PreviousNote as Dictionary = nil). If you've never seen the equals sign used in a method declaration like this, it's probably because its a new feature in REALbasic 5.5 which you haven't seen before. Using "= <value>" after a parameter's type will make the parameter optional by specifying a default value. In our case it is nil, thus when the PlayNote method is called from the line above, the second parameter (which is omitted in the line of code) will be nil.

Sub PlayNote(CurrentNote as Dictionary, PreviousNote as Dictionary = nil)
   
   // Stop the Previous Note
   if PreviousNote <> nil then
      NotePlayer1.PlayNote PreviousNote.Value("Pitch").IntegerValue, 0
   end if
   
   // Play the Current Note
   if CurrentNote.HasKey("Velocity") then
      NotePlayer1.PlayNote CurrentNote.Value("Pitch").IntegerValue, 
CurrentNote.Value("Velocity").IntegerValue
   else
      NotePlayer1.PlayNote CurrentNote.Value("Pitch").IntegerValue, 80
   end if
End Sub

That aside, all that our PlayNote method does, is stop the previous note (by "playing" it again with a Velocity value of 0), and plays the next note.

Starting to Record
The code for the Record button is:

// Set Caption
if me.Value then
  me.Caption = "Stop"
else
  me.Caption = "Record"
end if


// Record Notes
if me.Value then
  EFNotes.Text = ""
  RecStartTime = -1
end if

It, as you can see, is fairly simple and straight forward. Remember that the Record button is a toggling BevelButton, and to compliment that, we toggle the button's caption between "Record" and "Stop". Other than that, we remove the text from the field (since when the user presses keys, it will be inserted into the field), and set the RecStartTime variable to -1. This Double property of the window keeps track of when the first note of the song being recorded was pressed. This variable is used very much in the same way that the startTime variable is used in the Play button's code. The only difference is that clock doesn't actually start until the first key is pressed, and the -1 value flags that no key has been pressed yet.

Recording Notes
The code for handling the key presses to add notes is placed in the KeyDown event of the EFNotes EditField because the field will always have the user's focus and always grab the key events.

Function KeyDown(Key As String) As Boolean
   dim Pitch as Integer, Time as Double
   
   
   // If we're recording, then record
   if BtnRecord.Value then
      
      // Get Pitch
      Select Case Key
      Case "a"
         Pitch = 60 // C
      Case "w"
         Pitch = 61 // D Flat
      Case "s"
         Pitch = 62 // D
      Case "e"
         Pitch = 63 // E Flat
      Case "d"
         Pitch = 64 // E
      Case "f"
         Pitch = 65 // F
      Case "t"
         Pitch = 66 // G Flat
      Case "g"
         Pitch = 67 // G
      Case "y"
         Pitch = 68 // A Flat
      Case "h"
         Pitch = 69 // A
      Case "u"
         Pitch = 70 // B Flat
      Case "j"
         Pitch = 71 // B
      Case "k"
         Pitch = 72 // C
      Case "o"
         Pitch = 73 // D Flat
      Case "l"
         Pitch = 74 // D
      Case " "
         Pitch = -1 // Silence
      else
         Return true // not a key we recognize
      end Select

The Select statement above determines which key was pressed and sets the Pitch value accordingly. If the spacebar is pressed Pitch is -1 to signal that instead of playing another note, it should stop the previous one. If a key is pressed that we don't recognize, we simply ignore it.

      // Get Time
      if RecStartTime = -1 then RecStartTime = Microseconds
      Time = Microseconds - RecStartTime
      
      // Add Note
      AddNote Pitch, Time

Above we determine the amount of time that has passed since the first valid key was pressed after clicking the Record button, and then we use the AddNote method to actually add the text to the field and play the note.

      
      // Return True to signal we handled the event
      return true
   end if
End Function

The AddNote method is the last piece of the puzzle. The first if-statement determines whether or not the note being added is a silencer for the previously played note. If it is, then it modifies the Pitch and Velocity variables in the method to match the Pitch of the previous note and have a velocity of 0 to stop the sound. If the note is not a silencer, then it simply remembers the pitch value in the RecLastPitch window property we added earlier.

Sub AddNote(Pitch as Integer, Time as Double, Velocity As Integer = 80)
   dim note as Dictionary
   
   // Remember the Note if Not Silence
   if Pitch = -1 then
      Pitch = RecLastPitch
      Velocity = 0
   else
      RecLastPitch = Pitch
   end if

This if-statement is where the text for the note is added to the field.

   // Add The Note
   if Velocity = 80 then
      EFNotes.Text = EFNotes.Text + "NewNote(Pitch=" + Str(Pitch) + ",Time=" + Str(Time) 
+ ")" + EndOfLine
   else
      EFNotes.Text = EFNotes.Text + "NewNote(Pitch=" + Str(Pitch) + ",Time=" + Str(Time)
 + ",Velocity=" + Str(Velocity) + ")" + EndOfLine
   end if
   

Here we create a temporary note Dictionary and use it with the PlayNote method to play the note. Notice we also remember the last note that was played so that we can pass it to the PlayNote method so it will stop the previous note eliminating the continuous sound. If you're not sure why we're auto-stopping the previously played note, when the project is completed, comment out the line that stops the sound and run the project to learn/hear why.

   // Play the Note
   note = New Dictionary
   note.Value("Pitch") = Pitch
   note.Value("Velocity") = Velocity
   note.Value("Time") = Time
   
   if RecLastNote <> nil then
      PlayNote note, RecLastNote
   else
      PlayNote note
   end if
   
   
   // Remember Note
   RecLastNote = note
End Sub

Finished
Thats all there is to it! Hopefully you've enjoyed this tutorial, and it would be great if you walk away with an idea for a new program to write and sport the "Made with REALbasic" logo proudly. In future tutorials we might return to this project and beef it up. Some definite improvements would include: 1) More than one octave, 2) note timing relative to the previous note, 3) multiple instruments, 4) actual MIDI read/writing.

As always, you can download the project for this tutorial.






Please support ResExcellence by Visiting our Sponsors. One click makes a difference.


Download REALbasic and create your own software!

#!/usr/bin/perl -w # # Copyright 2000 by Michael Coyle # Released under GPL. # # Call it with: # [an error occurred while processing this directive] # # Get the file name from the browser... $file_name = $ENV{'QUERY_STRING'}; # Open the file... open (EP, $file_name); # Print to the browser... print "Content-Type: text/html \n\n"; # Load the file and keep spitting it out to the browser... while () { chomp; print "$_ "; } # Close the file and go home... close EP #!/usr/bin/perl -w # # Copyright 2000 by Michael Coyle # Released under GPL. # # Call it with: # [an error occurred while processing this directive] # # Get the file name from the browser... $file_name = $ENV{'QUERY_STRING'}; # Open the file... open (EP, $file_name); # Print to the browser... print "Content-Type: text/html \n\n"; # Load the file and keep spitting it out to the browser... while () { chomp; print "$_ "; } # Close the file and go home... close EP