Mark Peterson's profile

Melody Maker (phone app #4)

Summary
For this assignment (Adobe Gen Pro App Design course, Jun 2015, Class 4), we were to create a simple Wheel of Fortune app in Brackets and PhoneGap following the tutorial by Mark Shufflebottom (https://vimeo.com/126309355). We were to try to design it so a user could pick it up on sight (“Don’t Make Me Think!”), yet provide multiple outcomes in the form of a 16-segment circular image that rotates on a tap or swipe and then stops at a randomly selected segment. Our instructors also asked us to consider how we might use such an app in the classroom.
 
Being a musician, I considered how I could use the app to teach music. I thought it would be fun for both teachers and students (especially younger students) to have an app help create random melodies. I locked onto that thought and then transformed the Wheel of Fortune app into a random melody maker.
 
The skinny: When you start up the app, it shows a clean design with the app name, a partial wheel segmented into various music notes and symbols, an arrow that points at the active segment, and a Help button. If you swipe left, the wheel rotates clockwise to a randomly selected segment. Press the arrow: If the segment refers to a note, the app will play that tone; if not a note, the app will suggest something related, such as “choose a rest.” Press the Help button, and a popup tells you all of the above, and in addition suggests that you write down the sequence as it progresses and then either sing it or play it back on a keyboard when you have finished. (I did not have time to investigate how to get the app to record. That would be the next step.)
 
Sound simple? Ha. Simple designs that actually work often have a Hemi under the hood to make it appear that way.
 
(Notes: This post provides only partial code for this project. I assume the reader will view the tutorial and then understand the changes I have made. Also, Mark S. provided the original images.)
 (Top) Photoshop Groups and finished Music Wheel
(Bottom) C2 Smart Object: (a) Placed and rotated; (b) in Edit Contents mode
The Wheel
Creating the wheel took the first third of my project time.
 
1) In Photoshop, I surrounded the tutorial wheel image using a circular marquee, inverted the selection, and deleted all of the extra baggage that the tutorial image carried with it.
 
2) I created four template layers: A letter for the note name (Arial font), and a note, staff lines, and a ledger line for the music symbols (Finale’s Maestro font).
 
3) I created a segment “identifier” by copying all four templates, adjusting their content (such as the note’s letter name or whether the note stem points up or down) and positions (such as where the note sits on the staff), and then grouping them into a single Smart Object.
 
4) I copied that Smart Object to other Smart Objects and used Edit Contents to create each individual segment identifier. I created enough of these (13) to make a complete chromatic octave from C to C.
 
5) I overlaid the wheel with a circle object, placed and rotated each identifier in its respective segment, and set the top edge of each identifier flush with the circle to create an overall aligned, uniform appearance. I chose three non-note symbols to fill the remaining segments and placed them as simple text objects. I set all 16 segments in counterclockwise order such that they generally run three notes up the scale followed by a non-note symbol. I then hid the circular “guide.”
 
6) I filled each segment background with the appropriate color. Segments referring to white notes (on a keyboard) became white. Those referring to black notes became a very dark grey (so I could retain the segment lines, which are black). Those referring to non-note symbols became a decreasing intensity of red (dark red, burnt orange, yellow) around the circle. The wheel is basically a circular keyboard.
 
7) I filled the center circle with a gradient that passes through all three of the non-note colors, to make the entire wheel appear more cohesive.
 
8) I saved the image as PNG, to keep the outer transparency.
Help
I wanted to help the user figure out how to work the app, but with as little clutter as possible. The tutorial app builds instructions into the design, such as stating “Tap to Spin” at the top. I considered keeping that idea, but where would I then put the remaining instructions? Rather than fill up my beautiful black space with words, I opted to KISS and provide only a clearly labeled Help button for when the user is unable to figure it out unaided. This also freed up the header area for the app name.
 
I first tried out a simple <p> element with the word “Help,” which when tapped would display an Alert through a jQuery tap event. However, I didn’t like the word “Alert” showing up at the top of what is supposed to be a “Help” dialog. “Alert! You have Help!” sounds a bit too strong for a user to have to bear. Unfortunately, after researching a way to change “Alert” to a different word, I discovered from numerous posts that it is simply not possible, and this by design to dissuade evil hackers.
 
