kelvinluck.com

a stroke of luck

Shibuya and Sharify


Yesterday Adobe announced a new service called “Shibuya“. It’s “a monetization service for developers creating AIR applications”. This may sound familiar to anyone who knows Sharify – the service that I launched back in February which “allows you to easily monitize your Adobe® AIR™ application by turning it into a shareware application”.

My thoughts about this echo very closely those of Grant Skinner in his recent post “Thoughts on Adobe Squiggly & Developer Relations“. In one way I am glad that Adobe are stepping up to the plate and providing what I believe (from my own experiences and from the interest in Sharify) is a very useful and necessary service. But in another way I’m concerned that Adobe are competing with the very people they should be trying to empower – the users of their products.

A little history

As mentioned, Sharify was launched in February of this year under the name of shAIR. It received a fair amount of immediate attention – some of it from people inside Adobe and specifically from Robert Christensen, a senior product manager on Adobe AIR:

I wanted to write you a note and let you know that I’m excited about the shAIR framework that you are working on. Application monetization is a critically important area for AIR developers and I suspect quite a few developers will be leveraging your shAIR once you make it available

The only problem was that he felt that our choice of name infringed on the Abode AIR trademark so he asked if we would mind changing it. He acknowledged that this would involve some work for us and offered in exchange to support the launch of the renamed shAIR with blog posts and articles on the Adobe Developer Connection website. This wasn’t the first time Adobe had come down hard on people for using AIR in a domain name – it happened to freshairapps (see discussion here and here). AIRApps.net had an even worse experience wasting $15,000 sponsorship money for an Adobe MAX conference (as described by Edward Mansouri).

We considered our options carefully. Though we believed our choice of name didn’t infringe Adobe’s copyright (or at the very most it was a gray area) it was more important for us to maintain a good relationship with Adobe. Their offer to help with the promotion of our product was worth much more than the hassle of re-branding our (still very young) product. We made the necessary changes and I emailed Rob the news to be greeted by absolute silence. Previously he was replying to my emails within a day of receiving them. Now it took over 6 weeks and a few reminder emails until I got a response – the not very verbose “Confirming receipt of your email”!

My cynical side started to think that perhaps all Rob was interested in was getting me to change the name of the service. The promised help certainly never materialised. Maybe I should have been more proactive in chasing Adobe to promote Sharify but I opted to focus on improving my product. I was a little disheartened by the change in attitude but decided to give them the benefit of the doubt and assumed I could rely on their marketing support once Sharify was out of beta.

And then… Shibuya!

I’ve given the history just to make it clear that there were definitely people at relevant positions within Adobe who are were aware of the Sharify service (in addition to Rob I have also received correspondence from the Senior Product Manager on the Adobe AIR marketplace). It would seem to be at the very least polite to have contacted me before the announcement of Shibuya and told me that Adobe were working on something extremely similar. Instead, the first I heard was when somebody emailed me a link to an article on The Register.

As Grant notes in his post, it is not simply the case that somebody is competing with my product that upsets me. Sharify already has a competitor called Nitro LM which serves a similar purpose, though it is targeted at a different market (“the enterprise”). It is the fact that the competing company is the same company who is responsible for the platform that I am developing on.

I spent time creating a product which enhances Adobe’s platform and enables developers to make money using the platform. I imagine this leading to more developers spending their time creating AIR apps (as there is a potential payback) which would lead to more installs of the runtime which is presumably Adobe’s ultimate aim. By developing and releasing Shibuya Adobe are rendering the time I spent doing that to a large extent wasted. Which doesn’t encourage me to spend time building things to improve the ecosystem around Adobe’s products. It should also be a warning to other developers who are considering this, which can only damage the very ecosystem that Adobe should be trying to protect.

Another problem with Adobe deciding to tackle this problem is that they have the ability to make changes to the runtime itself. It is unclear from the Shibuya announcement whether this has happened in this case but I have logged feature requests with the AIR team for functionality which would have made Sharify more secure and easier to implement. I don’t know if Shibuya uses any features from an as yet unreleased runtime but if so it would seem that Adobe are not only competing with the people they should be helping but are competing with an unfair advantage!

The future

So – is this the end for Sharify? No! Not yet at least. Shibuya is not available yet and doesn’t have a release date that I can find. It seems that once available it will only allow purchasing from within the USA and Canada. And it appears to be tied in to one payment method (using the PayByCash payment provider) while Sharify allows developers to handle payment in any manner they choose. It is also unclear from the prerelease site if Adobe will charge for using Shibuya and if so, how much. And if Adobe have added features to the AIR runtime to help with the development of Shibuya then presumably these features will be available to other developers as well in which case we will be able leverage them to improve the security of Sharify.

