Reproductor de música en Actionscript 3, usando POO y patrón MVC

Hasta aquí todo el código necesario. De todas formas, como las clases Control y Reproductor las he explicado a trozos y saltando de una a otra por conveniencia, vuelvo a mostrar su código completo:

clase Control:

package controlador {

	import flash.display.Sprite;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.net.SharedObject;
	import modelo.Playlist;
	import modelo.Cancion;
	import vista.Reproductor;
	import vista.Tip;
	import fl.controls.List;
	import fl.controls.Slider;
	import flash.text.TextField;
	import fl.events.ListEvent;

	public class Control {

		private var _reproductor:Reproductor;
		private var _playlist:Playlist;
		private var _shared:SharedObject;
		private const RUTA_LISTA_XML:String = "http://www.fast-forward.es/activos/flash/reproductor mp3/playlist.xml";

		public function Control(padre:Sprite) {
			// Crear y mostrar reproductor
			_reproductor = new Reproductor();
			_reproductor.x = padre.stage.stageWidth / 2 - _reproductor.width / 2;
			_reproductor.y = padre.stage.stageHeight / 2 - _reproductor.height / 2;
			padre.addChild(_reproductor);
			// Cargar datos externos de la playlist
			_shared = SharedObject.getLocal("playlist");
			if (_shared.data.lista is Array && _shared.data.lista[0] is Cancion) {
				// Hay datos locales almacenados de la playlist
				// Obtenerlos del Shared Object y pasarlos al método de creación de la playlist
				crearPlaylist(_shared.data.lista);
			} else {
				// No hay datos locales almacenados de la playlist
				// Se carga la playlist por defecto desde archivo xml
				var datosXML:URLLoader = new URLLoader(new URLRequest(RUTA_LISTA_XML));
				datosXML.addEventListener(Event.COMPLETE, onDatosXML);
			}
		}

		private function onDatosXML(e:Event):void {
			var xml:XML = new XML(e.target.data);
			crearPlaylist(xml);
		}

		private function crearPlaylist(datos:*):void {
			_playlist = new Playlist(datos);
			// Una vez creada la playlist, mostrarla en el List del reproductor,
			mostrarLista();
			// Una vez la lista completada, se puede inicializar reproductor para su reproducción
			_reproductor.inicializar(_playlist);
			// y llamar al método de control del reproductor
			controlReproductor();
		}

		private function mostrarLista():void {
			// Obtener referencia al List del reproductor
			var list:List = _reproductor.getChildByName("listaCanciones") as List;
			// Recorrer la playlist y obtener canción a canción, procesándolas
			// de forma que se puedan añadir a la lista
			var obj:Object;
			var cancion:Cancion;
			for (var i:uint = 0; i < _playlist.lista.length; i++) {
				cancion = _playlist.lista[i];
				obj = {data:cancion.id, label:cancion.artista + " - " + cancion.nombre};
				list.addItem(obj);
			}
			list.selectedIndex = 0;
		}

		// Método que asigna el control de interacciones a los detectores
		private function controlReproductor():void {
			// Obtener referencias a los sprites gráficos ya creados del reproductor
			var display_txt:TextField = _reproductor.getElemento("temaActual_txt") as TextField;
			var botonPlay:Sprite = _reproductor.getElemento("botonPlay") as Sprite;
			var botonStop:Sprite = _reproductor.getElemento("botonStop") as Sprite;
			var botonPrevious:Sprite = _reproductor.getElemento("botonPrevious") as Sprite;
			var botonNext:Sprite = _reproductor.getElemento("botonNext") as Sprite;
			var botonNoRepeat:Sprite = _reproductor.getElemento("botonNoRepeat") as Sprite;
			var botonRandomOff:Sprite = _reproductor.getElemento("botonRandomOff") as Sprite;
			var botonAbrir:Sprite = _reproductor.getElemento("botonAbrir") as Sprite;
			var list:List = _reproductor.getElemento("listaCanciones") as List;
			var bVolumen:Slider = _reproductor.getElemento("barraVolumen") as Slider;

			// Poner inicialmente el elemento seleccionado en el display del reprod.
			display_txt.text = list.getItemAt(list.selectedIndex).label;

			// Control de los eventos:

			// Controles de reproducción
			botonPlay.addEventListener(MouseEvent.CLICK, accionReproductor);
			botonStop.addEventListener(MouseEvent.CLICK, accionReproductor);
			botonPrevious.addEventListener(MouseEvent.CLICK, accionReproductor);
			botonNext.addEventListener(MouseEvent.CLICK, accionReproductor);

			botonPlay.doubleClickEnabled = true;
			botonStop.doubleClickEnabled = true;
			botonPrevious.doubleClickEnabled = true;
			botonNext.doubleClickEnabled = true;
			botonPlay.addEventListener(MouseEvent.DOUBLE_CLICK, accionReproductor);
			botonStop.addEventListener(MouseEvent.DOUBLE_CLICK, accionReproductor);
			botonPrevious.addEventListener(MouseEvent.DOUBLE_CLICK, accionReproductor);
			botonNext.addEventListener(MouseEvent.DOUBLE_CLICK, accionReproductor);

			// Controles de opciones de reproducción
			botonNoRepeat.addEventListener(MouseEvent.CLICK, accionReproductor);
			botonRandomOff.addEventListener(MouseEvent.CLICK, accionReproductor);
			list.addEventListener(ListEvent.ITEM_DOUBLE_CLICK, accionReproductor);
			list.addEventListener(Event.CHANGE, accionReproductor);
			bVolumen.addEventListener(Event.CHANGE, accionReproductor);

			// Controles de hints o mensajes contextuales
			botonNoRepeat.addEventListener(MouseEvent.ROLL_OVER, crearTip);
			botonNoRepeat.addEventListener(MouseEvent.ROLL_OUT, cerrarTip);
			botonRandomOff.addEventListener(MouseEvent.ROLL_OVER, crearTip);
			botonRandomOff.addEventListener(MouseEvent.ROLL_OUT, cerrarTip);
			bVolumen.addEventListener(MouseEvent.ROLL_OVER, crearTip);
			bVolumen.addEventListener(MouseEvent.ROLL_OUT, cerrarTip);
		}

		// Manejador de los eventos de ROLL_OVER en botones de opción
		private function crearTip(e:MouseEvent):void {
			var b:Sprite = e.currentTarget as Sprite;
			switch (b.name) {
				case "botonNoRepeat":
					Tip.crear(_reproductor, "Repeat Off");
				break;
				case "botonRepeatOnce":
					Tip.crear(_reproductor, "Repeat One");
				break;
				case "botonRepeatAll":
					Tip.crear(_reproductor, "Repeat All");
				break;
				case "botonRandomOff":
					Tip.crear(_reproductor, "Random Off");
				break;
				case "botonRandomOn":
					Tip.crear(_reproductor, "Random On");
				break;
				case "barraVolumen":
					Tip.crear(_reproductor, "Volumen");
					b.removeEventListener(MouseEvent.ROLL_OVER, crearTip);
					//b.removeEventListener(MouseEvent.ROLL_OUT, cerrarTip);
				break;
			}
		}

		// Manejador de evento ROLL_OUT en botones de opción
		private function cerrarTip(e:MouseEvent):void {
			Tip.cerrar();
		}

		// Manejador de eventos de CLICK y DOUBLE_CLICK en botones
		private function accionReproductor(e:Event):void {
			var boton:Sprite = e.currentTarget as Sprite;
			var botonPlay, botonPause, botonRep, botonRandom:Sprite;
			trace(boton);
			switch (boton.name) {
				case "botonPlay":
					_reproductor.play();
					botonPause = new BotonPause();
					intercambiar(boton, botonPause, "botonPause");
				break;
				case "botonStop":
					_reproductor.stop();
					if ((botonPause = Sprite(_reproductor.getChildByName("botonPause"))) != null) {
						botonPlay = new BotonPlay();
						intercambiar(botonPause, botonPlay, "botonPlay");
					}
				break;
				case "botonPause":
					_reproductor.pause();
					botonPlay = new BotonPlay();
					intercambiar(boton, botonPlay, "botonPlay");
				break;
				case "botonPrevious":
					_reproductor.previous();
				break;
				case "botonNext":
					_reproductor.next();
				break;
				case "botonNoRepeat":
					// Al clicar "no repeat" pasamos al estado "repeat once", y poner su icono correspondiente
					_reproductor.estadoRepeat = 1;
					botonRep = new BotonRepeatOnce();
					intercambiar(boton, botonRep, "botonRepeatOnce");
				break;
				case "botonRepeatOnce":
					// Al clicar "repeat once" pasamos al estado "repeat all", y ponemos su icono
					_reproductor.estadoRepeat = 2;
					botonRep = new BotonRepeatAll();
					intercambiar(boton, botonRep, "botonRepeatAll");
				break;
				case "botonRepeatAll":
					// Al clicar "repeat all" pasamos al estado "no repeat", y ponemos su icono
					_reproductor.estadoRepeat = 0;
					botonRep = new BotonNoRepeat();
					intercambiar(boton, botonRep, "botonNoRepeat");
				break;
				case "listaCanciones":
					// Comprobar si se trata de un doble clic en un item o de un cambio de item
					trace("Tipo de evento: " + e.type);
					if (e.type == Event.CHANGE) {
						_reproductor.reset();
					}
					if (e.type == ListEvent.ITEM_DOUBLE_CLICK) {
						if (_reproductor.isPlaying) {
							_reproductor.stop();
						}
						_reproductor.play();
						if ((botonPlay = _reproductor.getChildByName("botonPlay") as Sprite) != null) {
							botonPause = new BotonPause();
							intercambiar(botonPlay, botonPause, "botonPause");
						}
					}
				break;
				case "botonRandomOff":
					// Al hacer clic en "estado random off", pasamos a "estado random on" y mostramos su botón
					_reproductor.estadoRandom = true;
					botonRandom = new BotonRandomOn();
					intercambiar(boton, botonRandom, "botonRandomOn");
				break;
				case "botonRandomOn":
					// Al hacer clic en "estado random on", pasamos a "estado random off" y mostramos su botón
					_reproductor.estadoRandom = false;
					botonRandom = new BotonRandomOff();
					intercambiar(boton, botonRandom, "botonRandomOff");
				break;
				case "barraVolumen":
					// Ordenar al reproductor que se actualice su volumen dependiendo del
					// nuevo valor del slider "barraVolumen"
					_reproductor.cambiarVolumen();
			}
		}

		private function intercambiar(boton1:Sprite, boton2:Sprite, nomBoton2:String):void {
			// Por si había un tip abierto, cerrarlo
			Tip.cerrar();
			// Intercambio
			boton2.x = boton1.x;
			boton2.y = boton1.y;
			boton2.name = nomBoton2;
			_reproductor.addChild(boton2);
			// Añadir detectores correspondientes para el nuevo botón
			boton2.addEventListener(MouseEvent.CLICK, accionReproductor);
			boton2.addEventListener(MouseEvent.ROLL_OVER, crearTip);
			boton2.addEventListener(MouseEvent.ROLL_OUT, cerrarTip);
			// Eliminar detectores del botón que se va a eliminar
			boton1.removeEventListener(MouseEvent.CLICK, accionReproductor);
			boton1.removeEventListener(MouseEvent.ROLL_OVER, crearTip);
			boton1.removeEventListener(MouseEvent.ROLL_OUT, cerrarTip);
			_reproductor.removeChild(boton1);
		}
	}
}