Fortunately, these same doomsday posts also suggested an alternative: popup messages. I went to the jQuery Mobile Demo pages, found a simple popup solution, and implemented it. I think it works nicely.
Random Segments
The tutorial explains how the wheel spins to a random segment, so I post here only a summary: A tap (or a swipe) triggers a jQuery event. The event handler calls Math.random() to select a random number from 0 to 1. It then multiplies it by 16 (there are 16 segments), retrieves the integer portion via Math.floor(), and multiplies the result by 22.5 (360 degrees divided by 16 segments) to determine how many degrees to rotate. It adds this to 11.25 so that the rotation always lands in the middle of a segment and not on a black line. It finally uses CSS transform and rotate to rotate the wheel.
Swipe: Which Direction?
Our instructor showed us how to implement swipeleft as an alternative to tapping the image to trigger the rotation. I tried his code and it initially worked great. However, I quickly discovered that mixing swipeleft with touchstart (as the tutorial uses) produces erratic results, since they both try to work at the same time. Furthermore, swipeleft by itself sometimes makes the wheel rotate one direction and then another. What? Shouldn’t a left swipe always make the wheel rotate clockwise?
 
After much research and testing, I discovered an important truth: The CSS rotate function does not rotate x degrees; instead, it rotates TO a specified degree. For example, if the previous rotation = 11.25 degrees, and the next = 191.25, the object will rotate 180 degrees clockwise to get to 191.25. If the rotation after that = 181.25, the object will rotate back (counterclockwise) 10 degrees. It doesn’t matter at all which direction you swipe; all that matters to rotate is the degree target.
 
Furthermore, rotate ignores 360-multiples. If you pass 360 degrees, it will just subtract 360, or a multiple thereof, and go clockwise to the leftover degree. For example, if you start at 11.25 and go to 191.25, the object will rotate TO 191.25. If you then go to 371.25, the object will not rotate once around (360) plus 11.25; it will simply rotate halfway around to 11.25. If you then go to 731.25, the object will rotate once around—not twice around—and return to 11.25. The target can increase infinitely, and rotate will ignore any round trips implied by 360-multiples and simply move to the leftover degree.
 
This last point means that you cannot build an algorithm that both adds to (+=) and subtracts from (-=) the target degree and expect the rotation to always go in one direction. For example, if you try to calculate a target degree that is always 360 degrees or less, rotate will simply go back and forth like a caged animal. If you want to go consistently in one direction, you have to either always add to or always subtract from the target degree. Remember, this is because the target degree can increase (or decrease) infinitely, and rotate will still know what to do.
 
To make the wheel consistently rotate clockwise, I therefore made the target degree continually increase.
Swipe: Which Segment?
I wanted the user to be able to play the tone corresponding to a note segment. For example, if the user lands on the “C” segment, it should play a C. If the user lands on the “Ab” segment, it should play an Ab. How to tell the app which segment is which?
 
I use two facts to resolve this issue.
 
1) I know exactly which degree corresponds to which segment. For example, I know that the “C” segment is the first segment, and that the value 11.25 or any 360-multiple added to 11.25 will take the wheel there. I also know that “G” is the 10th segment, and that the value 213.75 or any 360-multiple added to 213.75 will take the wheel there. And so on.
 
2) I know that rotate ignores 360-multiples, as discussed above.
 
So I simply use one variable to track the total number of degrees rotated (which can be infinite, and rotate will still know what to do), and I assign another variable to ride alongside the first but which always subtracts 360 whenever it goes over. The first variable will always tell the app where to rotate, and always clockwise. The second variable will always tell the app which segment is active.
 
I could then use a switch statement to tell the app what to do with whatever segment is active, based on the second variable’s segment-identifying value.
Audio on the Active Segment
Playing audio presented a far greater challenge than I expected. I have previously taken a course in HTML and JavaScript, and there I successfully recreated the textbook’s examples of how to implement the HTML5 <audio> tag. I tried the same thing in my app, and it failed miserably.
 