However, until we know more about the final form of Shibuya it is hard justify spending additional development time on Sharify. We are committed to ensuring it continues to work for the members of the private beta who are currently using it but there are some exciting improvements in the pipeline which will have to be put on hold until we establish whether implementation would be a waste of time. We will also have to very carefully consider any other projects which might enhance the Flash Platform.

Update: This story was also covered on the register


Google maps for flash marker clustering


I’ve recently been working on a project which makes extensive use of the Google maps API for flash (more about that once it launches). One of the things that was necessary for this project was clustering of markers when they were too close together. To understand what I mean by this click the image below to check out the example:

Google Maps for Flash Clustering Screenshot

As you can see, the capital cities of the world are all displayed on the map as small red dots. I got the list of capital cities from here and converted them to XML for the example – note that some of them (e.g. Rome) appear to be slightly incorrectly positioned. If the cities are too close to each other for the current zoom level then they are clustered into larger red dots with numbers in the middle.

This has of course been done before (e.g. here and here) but the solutions didn’t work for my situation. The first is grid based (rather than distance based) which can give some strange results. And more importantly for my project I needed to use custom markers (for both the individual markers and the clusters) and that didn’t seem possible without changing the actual library code. And with the second solution the markers seem to “jiggle” as you drag the map and I wasn’t sure about whether the license permitted use in a commercial project.

So I found a post from Mika Tuupola. In it he explains the advantage of distance based (as opposed to grid based) clustering algorithms. He then provides some PHP sourcecode to implement distance based clustering.

I started off with a fairly straightforward port of the code from the article but I found that it needed to be optimised quite heavily so that it would work performantly in Flash (especially since it needs to re-calculate the clustering on every zoom change). My final Cluster class looked like this:

package com.kelvinluck.gmaps
{
   import com.google.maps.overlays.Marker;

   import flash.geom.Point;
   import flash.utils.Dictionary;

   /**
    * Distance based clustering solution for google maps markers.
    *
    * <p>Algorithm based on Mika Tuupola's "Introduction to Marker
    * Clustering With Google Maps" adapted for use in a dynamic
    * flash map.</p>
    *
    * @author Kelvin Luck
    * @see http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps
    */

   public class Clusterer
   {
     
      public static const DEFAULT_CLUSTER_RADIUS:int = 25;

      private var _clusters:Array;
      public function get clusters():Array
      {
         if (_invalidated) {
            _clusters = calculateClusters();
            _invalidated = false;
         }
         return _clusters;
      }

      private var _markers:Array;
      public function set markers(value:Array):void
      {
         if (value != _markers) {
            _markers = value;
            _positionedMarkers = [];
            for each (var marker:Marker in value) {
               _positionedMarkers.push(new PositionedMarker(marker));
            }
            _invalidated = true;
         }
      }

      private var _zoom:int;
      public function set zoom(value:int):void
      {
         if (value != _zoom) {
            _zoom = value;
            _invalidated = true;
         }
      }

      private var _clusterRadius:int;
      public function set clusterRadius(value:int):void
      {
         if (value != _clusterRadius) {
            _clusterRadius = value;
            _invalidated = true;
         }
      }

      private var _invalidated:Boolean;
      private var _positionedMarkers:Array;

      public function Clusterer(markers:Array, zoom:int, clusterRadius:int = DEFAULT_CLUSTER_RADIUS)
      {
         this.markers = markers;
         _zoom = zoom;
         _clusterRadius = clusterRadius;
         _invalidated = true;
      }

      private function calculateClusters():Array
      {
         var positionedMarkers:Dictionary = new Dictionary();
         var positionedMarker:PositionedMarker;
         for each (positionedMarker in _positionedMarkers) {
            positionedMarkers[positionedMarker.id] = positionedMarker;
         }
         
         // Rather than taking a sqaure root and dividing by a power of 2 to calculate every distance we
         // do the calculation once here (backwards).
         var compareDistance:Number = Math.pow(_clusterRadius * Math.pow(2, 21 - _zoom), 2);
         
         var clusters:Array = [];
         var cluster:Array;
         var p1:Point;
         var p2:Point;
         var x:int;
         var y:int;
         var compareMarker:PositionedMarker;
         for each (positionedMarker in positionedMarkers) {
            if (positionedMarker == null) {
               continue;
            }
            positionedMarkers[positionedMarker.id] = null;
            cluster = [positionedMarker.marker];
            for each (compareMarker in positionedMarkers) {
               if (compareMarker == null) {
                  continue;
               }
               p1 = positionedMarker.point;
               p2 = compareMarker.point;
               x = p1.x - p2.x;
               y = p1.y - p2.y;
               if (x * x + y * y < compareDistance) {
                  cluster.push(compareMarker.marker);
                  positionedMarkers[compareMarker.id] = null;
               }
            }
            clusters.push(cluster);
         }
         return clusters;
      }
   }
}

