SWF 10 musical harmonizer

This is a demonstration of a SWF created entirely in ActionScript 3, using only as3compile from the swftools suite.

By clicking on the keyboard, the SWF synthesizes a simple sound and supplies another note that harmonizes with it (though because it has no tonality programmed in, the harmony will sound odd).

The problem of latency here is noticeable: there is a significant gap between clicking on a key and the start of sound playback. Unfortunately the only way of improving this is by reducing the sound quality.

The SWF uses the dynamic sound generation capabilities of Flash version 10, so older versions of the player will not work.

Gnash does not yet support AVM2, so this SWF cannot be played using free software.

The code:

// Sound playing SWF.

import flash.media.Sound;
import flash.media.SoundChannel;
import flash.events.SampleDataEvent;
import flash.events.MouseEvent;
import flash.events.Event;
import flash.display.Sprite;
import flash.display.MovieClip;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFormat;

package soundplayer {

    public class SoundGenerator {

        public function SoundGenerator() {
            so = new Sound();
        }

        public function playSound(i:int):void {
            changed = true;
            topnote = i;
            so.addEventListener(SampleDataEvent.SAMPLE_DATA, generateSample);
            ch = so.play();
        }

        public function stopSound():void {
            so.removeEventListener(SampleDataEvent.SAMPLE_DATA, generateSample);
        }
        
        private function find(a, t):Object {
            for (var c in a) {  
                if (a[c].i == t) return a[c].f;
            }   
        }
 
        /// Select a harmonic interval from the possibilies.       
        private function selectInterval(now, lastint):int {
            var t:Array = find(intervals, lastint);
            var i = now - t[int(Math.random() * t.length)];
            return i;
        }

        /// Callback for sample event.
        private function generateSample(event:SampleDataEvent):void {
            if (changed) {
                bottomnote = selectInterval(topnote, lastint);
                changed = false;
            }
            lastint = topnote - bottomnote;

            for (var i:int = 0; i < 3072; ++i) {
                var a:Number = (i + event.position);
                event.data.writeFloat(trans(a, topnote));
                event.data.writeFloat(trans(a, bottomnote));
            }
        }

        /// The sound object
        private var so:Sound;

        /// Sound output control
        private var ch:SoundChannel;

        /// Whether the sound has changed since the last sample generation.
        private var changed:Boolean;

        /// The previous interval played.
        private var lastint:int = 12;

        /// The currently playing sound.
        private var topnote:int = 1;

        /// The next playing sound.
        private var bottomnote:int = 1;

        /// Nice intervals.
        //
        /// Note these are in semitones!
        /// Minor 3rd: 3
        /// Major 3rd: 4
        /// Perfect 4th: 5
        /// Perfect 5th: 7
        /// Minor 6th: 8
        /// Major 6th: 9
        /// Octave: 12
        private const intervals:Array = [ { i:3, f:[ 3, 4, 5, 7, 8, 9 ] },
                                 { i:4, f:[ 3, 4, 7 ] },
                                 { i:5, f:[ 3, 4, 5 ] },
                                 { i:7, f:[ 3, 4, 8, 9 ] },
                                 { i:8, f:[ 8, 9, 3, 4, 5, 12] },
                                 { i:9, f:[ 8, 9, 3, 4, 5, 12] },
                                 { i:12, f:[5, 7, 8, 9 ] }
                                ];
    }

    // Helper function for generating a moderately attractive wave form. 
    private function generateWave(t:Number):Number {
        return Math.sin(t) + Math.sin(t * 2) * 0.2 +
            Math.sin(t * 4) * 0.05 + Math.sin(t * 8) * 0.2 +
            Math.sin(t * 17) * 0.01;
    }

    /// Helper function to get the pitch correct.
    private function trans(val:int, off:int):Number {
        const C:Number = 258.65;
        const freq:Number = Math.PI * 2 * C;
        return generateWave(freq * Math.pow(2, off / 12) *
            val / 44100);
    }
        
}

package demo {

    import soundplayer.SoundGenerator;

    /// The SoundPlayer
    public class SoundPlayer {

        public function SoundPlayer(s:Sprite) {
            generator = new SoundGenerator();
            sp = s; 
            sp.addEventListener(MouseEvent.MOUSE_DOWN, start);
            sp.addEventListener(MouseEvent.MOUSE_UP, stop);
        }
        
        public function start(event:MouseEvent):void {
            var key:Key = event.target as Key;
            if (!key) return;
            generator.playSound(key.val);
        }

        public function stop(event:MouseEvent):void {
            generator.stopSound();
        }

        private var sp:Sprite;
        private var generator:SoundGenerator;
    };

    public class Key extends Sprite {
        public var val:int;
    }

    private function addKey(s:Sprite, x:int, y:int, val:int,
        black:Boolean = false):Key {   

            var key:Key = new Key();

            var height:int = black ? 35 : 50;
            var width:int = 20;

            key.graphics.lineStyle(1, black ? 0xffffff : 0x0);
            key.graphics.beginFill(black ? 0x0 : 0xffffff);
            key.graphics.moveTo(x, y);
            key.graphics.lineTo(x + width, y);
            key.graphics.lineTo(x + width, y + height);
            key.graphics.lineTo(x, y + height);
            key.graphics.lineTo(x, y);
            key.val = val;
            s.addChild(key);    
            return key;
    }

    /// Root timeline.
    //
    /// This is not attractive or clever. It just sets up a demonstration of
    /// the sound generator.
    public class Main extends MovieClip {

        public function Main() {

            var s = new Sprite();
            var i = new SoundPlayer(s);
            
            var format:TextFormat = new TextFormat();
            format.font = "Sans";
            format.size = 20;

            var tf:TextField = new TextField();
            tf.x = 5; tf.y = 5;
            tf.text = "The automatic harmonizer";
            tf.autoSize = TextFieldAutoSize.LEFT;
            tf.setTextFormat(format);
            s.addChild(tf);
            s.x = 40;

            var whites:Array = [ 0, 2, 4, 5, 7, 9, 11 ];
            var blacks:Array = [ { p:0, v:1 },
                                 { p:1, v:3 },
                                 { p:3, v:6 },
                                 { p:4, v:8 },
                                 { p:5, v:10 } ];

            for (var oct:int = 0; oct < 3; ++oct) {

                // White keys
                for (var i:int in whites) {
                    addKey(s, (oct * 140) + i * 20, 100, whites[i] + oct * 12);
                }
                
                // Black keys           
                for (var i:int in blacks) {
                    addKey(s, (oct * 140) + blacks[i].p * 20 + 10, 100,
                        blacks[i].v + oct * 12, true);
                }
            }

            this.addChild(s);

        }

    }

}