I spent some sleep researching the problem, and although I never did find out why my textbook’s solution would not work, I did find a solution that works. It creates an <audio> element, as well as an event handler, on the fly as the page loads. All I had left to do then was change the src audio file, depending on the switch result for the current segment, and then play it.
 
I recorded the sound samples from Finale 2014. I created a new sheet music document, added a single measure with two beats, added a quarter note, and exported it as an MP3 file. I then opened the audio file in my WavePad sound editor and cropped off the end silence so the total playback time is 0.8s. To create each of the other audio files, I changed the note in Finale, re-exported to MP3, and re-cropped.
Non-Notes
I wanted the user to be able to incorporate other music elements into the melody, such as rests and dynamics. I had three segments available, so I arbitrarily chose a quarter rest and two dynamics (forte and piano). I then added Alerts that inform the user respectively to choose some type of rest or to make the melody loud or soft at that point in the sequence.
 
If I had more time, I would try to find a non-Alert solution similar to that for the popup Help button. However, this class ends in less than two weeks! What I have done will have to do for now.
index.html <style>
<head>
    ...
    <style>
        .center {
            text-align: center;  /* Center text in a div */
        }
 
        .heading {
            font-size: xx-large; /* Very large font */
            margin: 0 auto;      /* Center text (in connection with .center, above) */
            margin-top: 10px;    /* A little bit of space on top */
            padding: 0;          /* No padding */
        }
           
        #holder {
            /* https://developer.mozilla.org/en-US/docs/Web/CSS/overflow */
            /*   "The content is clipped and no scrollbars are provided." */
            /*   Take out scroll bars even if image overflows container boundaries */
            overflow: hidden;
                             
            /* Make sure the div is as wide as any mobile device it runs on */
            /*   Theme [role="main" class="ui-content"] makes padding: 16 16 16 16 */
            /*   [width: 100%] extends ONLY TO L-R PADDING; 32px < window */
            /*   See: http://www.w3schools.com/cssref/pr_dim_width.asp */
            /* THIS <DIV> WILL BE AS TALL AS ITS ELEMENTS IN THEIR NORMAL POSITIONS */
            /*   Ex. #wheel is 763px tall; "arrow" is 100px tall */
            width: 100%;
        }
 
        #spinner {
            /* Spinner block W = W of wheel (wheel fits exactly L-R) */
            width: 763px;
 
            /* Position block relative to its NORMAL position inside #holder <div> */
            /*   See: http://www.w3schools.com/cssref/pr_class_position.asp */
            position: relative;
                                  
            /* Move T of block up so shows only lower half (382) minus a bit (43) */
            /*   Block still retains full height (763) */
            top: -425px;          
                                  
            /* NEED THE NEXT THREE PROPERTIES to center wheel no matter the device */
 
            /* Center spinner block L-R, flush T (elements under will be flush B) */
            /*   ONLY centers if browser window W >= spinner W (763) */
            /*   If browser window W < spinner W, L side = flush L, R = off screen */
            margin: 0 auto;       
                                  
            /* left + margin-left = Centers spinner block no matter the device */
            left: 50%;             /* Move L of block to exact middle of #holder */
            margin-left: -382px;   /* Adjust block to the L, half its own W (763) */
        }
           
        #wheel {
            /* Wheel will take 0.5s to spin and will ease in/out */
            -webkit-transition: all 0.5s ease-in-out;
 
            /* Origin point is dead center of wheel (763x763) */
            -webkit-transform-origin: 382px 382px;
        }
           
        #holder2 {
            /* See notes for #holder. */
            /* overflow must be "visible" or #arrow won't cross #holder bottom
                   boundary (768px) intact */
            overflow: visible;
            width: 100%;
        }
           
        #arrow {
            /* Position arrow relative to its NORMAL position inside #holder2 <div> */
            position: relative;
 
            /* Move T of arrow up as far as #spinner (425px)
                 Add 20px = 445px, so arrow sits on wheel */
            top: -445px;
 
 
            /* left + margin-left = Centers spinner block no matter the device */
            left: 50%;             /* Move L of arrow to exact middle of #holder */
            margin-left: -50px;    /* Adjust block to the L, half its own W (100) */
        }
           
        #help {
            /* Position button relative to its NORMAL position in #ui-content <div> */
            position: relative;
            top: -425px;           /* Move T of button up almost as far as #arrow */
        }
           
        .bigBold {
            font-size: 25px;
            font-weight: bold;
        }
    </style>