import com.google.maps.LatLng;
import com.google.maps.overlays.Marker;

import flash.geom.Point;

internal class PositionedMarker
{

   public static const OFFSET:int = 268435456;
   public static const RADIUS:Number = OFFSET / Math.PI;
   
   // public properties are quicker than getters - speed is important here...
   public var position:LatLng;
   public var point:Point;

   private var _marker:Marker;
   public function get marker():Marker
   {
      return _marker;
   }

   private var _id:int;
   public function get id():int
   {
      return _id;
   }

   private static var globalId:int = 0;

   public function PositionedMarker(marker:Marker)
   {
      _marker = marker;
      _id = globalId++;
      position = marker.getLatLng();
     
      var o:int = OFFSET;
      var r:Number = RADIUS;
      var d:Number = Math.PI / 180;
      var x:int = Math.round(o + r * position.lng() * d);
      var lat:Number = position.lat();
      var y:int = Math.round(o - r * Math.log((1 + Math.sin(lat * d)) / (1 - Math.sin(lat * d))) / 2);
      point = new Point(x, y);
   }
}

You can download the class and the code for the example from it’s github repository. Note that the Clusterer class is all that you need to use – the rest of the classes are just for the sake of the example. I hope it’s useful – if you make anything cool with it then please post in the comments.



shAIR is now Sharify


Not too long ago I posted about the launch of shAIR – a service which allows developers to easily add shareware functionality to their Adobe AIR applications.

Shortly after the launch we were approached by representatives of Adobe informing us that our choice of name infringed on their trademark. To cut a long story short, while we weren’t convinced that Adobe’s claim was fair, we decided to change the name of the service.

So I take great pleasure in introducing you to Sharify. Check out the website to find out just how easy it is to convert your AIR application into a shareware application. You just need to set up the relevant information on sharify.it and integrate a small swc file. Then you can sell your application to your users and Shairfy will ensure that only people who have purchased a license can use it (after an optional trial period).

The service is still in private beta but if you sign up on the site and include a good description of the application you’ve built or are building then we will be happy to invite you to try it out. What are you waiting for? It’s time to Sharify it!

http://www.sharify.it/



Second steps with Flash 10 audio programming


A while back I did some experimenting with the new Flash 10 audio features. Since then I’ve received a couple of emails from people who have noticed that the flash player can freeze up when the mp3 file is initially extracted with the Sound.extract command – especially with longer mp3 files.

The solution is to simply extract only as much of the sound as you need to work with on each sampleData callback. However, this can get confusing when you combine it with the speed changing code from my first example. So I’ve put together another example which uses this method:

The code is available for download here or you can see it below:

package com.kelvinluck.audio
{
   import flash.events.Event;
   import flash.events.SampleDataEvent;
   import flash.media.Sound;
   import flash.media.SoundChannel;
   import flash.net.URLRequest;
   import flash.utils.ByteArray;    

   /**
    * @author Kelvin Luck
    */

   public class MP3Player
   {
     
      public static const BYTES_PER_CALLBACK:int = 4096; // Should be >= 2048 && < = 8192

      private var _playbackSpeed:Number = 1;

      public function set playbackSpeed(value:Number):void
      {
         if (value < 0) {
            throw new Error('Playback speed must be positive!');
         }
         _playbackSpeed = value;
      }

      private var _mp3:Sound;
      private var _dynamicSound:Sound;
      private var _channel:SoundChannel;

      private var _phase:Number;
      private var _numSamples:int;

      public function MP3Player()
      {
      }

      public function loadAndPlay(request:URLRequest):void
      {
         _mp3 = new Sound();
         _mp3.addEventListener(Event.COMPLETE, mp3Complete);
         _mp3.load(request);
      }

      public function playLoadedSound(s:Sound):void
      {
         _mp3 = s;
         play();
      }
     
      public function stop():void
      {
         if (_dynamicSound) {
            _dynamicSound.removeEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData);
            _channel.removeEventListener(Event.SOUND_COMPLETE, onSoundFinished);
            _dynamicSound = null;
            _channel = null;
         }
      }

      private function mp3Complete(event:Event):void
      {
         play();
      }

      private function play():void
      {
         stop();
         _dynamicSound = new Sound();
         _dynamicSound.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData);
         
         _numSamples = int(_mp3.length * 44.1);
         
         _phase = 0;
         _channel = _dynamicSound.play();
         _channel.addEventListener(Event.SOUND_COMPLETE, onSoundFinished);
      }
     
      private function onSoundFinished(event:Event):void
      {
         _channel.removeEventListener(Event.SOUND_COMPLETE, onSoundFinished);
         _channel = _dynamicSound.play();
         _channel.addEventListener(Event.SOUND_COMPLETE, onSoundFinished);
      }

      private function onSampleData( event:SampleDataEvent ):void
      {
         var l:Number;
         var r:Number;
         var p:int;
         
         
         var loadedSamples:ByteArray = new ByteArray();
         var startPosition:int = int(_phase);
         _mp3.extract(loadedSamples, BYTES_PER_CALLBACK * _playbackSpeed, startPosition);
         loadedSamples.position = 0;
         
         while (loadedSamples.bytesAvailable > 0) {
           
            p = int(_phase - startPosition) * 8;
           
            if (p < loadedSamples.length - 8 && event.data.length <= BYTES_PER_CALLBACK * 8) {
               
               loadedSamples.position = p;
               
               l = loadedSamples.readFloat();
               r = loadedSamples.readFloat();
           
               event.data.writeFloat(l);
               event.data.writeFloat(r);
               
            } else {
               loadedSamples.position = loadedSamples.length;
            }
           
            _phase += _playbackSpeed;
           
            // loop
            if (_phase >= _numSamples) {
               _phase -= _numSamples;
               break;
            }
         }
      }
   }
}

You can compare it to the code in the original post to see the changes I made.

One thing to note is that there is still a delay when you load an MP3 in my example. This is because I am using the same FileReference.browse > Sound object hack as last time and this needs to loop over the entire loaded mp3 file while turning it into a Sound object. This wouldn’t be an issue in most use-cases where you have loaded the sound through Sound.load.

I also removed the option of playing the sound backwards in this example as that would have added further complexity to the code and hurt my head even more!



Tweetcoding – 140 characters of actionscript 3


Just over a week ago, Grant Skinner started a competition on Twitter called Tweetcoding. It’s very simple:
#tweetcoding: code something cool in <=140 characters of AS3
By happy coincidence this was just after I’d decided to give Twitter another chance and so I heard about the competition and decided to get involved. The first thing I did was to put together a quick tweetcoding minifier using jQuery – it lets you paste in your (slightly) readable AS3 code and it strips unnecessary whitespace and tells you how many characters you’ve used. Definitely much easier than the find and replace gymnastics I was doing in my text editor to start with!

Next I had big plans for creating a website to allow you to compile your own tweetcodes online. But all three of my approaches failed – I couldn’t trigger Java (and therefore mxmlc) from PHP on my shared host, I couldn’t piggyback on the wonderfl API (they use mxmlc behind the scenes too) and screaming donkey couldn’t handle the DisplayList. Luckily, Robert Cadena had the same idea and managed to execute it wonderfully and produce the tweetcoding compiling robot (I’m not sure what it’s really called!). You can visit that page and check out all of the great entries without compiling yourself.

The next step was to find a way to compile my tweetcoding from FDT – my preferred editor. I tried using Flash but it’s little code window annoyed me quickly and I don’t have CS4 yet so I couldn’t access any of the flash player 10 features. So I set up a project in FDT and created the following class:

package
{
   import flash.filters.*;
   import flash.text.TextField;  
   import flash.media.Microphone;  
   import flash.ui.Mouse;  
   import flash.events.*;        
   import flash.display.*;

   dynamic public class Tweetcode extends MovieClip
   {

      public var g:Graphics;
      public var mt:Function;
      public var lt:Function;
      public var ls:Function;
      public var m:Class;
      public var r:Function;
      public var s:Function;
      public var o:Object;
      public var i:Number = 0;

      public function Tweetcode():void
      {
         stage.scaleMode='noScale';
         stage.align='top';
         g = graphics;
         mt = g.moveTo;
         lt = g.lineTo;
         ls = g.lineStyle;
         m = Math;
         r = m.random;
         s = m.sin;
         o = {};

         addEventListener("enterFrame", f);
      }

      public function f(e:Event):void
      {
         // 140 characters here!
      }
   }
}

As you can see, it includes Grant’s gimmie code and space for me to add my code. I’ve added extra imports as I’ve needed them but I’m sure there are more that could be included. And I added some static typing to the variables even though this isn’t necessary – it does means I get a little help from FDT’s code hinting. Then I added ” –strict=false” to the mxmlc command line in my launch target and I was good to go :)

Below are some of my tweetcoding attempts in reverse chronological order (that doesn’t necessarily mean that they get better though!). Make sure you check out all of the other entries as well though – there is some incredible stuff. It is amazing what you can cram into so few characters!

Flickering Flame

A simple flame-like effect. View

g.clear(),o[++i]={x:mouseX,y:mouseY,b:9},filters=[new BlurFilter(4,4)];for each(p in o)a=p.b-=.2,ls(a,3e9,a),mt(p.x,p.y),lt(p.x+a,p.y--+a);

Windmill

Blow into your microphone to make it spin around!. View

if(i&lt;6.5) q=Microphone.getMicrophone(),q.setLoopBack(),ls(2),x=y=99,mt(0,0),lt(90*s(i),90*m.cos(i)),i+=m.PI/18;rotation+=q.activityLevel/9;

Colourful bondage

The coloured lines want to keep your mouse prisoner. View

g.clear(),Mouse.hide(),o[++i]={x:mouseX,y:mouseY,b:7,c:i< &lt;16+i<&lt;32};for each(p in o)a=p.b-=.3,ls(a,p.c,a),mt(p.x,p.y),lt(mouseX,mouseY);

Stripy wallpaper

Just some nice animating blue stripes. View

g.clear();t=stage,o[++i]={x:300+s(i)*300,b:9,c:9*i};for each(p in o)a=p.b-=.3,ls(a,p.c,a),l=p.a,mt(p.x,0),lt(p.x,t.stageHeight);

Mouse bubbles

Little bubble like particles escaping from the mouse. View

g.clear(),o[++i]={x:mouseX,y:mouseY,a:r()*9-5,b:r()*9};for each(p in o)a=p.b--,ls(2),p.a*=.9,p.b*=.9,mt(p.x+=p.a,p.y+=p.b),lt(p.x+1,p.y+1);

Ninja the mouse killing line

Weird title (I guess the tweetcoding had gone to my head) but probably my favourite of my entries. View

g.clear();o[++i]={x:mouseX,y:mouseY,a:o[i-1],b:9};for each(p in o)!p.a||(ls(p.b--),l=p.a,mt(p.x-=(p.x-l.x)/6,p.y-=(p.y-l.y)/6),lt(l.x,l.y));

Heartbeat

Short and simple. View

g.clear();o[++i]={x:i,y:99+s(i)*99,a:o[i-1]||{x:0,y:99}};for each(p in o)ls(1,p.x*0x020101),mt(p.x,p.y-=(p.y-p.a.y)/6),lt(p.x,p.y+2);

Marching ants

They follow the mouse and slowly straighten out over time. View

g.clear();ls(2);o[++i]={x:mouseX,y:mouseY,a:o[i-1]||{x:250,y:250}};for each(p in o)mt(p.x-=(p.x-p.a.x)/6,p.y-=(p.y-p.a.y)/6),lt(p.x+1,p.y);

Silly String

Stringy stuff falls from your mouse. View

g.clear();ls(2);o[++i]={x:mouseX,y:mouseY,a:o[i-1]||{x:9,y:9}};for each(p in o)mt(p.x-=(p.x-p.a.x)/6,p.y-=(p.y-p.a.y)/6),lt(p.a.x,p.a.y);

Random Silly String

Stringy stuff gets spread around the screen. View

g.clear();ls(1);o[++i]={x:500*r(),y:500*r(),a:o[i-1]||{x:9,y:9}};for each(p in o)mt(p.x-=(p.x-p.a.x)/6,p.y-=(p.y-p.a.y)/6),lt(p.a.x,p.a.y);

First try

This one is from before there was any gimmie code so it's 140 characters that run by themselves. Unsurprisingly, they do very little! View

var w=x=y=200,b,g=graphics,m=Math,l=g.lineTo,c=m.cos,s=m.sin,i=361;while(i--){g.lineStyle(1,m.random()*0xfff);l(w*c(i),w*s(i));l(0,0);}

Tweetcoding is great fun and seems to be part of a trend to impose constraints to trigger creativity. I've been a close follower of the 25 lines competition and been blown away by what people have achieved there. I'm also really interested in the 4k flash game competition and would love to find the time to put an entry together. It's incredible the amazing results you can get by abandoning anything like best practises and trying to squeeze something interesting out of a constrained situation. Good clean geek fun :D



Interviewed on actionscripthero.org


Last week I received an email Pablo Parrado of actionscripthero.org:
We’re trying to make ActionScriptHero.org a community portal that explores that social aspect of the Flash community and get to know the people behind the names.
He went on to ask me to take part in a series of interviews he is doing. I was honored to be asked and to add my thoughts to those of some leading lights of the flash world (42 in total at the moment). So I completed the interview and yesterday it went live. If you want to read my rambling thoughts about the past and future of flash then please head on over to my interview with actionscript hero. Thanks Pablo!

New This happened website


This happened is a series of events focusing on the stories behind interaction design. Having ideas is easier than making them happen. We delve into projects that exist today, how their concepts and production process can help inform future work.
This happened is an event originally organised by Chris O’Shea, Joel Gethin Lewis and Andreas Müller in London. I’ve been lucky enough to attend a few times and it is always really interesting and inspiring. The fact that the speakers talk about the process and the failures along the way is much more interesting than someone just showcasing their work. And I find the field of “interaction design” really interesting – a world somewhere between computers and the real world and somewhere between art and science.