Clase Reproductor:

package vista {

	import flash.display.Sprite;
	import flash.display.DisplayObject;
	import modelo.Playlist;
	import modelo.Cancion;
	import vista.BarraTiempo;
	import flash.media.Sound;
	import flash.media.SoundChannel;
	import flash.media.SoundTransform;
	import flash.net.URLRequest;
	import flash.events.Event;
	import flash.utils.Timer;
	import flash.events.TimerEvent;
	import flash.text.TextFormat;
	import flash.events.ProgressEvent;
	import flash.filters.DropShadowFilter;
	import flash.events.MouseEvent;
	import flash.display.Graphics;
	import fl.managers.StyleManager;
	import flash.text.Font;
	import fl.controls.List;
	import fl.controls.Slider;

	public class Reproductor extends Sprite {

		private var _playlist:Playlist;
		private var _sonido:Sound;
		private var _channel:SoundChannel;
		private var _posicion:Number;
		private var _timer:Timer;
                private var _tiempoCargado:Number; // establece diferencia entre bytes cargados y totales
                                             // para estimar tiempo total de la canción mientras se carga
		private var _list:List;
		private var _barraTiempo:BarraTiempo;

		private var _estadoRepro:uint = 0; // 0 -> parado, 1 -> play, 2 - > pause
		private var _estadoRepeat:uint = 0; // 0 -> No repetir (solo una secuencia), 1 -> Repetir una, 2 - > Repetir todo
		private var _estadoRandom:Boolean = false;
		private var _playing:Boolean = false;
		private var _volumen:Number;

		public function Reproductor() {
			_barraTiempo = this.getChildByName("barraTiempo") as BarraTiempo;
			_barraTiempo.addEventListener(_barraTiempo.PUNTERO_SOLTADO, nuevaPosicion);
			_posicion = 0;
			_volumen = 1;
			// Añadir efecto de sombra al reproductor
			var sombra:DropShadowFilter = new DropShadowFilter(4, 45, 0, .4, 5, 5);
			this.filters = [sombra];
			// Añadir área de arrastre del reproductor
			var zonaActiva:Sprite = new Sprite();
			var g:Graphics = zonaActiva.graphics;
			g.beginFill(0x000000, 0);
			g.drawRect(0, 0, this.width, 15);
			g.endFill();
			addChild(zonaActiva);
			zonaActiva.addEventListener(MouseEvent.MOUSE_DOWN, moverReproductor);
			zonaActiva.addEventListener(MouseEvent.MOUSE_UP, soltarReproductor);
			zonaActiva.buttonMode = true;
		}

		private function moverReproductor(e:MouseEvent):void {
			this.startDrag();
		}

		private function soltarReproductor(e:MouseEvent):void {
			this.stopDrag();
		}

		// Una vez disponible una playlist, se inicializa con ella el reproductor (su lista de canciones)
		public function inicializar(playlist:Playlist):void {
			_list = this.getChildByName("listaCanciones") as List;
			formatear(_list);
			_playlist = playlist;
			_timer = new Timer(10, 0);
			_timer.addEventListener(TimerEvent.TIMER, onTimer);
		}

		private function nuevaPosicion(e:Event):void {
			if (_channel != null) {
				var ratio:Number = _barraTiempo.xPuntero / _barraTiempo.width;
				this.stop();
				_posicion = _sonido.length * ratio;
				trace("Buscar nueva posicion... " + _posicion);
				this.play();
			}
		}

		public function getElemento(nombre:String):DisplayObject {
			return this.getChildByName(nombre);
		}

		public function play():void {
			if (_playlist != null) {
				_list.scrollToSelected();
				var indice:uint = _list.selectedIndex;
				var ruta:String = _playlist.lista[indice].ruta;
				if (_channel != null && mismoTema(ruta)) {
					_channel = _sonido.play(_posicion);
				} else {
					crearSonido(ruta);
					var display_txt:TextField = this.getChildByName("temaActual_txt") as TextField;
					display_txt.text = _list.getItemAt(_list.selectedIndex).label;
				}
				_channel.addEventListener(Event.SOUND_COMPLETE, finCancion);
				var soundTrans:SoundTransform = _channel.soundTransform;
				soundTrans.volume = _volumen;
				_channel.soundTransform = soundTrans;
				_playing = true;
				_timer.start();
			}
		}

		// Comprueba si el sonido actual y el seleccionado en la lista coinciden, o se ha cambiado
		// la selección.
		private function mismoTema(ruta:String):Boolean {
			if (_sonido.url.indexOf(ruta) != -1) {
				// El sonido a reproducir o reproduciéndose coincide con el seleccionado en la lista
				return true
			} else {
				// Se ha cambiado el sonido seleccionado en la lista
				return false;
			}
		}

		private function crearSonido(ruta:String):void {
			if (_sonido != null) {
				_sonido.removeEventListener(ProgressEvent.PROGRESS, datosRecibidos);
				_sonido = null;
			}
			_sonido = new Sound(new URLRequest(ruta));
			_sonido.addEventListener(ProgressEvent.PROGRESS, datosRecibidos);
			trace("Reproduciendo sonido " + _sonido + " desde " + _posicion);
			_channel = _sonido.play(_posicion);
		}

		private function datosRecibidos(e:ProgressEvent):void {
			_tiempoCargado = e.bytesLoaded / e.bytesTotal;
			_barraTiempo.actualizarBuffer(_tiempoCargado);
		}

		private function finCancion(e:Event):void {
			next();
		}

		public function stop():void {
			if (_channel != null) {
				_channel.stop();
				_posicion = 0;
				_playing = false;
				_timer.stop();
			}
			// Actualizar display de tiempo a 0
			var timer_txt:TextField = this.getChildByName("time_txt") as TextField;
			timer_txt.text = "00:00";
			// Resetear barra de tiempo
			_barraTiempo.reset();
		}

		public function pause():void {
			// Comprobar si se ha cambiado de canción en la lista.
			// Si se ha cambiado, el pause se convertirá en un stop y la reproducción se reanudará al inicio de la nueva canción.
			// Si no ha habido cambio, se almacena la posición donde ha quedado la reproducción para reanudar ahí
			if (mismoTema(_playlist.lista[_list.selectedIndex].ruta)) {
				_posicion = _channel.position;
				_channel.stop();
				_playing = false;
				_timer.stop();
			} else {
				this.stop();
			}
		}

		public function previous():void {
			var indice:int = _list.selectedIndex;

			if (_estadoRepeat == 1) {
				// Modo repetir una, se reproduce el principio de la misma

			} else if (_estadoRepeat == 0 && !_estadoRandom) {
				// Modo aleatorio en off, y en modo no repetir
				if (_list.selectedIndex == 0) {
					// Estaba reproduciendose primera cancion de playlist
					// Se sigue en la primera canción
					indice = 0;
				} else {
					// No estaba reproduciendose primera cancion de playlist
					indice = _list.selectedIndex - 1;
				}
				_list.selectedIndex = indice;

			} else if (_estadoRandom && _estadoRepeat != 1) {
				// Modo aleatorio ON, y modo reproduccion no es repetir solo una
				indice = Math.floor(Math.random()*(_list.length - 1));
				_list.selectedIndex = indice;
				trace("Reproducir aleatoriamente tema de indice " + indice);

			} else if (!_estadoRandom && _estadoRepeat == 2) {
				// Modo random OFF y modo repetir todas
				if (_list.selectedIndex > 0) {
					// No es principio de lista principal
					indice = _list.selectedIndex - 1;
				} else {
					// Es principio de lista principal
					indice = _list.length - 1;
				}
				_list.selectedIndex = indice;
			}

			if (this._playing) {
				this.stop();
				this.play();
			}
		}

		public function next():void {
			var indice:int = _list.selectedIndex;

			if (_estadoRepeat == 1) {
				// Modo repetir una, se reproduce el principio de la misma

			} else if (_estadoRepeat == 0 && !_estadoRandom) {
				// Modo no repeat, secuencial no aleatorio (random OFF)
				if (_list.selectedIndex < _list.length - 1) {
					// No es final de lista principal
					indice = _list.selectedIndex + 1;
				} else {
					// Es final de lista principal
					// No hacer nada
				}
				_list.selectedIndex = indice;

			} else if (_estadoRandom && _estadoRepeat != 1) {
				// Modo random ON y no activo modo repetir una
				indice = Math.floor(Math.random()*(_list.length - 1));
				_list.selectedIndex = indice;

			} else if (!_estadoRandom && _estadoRepeat == 2) {
				// Modo random OFF y modo repetir todas
				if (_list.selectedIndex < _list.length - 1) {
					// No es final de lista principal
					indice = _list.selectedIndex + 1;
				} else {
					// Es final de lista principal
					indice = 0;
				}
				_list.selectedIndex = indice;

			}

			if (this._playing) {
				this.stop();
				this.play();
			}
		}

		public function cambiarVolumen():void {
			// Actualizar volumen del reproductor dependiendo del nuevo valor del slider
			var volumen:Slider = this.getChildByName("barraVolumen") as Slider;
			_volumen = volumen.value;
			if (_channel != null) {
				var soundTrans:SoundTransform = _channel.soundTransform;
				soundTrans.volume = _volumen;
				_channel.soundTransform = soundTrans;
			}
		}

		private function onTimer(e:TimerEvent):void {
			// Actualizar display de tiempo dependiendo de posición del canal de audio
			var timer_txt:TextField = this.getChildByName("time_txt") as TextField;
			timer_txt.text = displayTime(_channel.position);
			_barraTiempo.actualizar(_channel.position, _sonido.length / _tiempoCargado);
		}

		private function displayTime(pos:Number):String {
			var horas, minutos, segundos:uint;
			var output:String;
			var totalSec:uint = pos / 1000;
			var resto:uint;
			horas = Math.floor(totalSec / (60*60));
			resto = totalSec % (60*60);
			minutos = Math.floor(resto / 60);
			resto = resto % 60;
			segundos = Math.floor(resto);
			output = ((horas > 0) ? String(horas) + ":" : "") +
					((minutos > 9) ? String(minutos) : "0" + String(minutos)) + ":" +
					((segundos > 9) ? String(segundos) : "0" + String(segundos));
			return output;
		}

		private function formatear(lista:List):void {
			var fuente:Font = new CenturyGothicRegular(); // Importar fuente incrustada en biblioteca
			var formatoTexto:TextFormat = new TextFormat();
			formatoTexto.color = 0x666688;
			formatoTexto.letterSpacing = .5;
			formatoTexto.font = fuente.fontName;
			StyleManager.setStyle("textFormat", formatoTexto);
		}

		public function set estadoRepeat(estado:uint):void {
			_estadoRepeat = estado;
		}

		public function set estadoRandom(estado:Boolean):void {
			_estadoRandom = estado;
		}

		public function get isPlaying():Boolean {
			return _playing;
		}

		public function reset():void {
			_posicion = 0;
			_list.scrollToSelected();
		}
	}
}

Hasta aquí el reproductor de música. Espero que os sea de utilidad. Cualquier comentario, sugerencia y crítica constructiva es bienvenida. :)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>