</head>
index.html <body>
<body>
    <div data-role="page" data-theme="b">
        <div data-role="header" class="center">
            <!-- Cannot use h1 here if want to change the font size.
                    "header" data-role overrides any such attempts. -->
            <p class="heading">Melody Maker</p>
        </div><!-- /header -->
 
        <div role="main" class="ui-content">
            <!-- Container for the wheel -->
            <div id="holder">
                <div id="spinner">
                    <img id="wheel" src="img/wheel_music.png">
                </div>
            </div>
               
            <!-- Container for the arrow -->
            <div id="holder2">
                <img id="arrow" src="img/arrow.png">
            </div>
            
            <!-- Container for the Help button and jQuery popup text -->
            <!--   See: http://demos.jquerymobile.com/1.4.5/popup/ -->
            <div id="help" class="center">
                <a href="#popupBasic" data-rel="popup"
                   class="ui-btn ui-corner-all ui-shadow ui-btn-inline bigBold"
                   data-transition="pop"
                   data-position-to="origin">Help</a>
                <div data-role="popup" id="popupBasic">
                    <p>
                        1) Swipe Left to Spin<br>
                        2) Press the arrow to sound the tone or get instructions<br>
                        3) Write the sequence as you go<br>
                        4) When finished, sing it or play it back on your favorite
                             keyboard!
                    </p>
                </div>
            </div>
        </div><!-- /content -->
    </div><!-- /page -->
    ...