So when Chris asked if I’d help out with a flickr viewer for the new website I was more than happy to help. I put together a little swf which is used throughout the site. It connects to the Flickr API (using as yet unfinished and unrealeased as3 version of flashr) and grabs photos matching certain machine tags. Depending on where in the site you are the swf displays relevant photos (e.g. from a particular city or a particular event) as a simple slidehow. The thishappened team can easily choose to display any photos that attendees have uploaded to flickr with relevant permissions. And they keep editorial control over the content via a special “auth” machine tag which they can generate through their CMS. It’s all pretty simple but it’s a nice way to bring user generated content to their site easily.

This happened is branching out and encouraging people to host events across the world so keep an eye out for one near you (or even set one up yourself) and if you get the chance make sure you attend.



Introducing shAIR – easily monetize your Adobe AIR applications


Update: shAIR is now called Sharify so I’ve updated the links below to point to the new website.

I am very pleased to announce the release of my latest project: shAIR. It is a product that is designed to be used by developers of Adobe AIR applications and makes it very easy for those developers to add “shAIRware” functionality to their applications.

shAIR

To quote Dr Woohoo – an early beta tester of shAIR:
shAIR solves the greatest challenge to the AIR platform for serious developers – how to integrate a trial period, registration & authentication – by creating a simple solution that helps protect and commoditize their applications and intellectual property. It’s simply brilliant!
shAIR is a combination of an as3 swc file and the website/ API which this connects to. A developer integrates the swc into their application and uses the administration panel on the shAIR website to create and administer licenses for the application.

The beautiful identity and website design was done by my good friends over at hoppermagic – thanks guys :) You should get in touch with them if you want to commision any similarily exquisite work!

The launch of shAIR is also interesting because it marks a change in the direction of my company (Luck Laboratories Ltd) from a purely consultancy based company to one which also produces it’s own products. It’s going to be an interesting ride but I’m really excited by the possibilities for shAIR and looking around the web I think it is something that lots of people have been looking for.



First steps with flash 10 audio programming


As I was reading my RSS feeds yesterday I came across a blog post by Andre Michelle where he released some sourcecode for using the new Sound APIs in Flash Player 10. I had a little spare time so I decided to finally set up FDT to allow me to author flash 10 swfs (which was easier than I expected) so I could do some playing.

The idea was to re-create my wave sequencer experiment using the APIs but to get started I did something simpler. I wrote a little class which allows you to load an MP3 file and play it back with the ability to change the playback speed dynamically. Here it is:

You can see the sourcecode for the relevant file below. The interesting stuff from an audio point of view is happening in the onSampleData callback. This is triggered by the Flash player whenever it needs a new buffer of audio samples to play. The code in that function is commented and hopefully pretty self explanatory. It is derived from code in my old wave sequencer experiment which was itself derived from some code in the popforge library.

package com.kelvinluck.audio
{
   import flash.events.Event;
   import flash.events.SampleDataEvent;
   import flash.media.Sound;
   import flash.net.URLRequest;
   import flash.utils.ByteArray;

   /**
    * @author Kelvin Luck
    */

   public class MP3Player
   {

      private var _playbackSpeed:Number = 1;

      public function set playbackSpeed(value:Number):void
      {
         _playbackSpeed = value;
      }

      private var _mp3:Sound;
      private var _loadedMP3Samples:ByteArray;
      private var _dynamicSound:Sound;

      private var _phase:Number;
      private var _numSamples:int;

      public function MP3Player()
      {
      }

      public function loadAndPlay(request:URLRequest):void
      {
         _mp3 = new Sound();
         _mp3.addEventListener(Event.COMPLETE, mp3Complete);
         _mp3.load(request);
      }

      public function playLoadedSound(s:Sound):void
      {
         var bytes:ByteArray = new ByteArray();
         s.extract(bytes, int(s.length * 44.1));
         play(bytes);
      }
     
      public function stop():void
      {
         if (_dynamicSound) {
            _dynamicSound.removeEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData);
            _dynamicSound = null;
         }
      }

      private function mp3Complete(event:Event):void
      {
         playLoadedSound(_mp3);
      }

      private function play(bytes:ByteArray):void
      {
         stop();
         _dynamicSound = new Sound();
         _dynamicSound.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData);
         
         _loadedMP3Samples = bytes;
         _numSamples = bytes.length / 8;
         
         _phase = 0;
         _dynamicSound.play();
      }

      private function onSampleData( event:SampleDataEvent ):void
      {
         
         var l:Number;
         var r:Number;
         
         var outputLength:int = 0;
         while (outputLength < 2048) {
            // until we have filled up enough output buffer
           
            // move to the correct location in our loaded samples ByteArray
            _loadedMP3Samples.position = int(_phase) * 8; // 4 bytes per float and two channels so the actual position in the ByteArray is a factor of 8 bigger than the phase
           
            // read out the left and right channels at this position
            l = _loadedMP3Samples.readFloat();
            r = _loadedMP3Samples.readFloat();
           
            // write the samples to our output buffer
            event.data.writeFloat(l);
            event.data.writeFloat(r);
           
            outputLength++;
           
            // advance the phase by the speed...
            _phase += _playbackSpeed;
           
            // and deal with looping (including looping back past the beginning when playing in reverse)
            if (_phase < 0) {
               _phase += _numSamples;
            } else if (_phase >= _numSamples) {
               _phase -= _numSamples;
            }
         }
      }
   }
}

As you can see, there are three public methods in the above class. loadAndPlay will load an mp3 file into a sound object and start playing it at the desired playbackSpeed. stop will stop the currently playing mp3. And playLoadedSound will start playing an already loaded sound object at the desired playbackSpeed. This is useful if you have already preloaded your sound objects but it is also useful for another important reason as you can see in the demo.

Thanks to some great work from an old friend of mine, it is possible to dynamically create a Sound object based on an MP3 loaded through the new FileReference.load() functionality in Flash 10. This is why in the demo you can browse for an mp3 file on your local machine which can then be dynamically controlled by Flash immediately without sending it to a server first.

You can download the complete FDT project of my demo here if you want to look through all of the code. I’m excited by the possibilities that are opening up in flash now that Adobe made some noise – I’ve got a long way to go before I can do anything nearly as incredible as the Hobnox audio tool but I’ve got some ideas and I’m looking forward to playing around with them :)

Update: Check out my follow on post where I examine how to extract the audio on demand rather than up front.



Experiment with Papervision 3D particles and effects


A while back I was prototyping something for a client which involved lots of red dots moving around in 3D space, realised using Papervision 3D. I didn’t end up persuing this route with the client in the end but the effect was pretty cool so I thought I might as well share it here.

The idea is that there is a bunch of particles who are bouncing around randomly stuck within an invisible cube. The effect looked OK by itself but then I decided to try adding effects. I used a BlurFilter and a BitmapColorEffect to give the each of the particles trails. Then I changed the clipping point like in the original borg cube effects demo to give the impression of the particles falling. I like this version the best – if you move your mouse from side to side around the bottom of the demo swf then it starts to look like some kind of flocking is going on (like in my perlin noise experiment).

Click on the image below to see the demo. Click inside the demo swf to give it focus and then you can use the following keys:
  • 1 – Sets the render mode to normal clean particles (the default).
  • 2 – Sets the render mode to particles with trails.
  • 3 – Sets the render mode to falling particles with trails.
  • c – Toggles display of a cube showing the area the particles are contained within.

Particles and effects in Papervision 3D

The sourcecode for this example is pretty simple. You can see it below or you can download it from here.

package  
{
   import org.papervision3d.core.effects.BitmapColorEffect;
   import org.papervision3d.core.effects.BitmapLayerEffect;
   import org.papervision3d.core.geom.Particles;
   import org.papervision3d.materials.WireframeMaterial;
   import org.papervision3d.materials.utils.MaterialsList;
   import org.papervision3d.objects.DisplayObject3D;
   import org.papervision3d.objects.primitives.Cube;
   import org.papervision3d.view.AbstractView;
   import org.papervision3d.view.BasicView;
   import org.papervision3d.view.layer.BitmapEffectLayer;
   
   import flash.display.StageQuality;
   import flash.events.Event;
   import flash.events.KeyboardEvent;
   import flash.filters.BlurFilter;
   import flash.geom.Point;      

   /**
    * @author Kelvin Luck
    */

   [SWF(width='450', height='450', backgroundColor='#000000', frameRate='41')]

   public class ParticlesCube extends BasicView
   {
     
      public static const NUM_PARTICLES:int = 300;
      public static const CONTAINING_CUBE_SIZE:int = 500;
     
      public static const RENDER_MODE_CLEAN:int = 0;
      public static const RENDER_MODE_TRAILS:int = 1;
      public static const RENDER_MODE_FALLING:int = 2;
     
      private var particlesContainer:DisplayObject3D;
      private var particlesHolder:Particles;
      private var particles:Array;
      private var boundsCube:Cube;

      private var bfx:BitmapEffectLayer;
     
      private var _renderMode:int;
      public function set renderMode(value:int):void
      {
         if (value == _renderMode) return;
         
         clearBitmapEffects();
         
         var clippingPoint:Point = new Point();
         
         switch (value) {
            case RENDER_MODE_CLEAN:
               // nothing - effects already cleared above...
               break;
            case RENDER_MODE_FALLING:
               clippingPoint.y = -2;
               // fall through...
            case RENDER_MODE_TRAILS:
               bfx = new BitmapEffectLayer(viewport, stage.stageWidth, stage.stageHeight, true, 0xffffff);
               
               bfx.addEffect(new BitmapLayerEffect(new BlurFilter(2, 2, 2)));
               bfx.addEffect(new BitmapColorEffect(1, 1, 1, .9));
               
               bfx.clippingPoint = clippingPoint;
               
               bfx.addDisplayObject3D(particlesHolder);
               
               viewport.containerSprite.addLayer(bfx);
               break;
            default:
               throw new Error(value + ' is an invalid render mode');
         }
         _renderMode = value;
      }
     
      private var _displayCube:Boolean = true;
      public function set displayCube(value:Boolean):void
      {
         if (value != _displayCube) {
            _displayCube = value;
            boundsCube.visible = value;
         }
      }

      public function ParticlesCube()
      {
         super(550, 550);
         
         stage.quality = StageQuality.MEDIUM;
         
         particlesContainer = new DisplayObject3D();
         scene.addChild(particlesContainer);
         
         var cubeMaterial:WireframeMaterial = new WireframeMaterial(0x0000ff, 1, 2);
         var materialsList:MaterialsList = new MaterialsList();
         materialsList.addMaterial(cubeMaterial, 'all');
         
         boundsCube = new Cube(materialsList, CONTAINING_CUBE_SIZE, CONTAINING_CUBE_SIZE, CONTAINING_CUBE_SIZE);
         particlesContainer.addChild(boundsCube);
         displayCube = false;
         
         particlesHolder = new Particles();
         particlesContainer.addChild(particlesHolder);
         
         init(NUM_PARTICLES, CONTAINING_CUBE_SIZE);
         
         stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
         
         startRendering();
      }

      public function init(numParticles:int, containingCubeSize:int):void
      {
         var movingParticle:MovingParticle;
         
         if (particles) {
            particlesHolder.removeAllParticles();
         }
         
         particles = [];
         
         var i:int = numParticles;
         while (i--) {
            movingParticle = new MovingParticle(containingCubeSize);
            particlesHolder.addParticle(movingParticle);
            particles.push(movingParticle);
         }
         
      }

      override protected function onRenderTick(event:Event = null):void
      {
         // move each particle
         var movingParticle:MovingParticle;
         for each (movingParticle in particles) {
            movingParticle.position();
         }
         
         // twist the container based on mouse position
         particlesContainer.rotationY+=((stage.stageWidth/2)-mouseX)/200;
         particlesContainer.rotationX+=((stage.stageHeight/2)-mouseY)/200;
         
         // render
         super.onRenderTick(event);
      }
     
      private function clearBitmapEffects():void
      {
         if (bfx) {
            viewport.containerSprite.removeLayer(bfx);
            bfx = null;
         }
      }
     
      private function onKeyDown(event:KeyboardEvent):void
      {
         switch (String.fromCharCode(event.keyCode)) {
            case '1':
               renderMode = RENDER_MODE_CLEAN;
               break;
            case '2':
               renderMode = RENDER_MODE_TRAILS;
               break;
            case '3':
               renderMode = RENDER_MODE_FALLING;
               break;
            case 'c':
            case 'C':
               displayCube = !_displayCube;
               break;
         }
      }
   }
}

import org.papervision3d.core.geom.renderables.Particle;
import org.papervision3d.materials.special.ParticleMaterial;

internal class MovingParticle extends Particle
{
   
   public static const PARTICLE_SIZE:int = 10;
   public static const MAX_SPEED:int = 5;
   
   private var dX:Number;
   private var dY:Number;
   private var dZ:Number;
   private var halfSize:Number;

   public function MovingParticle(containingCubeSize:int)
   {
      var mat:ParticleMaterial = new ParticleMaterial(0xff0000, 1, ParticleMaterial.SHAPE_CIRCLE);
      super(mat, PARTICLE_SIZE);
     
      var size:int = containingCubeSize;
      halfSize = size / 2;
     
      x = (Math.random() * size) - halfSize;
      y = (Math.random() * size) - halfSize;
      z = (Math.random() * size) - halfSize;
     
      dX = Math.random() * MAX_SPEED;
      dY = Math.random() * MAX_SPEED;
      dZ = Math.random() * MAX_SPEED;
     
   }
   
   public function position():void
   {
      x += dX;
      if (x > halfSize || x < -halfSize) dX *= -1;
      y += dY;
      if (y > halfSize || y < -halfSize) dY *= -1;
      z += dZ;
      if (z > halfSize || z < -halfSize) dZ *= -1;
   }
}
// This line is just to stop the code formatter on my blog getting confused! >