</body>
index.js (partial, with comments)
...
// [Update DOM on a Received Event]
receivedEvent: function(id) {
 
    // [Add our App behavior here]
 
    // Set total degrees rotated so far, so rotations always stop mid-segment
    //   Wheel has 16 segments, separated by black lines
    //   Arrow starts on a black line
    //   360 degrees / 16 = 22.5 = Distance wheel must travel to next black line
    //   11.25 = Half that distance: Rotate halfway into segment, between black lines
    //   Add multiples of 22.5 to 11.25 so rotation always lands in mid-segment
    var degreesTotal = 11.25;
       
    // Set degrees moved within 360 so far
    //   Set to 0 initially to prevent user from playing any tone on app startup
    //   (On startup, the arrow points to a black line, not to a segment)
    var degrees360 = 0;
       
    // Add swipeleft functionality
    //   See: https://api.jquerymobile.com/swipeleft/
    //   Bind swipeleftHandler callback func/e-handler to swipeleft event on #wheel
    $("#wheel").on("swipeleft", swipeleftHandler);
 
    // Run swipeleftHandler EH w/similar code to tutorial's "touchstart" unnamed EH
    //   DO NOT RUN e.preventDefault() in swipeleftHandler
    //     If run this, swipeleft will not work
    //   DO RUN e.preventDefault() in touchstart
    //     Required there to prevent user from swiping entire window up
    //     COMMENT OUT OR DELETE everything else in touchstart
 
    // Spin the wheel clockwise to a randomly-chosen segment
    //   See: http://www.w3schools.com/cssref/css3_pr_transform.asp
    //   See: http://stackoverflow.com/questions/4072869/how-to-determine-direction-of-rotation-in-css3-transitions
    //     Positive degreesTotal = Clockwise
    //     Negative degreesTotal = Counterclockwise
    //     Positive or negative is RELATIVE TO PREVIOUS degreesTotal
    //       Ex. If degreesTotal = 11.25 and advances to 191.25, moves +180 deg CW
    //       Ex. If degreesTotal = 191.25 and goes back to 181.25, moves -10 deg CCW
    //     "degreesTotal" can increase infinitely
    //       CSS "rotate" advances "distance" degrees, NOT "degreesTotal" degrees
    //         Advances TO "degreesTotal", which is based on 360-degree rotations
    //       Ex. If degreesTotal = 371.25, rotates to seg 1 of 16 (360 + 11.25)
    //       Ex. If degreesTotal = 731.25, rotates to seg 1 of 16 (360 + 360 + 11.25)
    //   1) Generate a random # from 0-15 (16 wheel segments)
    //   2) Add 1 so wheel always moves at least one segment
    //   3) Multiply by distance wheel must travel per segment (22.5 degrees)
    //   4) Add total distance to current degreesTotal (min 11.25 degrees, at start)
    //        --> To turn CCW, SUBTRACT total distance from current degreesTotal
    //   5) Rotate the wheel
    //        Talk to the wheel
    //        Talk to its CSS
    //        Tell -webkit-transform property to rotate img TO "degreesTotal" degrees
           
    // Segments and angles on the music wheel:
    //   1) C - 11.25
    //   2) C# - 33.75
    //   3) D - 56.25
    //   4) Quarter Rest - 78.75
    //   5) D# - 101.25
    //   6) E - 123.75
    //   7) F - 146.25
    //   8) Forte - 168.75
    //   9) F# - 191.25
    //   10) G - 213.75
    //   11) G# - 236.25
    //   12) Piano - 258.75
    //   13) A - 281.25
    //   14) A# - 303.75
    //   15) B - 326.25
    //   16) C - 348.75
    // As wheel moves with each swipeleft, all angles are multiples of the above
    //   Can therefore subtract 360 to detect each segment
       
    function swipeleftHandler(event){
        // Get a random rotation distance and set the target end point
        var distance = Math.floor((Math.random() * 16) + 1) * 22.5;
        degreesTotal += distance;
           
        // Detect app startup and set up arrow to play its first tone
        //   after the user swipes and rotates the wheel at least once
        if (degrees360 == 0) {
            degrees360 = 11.25;
        }
           
        // Find out which segment should become active
        degrees360 += distance;
        if (degrees360 > 360) {
            degrees360 -= 360;
        }
           
        // Rotate to the target segment
        $('#wheel').css('-webkit-transform', 'rotate(' + degreesTotal + 'deg)');
    }
       
    // Prevent user from swiping the #holder <div> (and its contents) L-R or T-B
    //   Prevent swipe L-R:
    //     Can set a specific element to not allow swipe L-R
    //       Set from the (document) on specific events, for specific elements
    //       Must use both e.preventDefault() and e.stopPropogation()
    //       See: http://stackoverflow.com/questions/15723362/prevent-jquery-mobile-swipe-event-over-specific-element
    //       See: http://api.jquery.com/event.preventdefault/
    //       See: http://api.jquery.com/event.stopPropagation/
    //       THIS DOES NOT MATTER HERE because [overflow: hidden] auto-prevents L-R
    //     #holder [overflow: hidden] auto-prevents swipe L-R
    //        [hidden] puts <div> in block mode (16px L/R auto-padding, no scrolling)
    //        See: https://developer.mozilla.org/en-US/docs/Web/CSS/overflow
    //        See: http://www.w3schools.com/cssref/pr_class_display.asp
    //        See: Inspect Element on the page, which shows the [hidden] element in block mode
    //   Prevent swipe T-B:
    //     jQuery has no native mechanism to prevent swipe T-B (must create yourself)
    //       See: http://stackoverflow.com/questions/17131815/how-to-swipe-top-down-jquery-mobile
    //     Easier to just use e.preventDefault() on #holder
    //       Stops all swipe on all content, except the explicit swipeleft on #wheel
    //       However, prevents clicks on all elements inside #holder
    //       Therefore, must move #arrow OUT of #holder into the next <div> down
    //   Solution: Bind touchstart event to #holder and prevent its default behaviors:
    //     1. Firing of emulated mouse events
    //     2. Native page scroll (scrolling of screen when user touches it))
    //     See: https://developer.mozilla.org/en-US/docs/Web/API/event/preventDefault
    //     See: http://www.quora.com/Whats-the-default-behaviour-of-the-touch-e-g-touchstart-event-on-touch-screen-devices
    //     See: https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent
    //     See: http://blog.mobiscroll.com/working-with-touch-events/
    $('#holder').on('touchstart', function(e) {
 
        //   MUST LEAVE THIS ENABLED if using swipeleft
        e.preventDefault();
           
        // MUST DISABLE THE NEXT THREE LINES if using swipeleft
        //   Otherwise, conflicts and causes erratic results
        //var direction = Math.floor(Math.random() * 16) * 22.5;
        //degreesTotal += direction;
        //$('#wheel').css('-webkit-transform', 'rotate(' + degreesTotal + 'deg)');
    });
       
    // Play an audio file when tapping the #arrow
    //   See: http://stackoverflow.com/questions/8489710/play-an-audio-file-using-jquery-when-a-button-is-clicked
    //   See: http://stackoverflow.com/questions/10083788/play-sound-file-when-image-is-clicked?lq=1
    //   This works ONLY if #arrow is NOT inside #holder
    //     (Due to #holder e.preventDefault())
    //     (See comments above on swiping)
    // Can use either .on('tap') or .click(), same results
    //   See: http://www.w3schools.com/jquerymobile/jquerymobile_events_touch.asp
    $(document).ready(function() {
        var audioElement = document.createElement('audio');
        audioElement.setAttribute('autoplay', 'autoplay');
 
        $.get();
 
        audioElement.addEventListener("load", function() {
            audioElement.play();
        }, true);
 
        // ALTERNATIVE: $('#arrow').click(function() {
        //$('#arrow').click(function() {
        //    audioElement.setAttribute('src', 'audio/C1.mp3');
        //    audioElement.play();
        //});
        // Play the matching tone to the one shown on the wheel after being spun
        //   See the chart above
        $('#arrow').on('tap', function() {
            switch (degrees360) {
                case 11.25: // C1
                    audioElement.setAttribute('src', 'audio/C1.mp3');
                    audioElement.play();
                    break;
                case 33.75: // C#
                    audioElement.setAttribute('src', 'audio/C-sharp.mp3');
                    audioElement.play();
                    break;
                case 56.25: // D
                    audioElement.setAttribute('src', 'audio/D.mp3');
                    audioElement.play();
                    break;
                case 78.75: // Quarter rest
                    audioElement.setAttribute('src', '');
                    alert("Choose a rest (eighth, quarter, half, etc.)");
                    break;
                case 101.25: // D#
                    audioElement.setAttribute('src', 'audio/D-sharp.mp3');
                    audioElement.play();
                    break;
                case 123.75: // E
                    audioElement.setAttribute('src', 'audio/E.mp3');
                    audioElement.play();
                    break;
                case 146.25: // F
                    audioElement.setAttribute('src', 'audio/F.mp3');
                    audioElement.play();
                    break;
                case 168.75: // Forte
                    audioElement.setAttribute('src', '');
                    alert("Make it LOUD!");
                    break;
                case 191.25: // F#
                    audioElement.setAttribute('src', 'audio/F-sharp.mp3');
                    audioElement.play();
                    break;
                case 213.75: // G
                    audioElement.setAttribute('src', 'audio/G.mp3');
                    audioElement.play();
                    break;
                case 236.25: // G#
                    audioElement.setAttribute('src', 'audio/G-sharp.mp3');
                    audioElement.play();
                    break;
                case 258.75: // Piano
                    audioElement.setAttribute('src', '');
                    alert("Make it soft...");
                    break;
                case 281.25: // A
                    audioElement.setAttribute('src', 'audio/A.mp3');
                    audioElement.play();
                    break;
                case 303.75: // A#
                    audioElement.setAttribute('src', 'audio/A-sharp.mp3');
                    audioElement.play();
                    break;
                case 326.25: // B
                    audioElement.setAttribute('src', 'audio/B.mp3');
                    audioElement.play();
                    break;
                case 348.75: // C2
                    audioElement.setAttribute('src', 'audio/C2.mp3');
                    audioElement.play();
                    break;
            }
        });
    });
    console.log('Received Event: ' + id);
}
Melody Maker (phone app #4)
Published:

Melody Maker (phone app #4)

This project is an enhanced version of the “Wheel of Fortune” spinner app from the tutorial for this class. The tutorial version divides the whee Read More

Published: