From: yomguy Date: Thu, 17 Jun 2010 16:54:43 +0000 (+0000) Subject: make it a real module with a setup X-Git-Tag: 0.3.2~151 X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=7cc3adee4c999d02a52c5f96b979521932bf8d52;p=timeside.git make it a real module with a setup --- diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 81ad004..0000000 --- a/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- - -from core import * -from metadata import Metadata -import decoder -import encoder -import analyzer -import grapher diff --git a/analyzer/__init__.py b/analyzer/__init__.py deleted file mode 100644 index 0bd2039..0000000 --- a/analyzer/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# -*- coding: utf-8 -*- -from timeside.analyzer.core import * -from timeside.analyzer.duration import * -from timeside.analyzer.max_level import * -#from timeside.analyzer.mean_level import * -#from timeside.analyzer.dc import * diff --git a/analyzer/core.py b/analyzer/core.py deleted file mode 100644 index cbb9a17..0000000 --- a/analyzer/core.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Authors: -# Guillaume Pellerin - -pass - - diff --git a/analyzer/dc.py b/analyzer/dc.py deleted file mode 100644 index e92cfd5..0000000 --- a/analyzer/dc.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -from timeside.analyzer.core import * -from timeside.api import IValueAnalyzer -import numpy - - -class MeanDCShift(Processor): - implements(IValueAnalyzer) - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(MeanDCShift, self).setup(channels, samplerate, nframes) - self.value = 0 - - @staticmethod - @interfacedoc - def id(): - return "dc" - - @staticmethod - @interfacedoc - def name(): - return "Mean DC shift" - - @staticmethod - @interfacedoc - def unit(): - return "%" - - def __str__(self): - return "%s %s" % (str(self.value), unit()) - - def process(self, frames, eod=False): - self.value = numpy.round(100*numpy.mean(samples),4) - return frames, eod - - def result(self): - return self.value - diff --git a/analyzer/duration.py b/analyzer/duration.py deleted file mode 100644 index 7c2269c..0000000 --- a/analyzer/duration.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter -from timeside.analyzer.core import * -from timeside.api import IValueAnalyzer - - -class Duration(Processor): - """A rather useless duration analyzer. Its only purpose is to test the - nframes characteristic.""" - implements(IValueAnalyzer) - - @interfacedoc - def setup(self, channels, samplerate, nframes): - if not nframes: - raise Exception("nframes argument required") - super(Duration, self).setup(channels, samplerate, nframes) - - @staticmethod - @interfacedoc - def id(): - return "duration" - - @staticmethod - @interfacedoc - def name(): - return "Duration" - - @staticmethod - @interfacedoc - def unit(): - return "seconds" - - def result(self): - return self.input_nframes / float(self.input_samplerate) - \ No newline at end of file diff --git a/analyzer/max_level.py b/analyzer/max_level.py deleted file mode 100644 index 9ecc323..0000000 --- a/analyzer/max_level.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin -# Copyright (c) 2009 Olivier Guilyardi - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter -from timeside.analyzer.core import * -from timeside.api import IValueAnalyzer - - -class MaxLevel(Processor): - implements(IValueAnalyzer) - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(MaxLevel, self).setup(channels, samplerate, nframes) - self.max_value = 0 - - @staticmethod - @interfacedoc - def id(): - return "maxlevel" - - @staticmethod - @interfacedoc - def name(): - return "Max level" - - @staticmethod - @interfacedoc - def unit(): - # power? amplitude? - return "" - - def process(self, frames, eod=False): - max = frames.max() - if max > self.max_value: - self.max_value = max - - return frames, eod - - def result(self): - return self.max_value - - diff --git a/analyzer/mean_level.py b/analyzer/mean_level.py deleted file mode 100644 index 8b68324..0000000 --- a/analyzer/mean_level.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -from timeside.analyzer.core import * -from timeside.api import IValueAnalyzer -import numpy - - -class MeanLevel(Processor): - implements(IValueAnalyzer) - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(MeanLevel, self).setup(channels, samplerate, nframes) - self.value = -140 - - @staticmethod - @interfacedoc - def id(): - return "meanlevel" - - @staticmethod - @interfacedoc - def name(): - return "Mean RMS level" - - @staticmethod - @interfacedoc - def unit(): - return "dB" - - def __str__(self): - return "%s %s" % (str(self.value), unit()) - - def process(self, frames, eod=False): - max = numpy.round(20*numpy.log10(numpy.mean(numpy.sqrt(numpy.square(frames.max())))), 2) - if max > self.value: - self.value = max - - return frames, eod - - def result(self): - return self.value - diff --git a/analyzer/vamp/__init__.py b/analyzer/vamp/__init__.py deleted file mode 100644 index 1ded83d..0000000 --- a/analyzer/vamp/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -from timeside.analyzer.vamp.core import * diff --git a/analyzer/vamp/core.py b/analyzer/vamp/core.py deleted file mode 100644 index 49ae9a0..0000000 --- a/analyzer/vamp/core.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -from timeside.core import * -from tempfile import NamedTemporaryFile -from timeside.exceptions import SubProcessError -import os -import random -import subprocess -import signal -import time - -class VampCoreAnalyzer: - """Parent class for Vamp plugin drivers""" - - def __init__(self): - self.vamp_path = '/usr/lib/vamp/' - # needs vamp-examples package - self.host = 'vamp-simple-host' - self.buffer_size = 0xFFFF - - def id(self): - return "vamp_plugins" - - def name(self): - return "Vamp plugins" - - def unit(self): - return "" - - def get_plugins_list(self): - if os.path.exists(self.vamp_path): - args = ' --list-outputs' - command = self.host + args - #tmp_file = NamedTemporaryFile() - data = self.core_process(command, self.buffer_size) - text = '' - plugins = [] - for chunk in data: - text = text + chunk - lines = text.split('\n') - for line in lines: - if line != '': - struct = line.split(':') - struct = struct[1:] - plugins.append(struct) - return plugins - else: - return [] - - def get_wav_path(self, media_item): - return settings.MEDIA_ROOT + '/' + media_item.file - #return media_item - - def render(self, plugin, media_item): - self.wavFile = self.get_wav_path(media_item) - args = ' -s ' + ':'.join(plugin) + ' ' + str(self.wavFile) - command = command = self.host + args - data = self.core_process(command, self.buffer_size) - string = '' - values = {} - for chunk in data: - string = string + chunk - lines = string.split('\n') - for line in lines: - if line != '': - struct = line.split(':') - values[struct[0]] = struct[1] - return values - - def core_process(self, command, buffer_size): - """Encode and stream audio data through a generator""" - - __chunk = 0 - - try: - proc = subprocess.Popen(command, - shell = True, - bufsize = buffer_size, - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - close_fds = True) - except: - raise SubProcessError('Command failure:', command, proc) - - # Core processing - while True: - __chunk = proc.stdout.read(buffer_size) - status = proc.poll() - if status != None and status != 0: - raise SubProcessError('Command failure:', command, proc) - if len(__chunk) == 0: - break - yield __chunk - - diff --git a/api.py b/api.py deleted file mode 100644 index 7ceaecf..0000000 --- a/api.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2009 Parisson -# Copyright (c) 2007 Olivier Guilyardi -# Copyright (c) 2007-2009 Guillaume Pellerin -# -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -from timeside.component import Interface - -class IProcessor(Interface): - """Common processor interface""" - - @staticmethod - def id(): - """Short alphanumeric, lower-case string which uniquely identify this - processor, suitable for use as an HTTP/GET argument value, in filenames, - etc...""" - - # implementation: only letters and digits are allowed. An exception will - # be raised by MetaProcessor if the id is malformed or not unique amongst - # registered processors. - - def setup(self, channels=None, samplerate=None, nframes=None): - """Allocate internal resources and reset state, so that this processor is - ready for a new run. - - The channels, samplerate and/or nframes arguments may be required by - processors which accept input. An error will occur if any of - these arguments is passed to an output-only processor such as a decoder. - """ - - # implementations should always call the parent method - - def channels(self): - """Number of channels in the data returned by process(). May be different from - the number of channels passed to setup()""" - - def samplerate(self): - """Samplerate of the data returned by process(). May be different from - the samplerate passed to setup()""" - - def nframes(): - """The total number of frames that this processor can output, or None if - the duration is unknown.""" - - def process(self, frames=None, eod=False): - """Process input frames and return a (output_frames, eod) tuple. - Both input and output frames are 2D numpy arrays, where columns are - channels, and containing an undetermined number of frames. eod=True - means that the end-of-data has been reached. - - Output-only processors (such as decoders) will raise an exception if the - frames argument is not None. All processors (even encoders) return data, - even if that means returning the input unchanged. - - Warning: it is required to call setup() before this method.""" - - def release(self): - """Release resources owned by this processor. The processor cannot - be used anymore after calling this method.""" - - # implementations should always call the parent method - -class IEncoder(IProcessor): - """Encoder driver interface. Each encoder is expected to support a specific - format.""" - - def __init__(self, output): - """Create a new encoder. output can either be a filename or a python callback - function/method for streaming mode. - - The streaming callback prototype is: callback(data, eod) - Where data is a block of binary data of an undetermined size, and eod - True when end-of-data is reached.""" - - # implementation: the constructor must always accept the output argument. It may - # accept extra arguments such as bitrate, depth, etc.., but these must be optionnal - - @staticmethod - def format(): - """Return the encode/encoding format as a short string - Example: "MP3", "OGG", "AVI", ... - """ - - @staticmethod - def description(): - """Return a string describing what this encode format provides, is good - for, etc... The description is meant to help the end user decide what - format is good for him/her - """ - - @staticmethod - def file_extension(): - """Return the filename extension corresponding to this encode format""" - - @staticmethod - def mime_type(): - """Return the mime type corresponding to this encode format""" - - def set_metadata(self, metadata): - """Set the metadata to be embedded in the encoded output. - - In non-streaming mode, this method updates the metadata directly into the - output file, without re-encoding the audio data, provided this file already - exists. - - It isn't required to call this method, but if called, it must be before - process().""" - -class IDecoder(IProcessor): - """Decoder driver interface. Decoders are different of encoders in that - a given driver may support several input formats, hence this interface doesn't - export any static method, all informations are dynamic.""" - - def __init__(self, filename): - """Create a new decoder for filename.""" - # implementation: additional optionnal arguments are allowed - - def format(): - """Return a user-friendly file format string""" - - def encoding(): - """Return a user-friendly encoding string""" - - def resolution(): - """Return the sample width (8, 16, etc..) of original audio file/stream, - or None if not applicable/known""" - - def metadata(self): - """Return the metadata embedded into the encoded stream, if any.""" - -class IGrapher(IProcessor): - """Media item visualizer driver interface""" - - # implementation: graphers which need to know the total number of frames - # should raise an exception in setup() if the nframes argument is None - - def __init__(self, width, height): - """Create a new grapher. width and height are generally - in pixels but could be something else for eg. svg rendering, etc.. """ - - # implementation: additional optionnal arguments are allowed - - @staticmethod - def name(): - """Return the graph name, such as "Waveform", "Spectral view", - etc.. """ - - def set_colors(self, background=None, scheme=None): - """Set the colors used for image generation. background is a RGB tuple, - and scheme a a predefined color theme name""" - - def render(self): - """Return a PIL Image object visually representing all of the data passed - by repeatedly calling process()""" - -class IAnalyzer(IProcessor): - """Media item analyzer driver interface. This interface is abstract, it doesn't - describe a particular type of analyzer but is rather meant to group analyzers. - In particular, the way the result is returned may greatly vary from sub-interface - to sub-interface. For example the IValueAnalyzer returns a final single numeric - result at the end of the whole analysis. But some other analyzers may return - numpy arrays, and this, either at the end of the analysis, or from process() - for each block of data (as in Vamp).""" - - def __init__(self): - """Create a new analyzer.""" - # implementation: additional optionnal arguments are allowed - - @staticmethod - def name(): - """Return the analyzer name, such as "Mean Level", "Max level", - "Total length, etc.. """ - - @staticmethod - def unit(): - """Return the unit of the data such as "dB", "seconds", etc... """ - -class IValueAnalyzer(IAnalyzer): - """Interface for analyzers which return a single numeric value from result()""" - - def result(): - """Return the final result of the analysis performed over the data passed by - repeatedly calling process()""" - - def __str__(self): - """Return a human readable string containing both result and unit - ('5.30dB', '4.2s', etc...)""" - -class IEffect(IProcessor): - """Effect processor interface""" - - def __init__(self): - """Create a new effect.""" - # implementation: additional optionnal arguments are allowed - - @staticmethod - def name(): - """Return the effect name""" - diff --git a/component.py b/component.py deleted file mode 100644 index ff20670..0000000 --- a/component.py +++ /dev/null @@ -1,142 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2009 Olivier Guilyardi -# -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - - -# This file defines a generic object interface mechanism and -# a way to determine which components implements a given interface. -# -# For example, the following defines the Music class as implementing the -# listenable interface. -# -# class Listenable(Interface): -# pass -# -# class Music(Component): -# implements(Listenable) -# -# Several class can implements a such interface, and it is possible to -# discover which class implements it with implementations(): -# -# list_of_classes = implementations(Listenable) -# -# This mechanism support inheritance of interfaces: a class implementing a given -# interface is also considered to implement all the ascendants of this interface. -# -# However, inheritance is not supported for components. The descendants of a class -# implementing a given interface are not automatically considered to implement this -# interface too. - -__all__ = ['Component', 'MetaComponent', 'implements', 'abstract', - 'interfacedoc', 'Interface', 'implementations', 'ComponentError'] - -class Interface(object): - """Marker base class for interfaces.""" - -def implements(*interfaces): - """Registers the interfaces implemented by a component when placed in the - class header""" - MetaComponent.implements.extend(interfaces) - -def abstract(): - """Declare a component as abstract when placed in the class header""" - MetaComponent.abstract = True - -def implementations(interface, recurse=True, abstract=False): - """Returns the components implementing interface, and if recurse, any of - the descendants of interface. If abstract is True, also return the - abstract implementations.""" - result = [] - find_implementations(interface, recurse, abstract, result) - return result - -def interfacedoc(func): - if isinstance(func, staticmethod): - raise ComponentError("@interfacedoc can't handle staticmethod (try to put @staticmethod above @interfacedoc)") - - if not func.__doc__: - func.__doc__ = "@interfacedoc" - func._interfacedoc = True - return func - -class MetaComponent(type): - """Metaclass of the Component class, used mainly to register the interface - declared to be implemented by a component.""" - - implementations = [] - implements = [] - abstract = False - - def __new__(cls, name, bases, d): - new_class = type.__new__(cls, name, bases, d) - - # Register implementations - if MetaComponent.implements: - for i in MetaComponent.implements: - MetaComponent.implementations.append({ - 'interface': i, - 'class': new_class, - 'abstract': MetaComponent.abstract}) - - # Propagate @interfacedoc - for name in new_class.__dict__: - member = new_class.__dict__[name] - if isinstance(member, staticmethod): - member = getattr(new_class, name) - - if member.__doc__ == "@interfacedoc": - if_member = None - for i in MetaComponent.implements: - if hasattr(i, name): - if_member = getattr(i, name) - if not if_member: - raise ComponentError("@interfacedoc: %s.%s: no such member in implemented interfaces: %s" - % (new_class.__name__, name, str(MetaComponent.implements))) - member.__doc__ = if_member.__doc__ - - MetaComponent.implements = [] - MetaComponent.abstract = False - - return new_class - -class Component(object): - """Base class of all components""" - __metaclass__ = MetaComponent - -def extend_unique(list1, list2): - """Extend list1 with list2 as list.extend(), but doesn't append duplicates - to list1""" - for item in list2: - if item not in list1: - list1.append(item) - -def find_implementations(interface, recurse, abstract, result): - """Find implementations of an interface or of one of its descendants and - extend result with the classes found.""" - for item in MetaComponent.implementations: - if (item['interface'] == interface and (abstract or not item['abstract'])): - extend_unique(result, [item['class']]) - - if recurse: - subinterfaces = interface.__subclasses__() - if subinterfaces: - for i in subinterfaces: - find_implementations(i, recurse, abstract, result) - -class ComponentError(Exception): - pass diff --git a/core.py b/core.py deleted file mode 100644 index f5b3c31..0000000 --- a/core.py +++ /dev/null @@ -1,221 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2009 Olivier Guilyardi -# -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -from timeside.component import * -from timeside.api import IProcessor -from timeside.exceptions import Error, ApiError -import re -import numpy - -__all__ = ['Processor', 'MetaProcessor', 'implements', 'abstract', - 'interfacedoc', 'processors', 'get_processor', 'ProcessPipe', - 'FixedSizeInputAdapter'] - -_processors = {} - -class MetaProcessor(MetaComponent): - """Metaclass of the Processor class, used mainly for ensuring that processor - id's are wellformed and unique""" - - valid_id = re.compile("^[a-z][_a-z0-9]*$") - - def __new__(cls, name, bases, d): - new_class = MetaComponent.__new__(cls, name, bases, d) - if new_class in implementations(IProcessor): - id = str(new_class.id()) - if _processors.has_key(id): - raise ApiError("%s and %s have the same id: '%s'" - % (new_class.__name__, _processors[id].__name__, id)) - if not MetaProcessor.valid_id.match(id): - raise ApiError("%s has a malformed id: '%s'" - % (new_class.__name__, id)) - - _processors[id] = new_class - - return new_class - -class Processor(Component): - """Base component class of all processors""" - __metaclass__ = MetaProcessor - - abstract() - implements(IProcessor) - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - self.input_channels = channels - self.input_samplerate = samplerate - self.input_nframes = nframes - - # default channels(), samplerate() and nframes() implementations returns - # the input characteristics, but processors may change this behaviour by - # overloading those methods - @interfacedoc - def channels(self): - return self.input_channels - - @interfacedoc - def samplerate(self): - return self.input_samplerate - - @interfacedoc - def nframes(self): - return self.input_nframes - - @interfacedoc - def process(self, frames, eod): - return frames, eod - - @interfacedoc - def release(self): - pass - - def __del__(self): - self.release() - - def __or__(self, other): - return ProcessPipe(self, other) - -class FixedSizeInputAdapter(object): - """Utility to make it easier to write processors which require fixed-sized - input buffers.""" - - def __init__(self, buffer_size, channels, pad=False): - """Construct a new adapter: buffer_size is the desired buffer size in frames, - channels the number of channels, and pad indicates whether the last block should - be padded with zeros.""" - - self.buffer = numpy.empty((buffer_size, channels)) - self.buffer_size = buffer_size - self.len = 0 - self.pad = pad - - def nframes(self, input_nframes): - """Return the total number of frames that this adapter will output according to the - input_nframes argument""" - - nframes = input_nframes - if self.pad: - mod = input_nframes % self.buffer_size - if mod: - nframes += self.buffer_size - mod - - return nframes - - - def process(self, frames, eod): - """Returns an iterator over tuples of the form (buffer, eod) where buffer is a - fixed-sized block of data, and eod indicates whether this is the last block. - In case padding is deactivated the last block may be smaller than the buffer size. - """ - src_index = 0 - remaining = len(frames) - - while remaining: - space = self.buffer_size - self.len - copylen = remaining < space and remaining or space - src = frames[src_index:src_index + copylen] - if self.len == 0 and copylen == self.buffer_size: - # avoid unnecessary copy - buffer = src - else: - buffer = self.buffer - buffer[self.len:self.len + copylen] = src - - remaining -= copylen - src_index += copylen - self.len += copylen - - if self.len == self.buffer_size: - yield buffer, (eod and not remaining) - self.len = 0 - - if eod and self.len: - block = self.buffer - if self.pad: - self.buffer[self.len:self.buffer_size] = 0 - else: - block = self.buffer[0:self.len] - - yield block, True - self.len = 0 - -def processors(interface=IProcessor, recurse=True): - """Returns the processors implementing a given interface and, if recurse, - any of the descendants of this interface.""" - return implementations(interface, recurse) - - -def get_processor(processor_id): - """Return a processor by its id""" - if not _processors.has_key(processor_id): - raise Error("No processor registered with id: '%s'" - % processor_id) - - return _processors[processor_id] - -class ProcessPipe(object): - """Handle a pipe of processors""" - - def __init__(self, *others): - self.processors = [] - self |= others - - def __or__(self, other): - return ProcessPipe(self, other) - - def __ior__(self, other): - if isinstance(other, Processor): - self.processors.append(other) - elif isinstance(other, ProcessPipe): - self.processors.extend(other.processors) - else: - try: - iter(other) - except TypeError: - raise Error("Can not add this type of object to a pipe: %s", str(other)) - - for item in other: - self |= item - - return self - - def run(self): - """Setup/reset all processors in cascade and stream audio data along - the pipe. Also returns the pipe itself.""" - - source = self.processors[0] - items = self.processors[1:] - - # setup/reset processors and configure channels and samplerate throughout the pipe - source.setup() - last = source - for item in items: - item.setup(last.channels(), last.samplerate(), last.nframes()) - last = item - - # now stream audio data along the pipe - eod = False - while not eod: - frames, eod = source.process() - for item in items: - frames, eod = item.process(frames, eod) - - return self - diff --git a/decoder/__init__.py b/decoder/__init__.py deleted file mode 100644 index 9f37f75..0000000 --- a/decoder/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -from timeside.decoder.core import * diff --git a/decoder/core.py b/decoder/core.py deleted file mode 100644 index 08d6e94..0000000 --- a/decoder/core.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -from timeside.core import * -import subprocess - -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2009 Parisson -# Copyright (c) 2007 Olivier Guilyardi -# Copyright (c) 2007-2009 Guillaume Pellerin -# -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Paul Brossier - -from timeside.core import Processor, implements, interfacedoc -from timeside.api import IDecoder -from numpy import array, frombuffer, getbuffer, float32 - -import pygst -pygst.require('0.10') -import gst -import gobject -gobject.threads_init () - - -class FileDecoder(Processor): - """ gstreamer-based decoder """ - implements(IDecoder) - - # duration ms, before discovery process times out - MAX_DISCOVERY_TIME = 3000 - - audioformat = None - audiochannels = None - audiorate = None - audionframes = None - mimetype = '' - - # IProcessor methods - - @staticmethod - @interfacedoc - def id(): - return "gstreamerdec" - - def setup(self, channels = None, samplerate = None, nframes = None): - # the output data format we want - caps = "audio/x-raw-float, width=32" - pipeline = gst.parse_launch('''uridecodebin uri=%s - ! audioconvert - ! %s - ! appsink name=sink sync=False ''' % (self.uri, caps)) - # store a pointer to appsink in our decoder object - self.sink = pipeline.get_by_name('sink') - # adjust length of emitted buffers - # self.sink.set_property('blocksize', 0x10000) - # start pipeline - pipeline.set_state(gst.STATE_PLAYING) - - @interfacedoc - def channels(self): - return self.audiochannels - - @interfacedoc - def samplerate(self): - return self.audiorate - - @interfacedoc - def nframes(self): - return self.audionframes - - @interfacedoc - def process(self, frames = None, eod = False): - try: - buf = self.sink.emit('pull-buffer') - except SystemError, e: - # should never happen - print 'SystemError', e - return array([0.]), True - if buf == None: - return array([0.]), True - return self.gst_buffer_to_numpy_array(buf), False - - @interfacedoc - def release(self): - # nothing to do for now - pass - - ## IDecoder methods - - @interfacedoc - def __init__(self, uri): - - # is this a file? - import os.path - if os.path.exists(uri): - # get the absolute path - uri = os.path.abspath(uri) - # first run the file/uri through the discover pipeline - self.discover(uri) - # and make a uri of it - from urllib import quote - self.uri = 'file://'+quote(uri) - - @interfacedoc - def format(self): - # TODO check - return self.mimetype - - @interfacedoc - def encoding(self): - # TODO check - return self.mimetype.split('/')[-1] - - @interfacedoc - def resolution(self): - # TODO check: width or depth? - return self.audiowidth - - @interfacedoc - def metadata(self): - # TODO check - return self.tags - - ## gst.extend discoverer - - def discover(self, path): - """ gstreamer based helper function to get file attributes """ - from gst.extend import discoverer - d = discoverer.Discoverer(path, timeout = self.MAX_DISCOVERY_TIME) - d.connect('discovered', self.discovered) - self.mainloop = gobject.MainLoop() - d.discover() - self.mainloop.run() - - def discovered(self, d, is_media): - """ gstreamer based helper executed upon discover() completion """ - if is_media and d.is_audio: - # copy the discoverer attributes to self - self.audiorate = d.audiorate - self.mimetype= d.mimetype - self.audiochannels = d.audiochannels - self.audiowidth = d.audiowidth - # conversion from time in nanoseconds to frames - from math import ceil - duration = d.audiorate * d.audiolength * 1.e-9 - self.audionframes = int (ceil ( duration ) ) - self.tags = d.tags - elif not d.is_audio: - print "error, no audio found!" - else: - print "fail", path - self.mainloop.quit() - - def gst_buffer_to_numpy_array(self, buf): - """ gstreamer buffer to numpy array conversion """ - chan = self.audiochannels - samples = frombuffer(buf.data, dtype=float32) - samples.resize([len(samples)/chan, chan]) - return samples - - -class SubProcessPipe: - - def __init__(self, command, stdin=None): - """Read media and stream data through a generator. - Taken from Telemeta (see http://telemeta.org)""" - - self.buffer_size = 0xFFFF - - if not stdin: - stdin = subprocess.PIPE - - self.proc = subprocess.Popen(command.encode('utf-8'), - shell = True, - bufsize = self.buffer_size, - stdin = stdin, - stdout = subprocess.PIPE, - close_fds = True) - - self.input = self.proc.stdin - self.output = self.proc.stdout - - -class DecoderSubProcessCore(Processor): - """Defines the main parts of the decoding tools : - paths, metadata parsing, data streaming thru system command""" - - def __init__(self): - self.command = 'ffmpeg -i "%s" -f wav - ' - - def process(self, source, options=None): - """Encode and stream audio data through a generator""" - - command = self.command % source - proc = SubProcessPipe(command) - return proc.output - - #while True: - #__chunk = proc.output.read(self.proc.buffer_size) - #status = proc.poll() - #if status != None and status != 0: - #raise ExportProcessError('Command failure:', command, proc) - #if len(__chunk) == 0: - #break - #yield __chunk - - - diff --git a/doc/img/timeside_schema.dia b/doc/img/timeside_schema.dia deleted file mode 100644 index 6431955..0000000 Binary files a/doc/img/timeside_schema.dia and /dev/null differ diff --git a/doc/img/timeside_schema.png b/doc/img/timeside_schema.png deleted file mode 100644 index 29f4bcb..0000000 Binary files a/doc/img/timeside_schema.png and /dev/null differ diff --git a/encoder/__init__.py b/encoder/__init__.py deleted file mode 100644 index 7eadf71..0000000 --- a/encoder/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- - -from timeside.encoder.core import * -#from timeside.encoder.ogg import * -from timeside.encoder.wav import * -#from timeside.encoder.mp3 import * -#from timeside.encoder.flac import * diff --git a/encoder/core.py b/encoder/core.py deleted file mode 100644 index 9786de1..0000000 --- a/encoder/core.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -from timeside.core import * -import subprocess - -class SubProcessPipe: - """Read media and stream data through a generator. - Taken from Telemeta (see http://telemeta.org)""" - - def __init__(self, command, stdin=None): - self.buffer_size = 0xFFFF - if not stdin: - stdin = subprocess.PIPE - - self.proc = subprocess.Popen(command.encode('utf-8'), - shell = True, - bufsize = self.buffer_size, - stdin = stdin, - stdout = subprocess.PIPE, - close_fds = True) - - self.input = self.proc.stdin - self.output = self.proc.stdout - -class EncoderSubProcessCore(Processor): - """Defines the main parts of the encoding tools : - paths, metadata parsing, data streaming thru system command""" - - def core_process(self, command, stdin): - """Encode and stream audio data through a generator""" - - proc = SubProcessPipe(command, stdin) - - while True: - __chunk = proc.output.read(proc.buffer_size) - #status = proc.poll() - #if status != None and status != 0: - #raise EncodeProcessError('Command failure:', command, proc) - if len(__chunk) == 0: - break - yield __chunk - - - diff --git a/encoder/flac.py b/encoder/flac.py deleted file mode 100644 index 0753441..0000000 --- a/encoder/flac.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -import os -import string -import subprocess - -from timeside.encoder.core import * -from timeside.api import IEncoder -from tempfile import NamedTemporaryFile - -class FlacEncoder(EncoderCore): - """Defines methods to encode to FLAC""" - - implements(IEncoder) - - def __init__(self): - self.quality_default = '-5' - self.dub2args_dict = {'creator': 'artist', - 'relation': 'album' - } - - @staticmethod - def id(): - return "flacenc" - - def format(self): - return 'FLAC' - - def file_extension(self): - return 'flac' - - def mime_type(self): - return 'audio/x-flac' - - def description(self): - return """ - Free Lossless Audio Codec (FLAC) is a file format for lossless audio - data compression. - """ - - def get_file_info(self): - try: - file1, file2 = os.popen4('metaflac --list "'+self.dest+'"') - info = [] - for line in file2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('EncoderError: metaflac is not installed or ' + \ - 'file does not exist.') - - def write_tags(self, file): - from mutagen.flac import FLAC - media = FLAC(file) - for tag in self.metadata: - name = tag[0] - value = clean_word(tag[1]) - if name in self.dub2args_dict.keys(): - name = self.dub2args_dict[name] - if name == 'comment': - media['DESCRIPTION'] = unicode(value) - else: - media[name] = unicode(value) - try: - media.save() - except: - raise IOError('EncoderError: cannot write tags.') - - - def get_args(self,options=None): - """Get process options and return arguments for the encoder""" - args = [] - if not options is None: - self.options = options - if not ('verbose' in self.options and self.options['verbose'] != '0'): - args.append('-s') - if 'flac_quality' in self.options: - args.append('-f ' + self.options['flac_quality']) - else: - args.append('-f ' + self.quality_default) - else: - args.append('-s -f ' + self.quality_default) - - #for tag in self.metadata.keys(): - #value = clean_word(self.metadata[tag]) - #args.append('-c %s="%s"' % (tag, value)) - #if tag in self.dub2args_dict.keys(): - #arg = self.dub2args_dict[tag] - #args.append('-c %s="%s"' % (arg, value)) - - return args - - def process(self, source, metadata, options=None): - buffer_size = 0xFFFF - self.metadata= metadata - self.options = options - args = self.get_args() - args = ' '.join(args) - ext = self.file_extension() - temp_file = NamedTemporaryFile() - command = 'flac %s - -o %s ' % (args, temp_file.name) - - stream = self.core_process(command, source) - - for __chunk in stream: - #temp_file.write(__chunk) - #temp_file.flush() - pass - - #self.write_tags(temp_file.name) - - while True: - __chunk = temp_file.read(buffer_size) - if len(__chunk) == 0: - break - yield __chunk - - temp_file.close() - - diff --git a/encoder/mp3.py b/encoder/mp3.py deleted file mode 100644 index 70f6dfb..0000000 --- a/encoder/mp3.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007 Parisson SARL -# Copyright (c) 2006-2007 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -import os -import string -import subprocess - -from timeside.encoder.core import * -from timeside.api import IEncoder - - -class Mp3Encoder(EncoderCore): - """Defines methods to encode to MP3""" - - implements(IEncoder) - - def __init__(self): - self.bitrate_default = '192' - self.dub2id3_dict = {'title': 'TIT2', #title2 - 'creator': 'TCOM', #composer - 'creator': 'TPE1', #lead - 'identifier': 'UFID', #Unique ID... - 'identifier': 'TALB', #album - 'type': 'TCON', #genre - 'publisher': 'TPUB', #comment - #'date': 'TYER', #year - } - self.dub2args_dict = {'title': 'tt', #title2 - 'creator': 'ta', #composerS - 'relation': 'tl', #album - #'type': 'tg', #genre - 'publisher': 'tc', #comment - 'date': 'ty', #year - } - - @staticmethod - def id(): - return "mp3enc" - - def format(self): - return 'MP3' - - def file_extension(self): - return 'mp3' - - def mime_type(self): - return 'audio/mpeg' - - def description(self): - return """ - MPEG-1 Audio Layer 3, more commonly referred to as MP3, is a patented - digital audio encoding format using a form of lossy data compression. - """ - - def get_file_info(self): - try: - file_out1, file_out2 = os.popen4('mp3info "'+self.dest+'"') - info = [] - for line in file_out2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('EncoderError: file does not exist.') - - def write_tags(self): - """Write all ID3v2.4 tags by mapping dub2id3_dict dictionnary with the - respect of mutagen classes and methods""" - from mutagen import id3 - id3 = id3.ID3(self.dest) - for tag in self.metadata.keys(): - if tag in self.dub2id3_dict.keys(): - frame_text = self.dub2id3_dict[tag] - value = self.metadata[tag] - frame = mutagen.id3.Frames[frame_text](3,value) - try: - id3.add(frame) - except: - raise IOError('EncoderError: cannot tag "'+tag+'"') - try: - id3.save() - except: - raise IOError('EncoderError: cannot write tags') - - def get_args(self): - """Get process options and return arguments for the encoder""" - args = [] - if not self.options is None: - if not ( 'verbose' in self.options and self.options['verbose'] != '0' ): - args.append('-S') - if 'mp3_bitrate' in self.options: - args.append('-b ' + self.options['mp3_bitrate']) - else: - args.append('-b '+self.bitrate_default) - #Copyrights, etc.. - args.append('-c -o') - else: - args.append('-S -c --tt "unknown" -o') - - for tag in self.metadata: - name = tag[0] - value = clean_word(tag[1]) - if name in self.dub2args_dict.keys(): - arg = self.dub2args_dict[name] - args.append('--' + arg + ' "' + value + '"') - return args - - def process(self, source, metadata, options=None): - self.metadata = metadata - self.options = options - args = self.get_args() - args = ' '.join(args) - command = 'lame %s - -' % args - - stream = self.core_process(command, source) - for __chunk in stream: - yield __chunk - diff --git a/encoder/ogg.py b/encoder/ogg.py deleted file mode 100644 index 566c529..0000000 --- a/encoder/ogg.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -import os -import string -import subprocess - -from timeside.encoder.core import * -from timeside.api import IEncoder - -class OggVorbisEncoder(EncoderCore): - """Defines methods to encode to OGG Vorbis""" - - implements(IEncoder) - - def __init__(self): - self.bitrate_default = '192' - self.dub2args_dict = {'creator': 'artist', - 'relation': 'album' - } - - @staticmethod - def id(): - return "oggenc" - - def format(self): - return 'OggVorbis' - - def file_extension(self): - return 'ogg' - - def mime_type(self): - return 'application/ogg' - - def description(self): - return """ - Vorbis is a free software / open source project headed by the Xiph.Org - Foundation (formerly Xiphophorus company). The project produces an audio - format specification and software implementation (codec) for lossy audio - compression. Vorbis is most commonly used in conjunction with the Ogg - container format and it is therefore often referred to as Ogg Vorbis. - (source Wikipedia) - """ - - def get_file_info(self, file): - try: - file_out1, file_out2 = os.popen4('ogginfo "' + file + '"') - info = [] - for line in file_out2.readlines(): - info.append(clean_word(line[:-1])) - self.info = info - return self.info - except: - raise IOError('EncoderError: file does not exist.') - - def write_tags(self, file): - from mutagen.oggvorbis import OggVorbis - media = OggVorbis(file) - for tag in self.metadata.keys(): - media[tag] = str(self.metadata[tag]) - media.save() - - def get_args(self): - """Get process options and return arguments for the encoder""" - args = [] - if not self.options is None: - if not ('verbose' in self.options and self.options['verbose'] != '0'): - args.append('-Q ') - if 'ogg_bitrate' in self.options: - args.append('-b '+self.options['ogg_bitrate']) - elif 'ogg_quality' in self.options: - args.append('-q '+self.options['ogg_quality']) - else: - args.append('-b '+self.bitrate_default) - else: - args.append('-Q -b '+self.bitrate_default) - - for tag in self.metadata: - name = tag[0] - value = clean_word(tag[1]) - args.append('-c %s="%s"' % (name, value)) - if name in self.dub2args_dict.keys(): - arg = self.dub2args_dict[name] - args.append('-c %s="%s"' % (arg, value)) - return args - - def process(self, source, metadata, options=None): - self.metadata = metadata - self.options = options - args = self.get_args() - args = ' '.join(args) - command = 'oggenc %s -' % args - - stream = self.core_process(command, source) - for __chunk in stream: - yield __chunk - - diff --git a/encoder/wav.py b/encoder/wav.py deleted file mode 100644 index d790aaa..0000000 --- a/encoder/wav.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2009 Parisson -# Copyright (c) 2007 Olivier Guilyardi -# Copyright (c) 2007-2009 Guillaume Pellerin -# -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Paul Brossier - -from timeside.core import Processor, implements, interfacedoc -from timeside.api import IEncoder -from numpy import array, frombuffer, getbuffer, float32 - -import pygst -pygst.require('0.10') -import gst -import gobject -gobject.threads_init () - - -class WavEncoder(Processor): - """ gstreamer-based encoder """ - implements(IEncoder) - - def __init__(self, output): - self.file = None - if isinstance(output, basestring): - self.filename = output - else: - raise Exception("Streaming not supported") - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(WavEncoder, self).setup(channels, samplerate, nframes) - # TODO open file for writing - # the output data format we want - pipeline = gst.parse_launch(''' appsrc name=src - ! audioconvert - ! wavenc - ! filesink location=%s ''' % self.filename) - # store a pointer to appsink in our encoder object - self.src = pipeline.get_by_name('src') - srccaps = gst.Caps("""audio/x-raw-float, - endianness=(int)1234, - channels=(int)%s, - width=(int)32, - rate=(int)%d""" % (int(channels), int(samplerate))) - self.src.set_property("caps", srccaps) - - # start pipeline - pipeline.set_state(gst.STATE_PLAYING) - self.pipeline = pipeline - - @staticmethod - @interfacedoc - def id(): - return "gstreamerenc" - - @staticmethod - @interfacedoc - def description(): - return "Gstreamer based encoder" - - @staticmethod - @interfacedoc - def file_extension(): - return "wav" - - @staticmethod - @interfacedoc - def mime_type(): - return "audio/x-wav" - - @interfacedoc - def set_metadata(self, metadata): - #TODO - pass - - @interfacedoc - def process(self, frames, eod=False): - buf = self.numpy_array_to_gst_buffer(frames) - self.src.emit('push-buffer', buf) - if eod: self.src.emit('end-of-stream') - return frames, eod - - def numpy_array_to_gst_buffer(self, frames): - """ gstreamer buffer to numpy array conversion """ - buf = gst.Buffer(getbuffer(frames)) - return buf diff --git a/exceptions.py b/exceptions.py deleted file mode 100644 index 40b4dcd..0000000 --- a/exceptions.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2009 Olivier Guilyardi -# -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -class Error(Exception): - """Exception base class for errors in TimeSide.""" - -class ApiError(Exception): - """Exception base class for errors in TimeSide.""" - -class SubProcessError(Error): - """Exception for reporting errors from a subprocess""" - - def __init__(self, message, command, subprocess): - self.message = message - self.command = str(command) - self.subprocess = subprocess - - def __str__(self): - if self.subprocess.stderr != None: - error = self.subprocess.stderr.read() - else: - error = '' - return "%s ; command: %s; error: %s" % (self.message, - self.command, - error) diff --git a/grapher/__init__.py b/grapher/__init__.py deleted file mode 100644 index 616ebf6..0000000 --- a/grapher/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- - -from timeside.grapher.core import * -from timeside.grapher.waveform import * -from timeside.grapher.spectrogram import * diff --git a/grapher/core.py b/grapher/core.py deleted file mode 100644 index 36bc0bd..0000000 --- a/grapher/core.py +++ /dev/null @@ -1,362 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# wav2png.py -- converts wave files to wave file and spectrogram images -# Copyright (C) 2008 MUSIC TECHNOLOGY GROUP (MTG) -# UNIVERSITAT POMPEU FABRA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# Authors: -# Bram de Jong -# Guillaume Pellerin - - -import optparse, math, sys -import ImageFilter, ImageChops, Image, ImageDraw, ImageColor -import numpy -from timeside.core import FixedSizeInputAdapter - - -color_schemes = { - 'default': { - 'waveform': [(50,0,200), (0,220,80), (255,224,0), (255,0,0)], - 'spectrogram': [(0, 0, 0), (58/4,68/4,65/4), (80/2,100/2,153/2), (90,180,100), - (224,224,44), (255,60,30), (255,255,255)] - }, - 'iso': { - 'waveform': [(0,0,255), (0,255,255), (255,255,0), (255,0,0)], - 'spectrogram': [(0, 0, 0), (58/4,68/4,65/4), (80/2,100/2,153/2), (90,180,100), - (224,224,44), (255,60,30), (255,255,255)] - }, - 'purple': { - 'waveform': [(173,173,173), (147,149,196), (77,80,138), (108,66,0)], - 'spectrogram': [(0, 0, 0), (58/4,68/4,65/4), (80/2,100/2,153/2), (90,180,100), - (224,224,44), (255,60,30), (255,255,255)] - } -} - - -class Spectrum(object): - """ FFT based frequency analysis of audio frames.""" - - def __init__(self, fft_size, nframes, samplerate, lower, higher, window_function=numpy.ones): - self.fft_size = fft_size - self.window = window_function(self.fft_size) - self.spectrum_range = None - self.lower = lower - self.higher = higher - self.lower_log = math.log10(self.lower) - self.higher_log = math.log10(self.higher) - self.clip = lambda val, low, high: min(high, max(low, val)) - self.nframes = nframes - self.samplerate = samplerate - self.spectrum_adapter = FixedSizeInputAdapter(self.fft_size, 1, pad=True) - - def process(self, frames, eod, spec_range=120.0): - """ Returns a tuple containing the spectral centroid and the spectrum (dB scales) of the input audio frames. - An adapter is used to fix the buffer length and then provide fixed FFT window sizes.""" - - for buffer, end in self.spectrum_adapter.process(frames, eod): - samples = buffer[:,0].copy() - if end: - break - - #samples = numpy.concatenate((numpy.zeros(self.fft_size/2), samples), axis=1) - samples *= self.window - fft = numpy.fft.fft(samples) - spectrum = numpy.abs(fft[:fft.shape[0] / 2 + 1]) / float(self.fft_size) # normalized abs(FFT) between 0 and 1 - length = numpy.float64(spectrum.shape[0]) - - # scale the db spectrum from [- spec_range db ... 0 db] > [0..1] - db_spectrum = ((20*(numpy.log10(spectrum + 1e-30))).clip(-spec_range, 0.0) + spec_range)/spec_range - energy = spectrum.sum() - spectral_centroid = 0 - - if energy > 1e-20: - # calculate the spectral centroid - if self.spectrum_range == None: - self.spectrum_range = numpy.arange(length) - - spectral_centroid = (spectrum * self.spectrum_range).sum() / (energy * (length - 1)) * self.samplerate * 0.5 - # clip > log10 > scale between 0 and 1 - spectral_centroid = (math.log10(self.clip(spectral_centroid, self.lower, self.higher)) - self.lower_log) / (self.higher_log - self.lower_log) - - return (spectral_centroid, db_spectrum) - - -def interpolate_colors(colors, flat=False, num_colors=256): - """ Given a list of colors, create a larger list of colors interpolating - the first one. If flatten is True a list of numers will be returned. If - False, a list of (r,g,b) tuples. num_colors is the number of colors wanted - in the final list """ - - palette = [] - - for i in range(num_colors): - index = (i * (len(colors) - 1))/(num_colors - 1.0) - index_int = int(index) - alpha = index - float(index_int) - - if alpha > 0: - r = (1.0 - alpha) * colors[index_int][0] + alpha * colors[index_int + 1][0] - g = (1.0 - alpha) * colors[index_int][1] + alpha * colors[index_int + 1][1] - b = (1.0 - alpha) * colors[index_int][2] + alpha * colors[index_int + 1][2] - else: - r = (1.0 - alpha) * colors[index_int][0] - g = (1.0 - alpha) * colors[index_int][1] - b = (1.0 - alpha) * colors[index_int][2] - - if flat: - palette.extend((int(r), int(g), int(b))) - else: - palette.append((int(r), int(g), int(b))) - - return palette - - -class WaveformImage(object): - """ Builds a PIL image representing a waveform of the audio stream. - Adds pixels iteratively thanks to the adapter providing fixed size frame buffers. - Peaks are colored relative to the spectral centroids of each frame packet. """ - - def __init__(self, image_width, image_height, nframes, samplerate, fft_size, bg_color=(0,0,0), color_scheme='default', filename=None): - self.image_width = image_width - self.image_height = image_height - self.nframes = nframes - self.samplerate = samplerate - self.fft_size = fft_size - self.filename = filename - - self.bg_color = bg_color - self.color_scheme = color_scheme - - if isinstance(color_scheme, dict): - colors = color_scheme['waveform'] - else: - colors = color_schemes[self.color_scheme]['waveform'] - - self.color_lookup = interpolate_colors(colors) - - self.samples_per_pixel = self.nframes / float(self.image_width) - self.buffer_size = int(round(self.samples_per_pixel, 0)) - self.pixels_adapter = FixedSizeInputAdapter(self.buffer_size, 1, pad=False) - self.pixels_adapter_nframes = self.pixels_adapter.nframes(self.nframes) - - self.lower = 800 - self.higher = 12000 - self.spectrum = Spectrum(self.fft_size, self.nframes, self.samplerate, self.lower, self.higher, numpy.hanning) - - self.image = Image.new("RGB", (self.image_width, self.image_height), self.bg_color) - self.pixel = self.image.load() - self.draw = ImageDraw.Draw(self.image) - self.previous_x, self.previous_y = None, None - self.frame_cursor = 0 - self.pixel_cursor = 0 - - def peaks(self, samples): - """ Find the minimum and maximum peak of the samples. - Returns that pair in the order they were found. - So if min was found first, it returns (min, max) else the other way around. """ - - max_index = numpy.argmax(samples) - max_value = samples[max_index] - - min_index = numpy.argmin(samples) - min_value = samples[min_index] - - if min_index < max_index: - return (min_value, max_value) - else: - return (max_value, min_value) - - def color_from_value(self, value): - """ given a value between 0 and 1, return an (r,g,b) tuple """ - - return ImageColor.getrgb("hsl(%d,%d%%,%d%%)" % (int( (1.0 - value) * 360 ), 80, 50)) - - def draw_peaks(self, x, peaks, spectral_centroid): - """ draw 2 peaks at x using the spectral_centroid for color """ - - y1 = self.image_height * 0.5 - peaks[0] * (self.image_height - 4) * 0.5 - y2 = self.image_height * 0.5 - peaks[1] * (self.image_height - 4) * 0.5 - - line_color = self.color_lookup[int(spectral_centroid*255.0)] - - if self.previous_y != None: - self.draw.line([self.previous_x, self.previous_y, x, y1, x, y2], line_color) - else: - self.draw.line([x, y1, x, y2], line_color) - - self.previous_x, self.previous_y = x, y2 - - self.draw_anti_aliased_pixels(x, y1, y2, line_color) - - def draw_anti_aliased_pixels(self, x, y1, y2, color): - """ vertical anti-aliasing at y1 and y2 """ - - y_max = max(y1, y2) - y_max_int = int(y_max) - alpha = y_max - y_max_int - - if alpha > 0.0 and alpha < 1.0 and y_max_int + 1 < self.image_height: - current_pix = self.pixel[x, y_max_int + 1] - - r = int((1-alpha)*current_pix[0] + alpha*color[0]) - g = int((1-alpha)*current_pix[1] + alpha*color[1]) - b = int((1-alpha)*current_pix[2] + alpha*color[2]) - - self.pixel[x, y_max_int + 1] = (r,g,b) - - y_min = min(y1, y2) - y_min_int = int(y_min) - alpha = 1.0 - (y_min - y_min_int) - - if alpha > 0.0 and alpha < 1.0 and y_min_int - 1 >= 0: - current_pix = self.pixel[x, y_min_int - 1] - - r = int((1-alpha)*current_pix[0] + alpha*color[0]) - g = int((1-alpha)*current_pix[1] + alpha*color[1]) - b = int((1-alpha)*current_pix[2] + alpha*color[2]) - - self.pixel[x, y_min_int - 1] = (r,g,b) - - def process(self, frames, eod): - if len(frames) != 1: - buffer = frames[:,0].copy() - buffer.shape = (len(buffer),1) - (spectral_centroid, db_spectrum) = self.spectrum.process(buffer, True) - for samples, end in self.pixels_adapter.process(buffer, eod): - if self.pixel_cursor < self.image_width: - peaks = self.peaks(samples) - self.draw_peaks(self.pixel_cursor, peaks, spectral_centroid) - self.pixel_cursor += 1 - - def save(self): - """ Apply last 2D transforms and write all pixels to the file. """ - - # middle line (0 for none) - a = 1 - - for x in range(self.image_width): - self.pixel[x, self.image_height/2] = tuple(map(lambda p: p+a, self.pixel[x, self.image_height/2])) - self.image.save(self.filename) - - -class SpectrogramImage(object): - """ Builds a PIL image representing a spectrogram of the audio stream (level vs. frequency vs. time). - Adds pixels iteratively thanks to the adapter providing fixed size frame buffers.""" - - def __init__(self, image_width, image_height, nframes, samplerate, fft_size, bg_color=None, color_scheme='default', filename=None): - self.image_width = image_width - self.image_height = image_height - self.nframes = nframes - self.samplerate = samplerate - self.fft_size = fft_size - self.filename = filename - self.color_scheme = color_scheme - - self.image = Image.new("P", (self.image_height, self.image_width)) - colors = color_schemes[self.color_scheme]['spectrogram'] - self.image.putpalette(interpolate_colors(colors, True)) - - self.samples_per_pixel = self.nframes / float(self.image_width) - self.buffer_size = int(round(self.samples_per_pixel, 0)) - self.pixels_adapter = FixedSizeInputAdapter(self.buffer_size, 1, pad=False) - self.pixels_adapter_nframes = self.pixels_adapter.nframes(self.nframes) - - self.lower = 100 - self.higher = 22050 - self.spectrum = Spectrum(self.fft_size, self.nframes, self.samplerate, self.lower, self.higher, numpy.hanning) - - # generate the lookup which translates y-coordinate to fft-bin - self.y_to_bin = [] - f_min = float(self.lower) - f_max = float(self.higher) - y_min = math.log10(f_min) - y_max = math.log10(f_max) - for y in range(self.image_height): - freq = math.pow(10.0, y_min + y / (image_height - 1.0) *(y_max - y_min)) - bin = freq / 22050.0 * (self.fft_size/2 + 1) - - if bin < self.fft_size/2: - alpha = bin - int(bin) - - self.y_to_bin.append((int(bin), alpha * 255)) - - # this is a bit strange, but using image.load()[x,y] = ... is - # a lot slower than using image.putadata and then rotating the image - # so we store all the pixels in an array and then create the image when saving - self.pixels = [] - self.pixel_cursor = 0 - - def draw_spectrum(self, x, spectrum): - for (index, alpha) in self.y_to_bin: - self.pixels.append( int( ((255.0-alpha) * spectrum[index] + alpha * spectrum[index + 1] )) ) - - for y in range(len(self.y_to_bin), self.image_height): - self.pixels.append(0) - - def process(self, frames, eod): - if len(frames) != 1: - buffer = frames[:,0].copy() - buffer.shape = (len(buffer),1) - - # FIXME : breaks spectrum linearity - for samples, end in self.pixels_adapter.process(buffer, eod): - if self.pixel_cursor < self.image_width: - (spectral_centroid, db_spectrum) = self.spectrum.process(samples, True) - self.draw_spectrum(self.pixel_cursor, db_spectrum) - self.pixel_cursor += 1 - - def save(self): - """ Apply last 2D transforms and write all pixels to the file. """ - self.image.putdata(self.pixels) - self.image.transpose(Image.ROTATE_90).save(self.filename) - - -class Noise(object): - """A class that mimics audiolab.sndfile but generates noise instead of reading - a wave file. Additionally it can be told to have a "broken" header and thus crashing - in the middle of the file. Also useful for testing ultra-short files of 20 samples.""" - - def __init__(self, num_frames, has_broken_header=False): - self.seekpoint = 0 - self.num_frames = num_frames - self.has_broken_header = has_broken_header - - def seek(self, seekpoint): - self.seekpoint = seekpoint - - def get_nframes(self): - return self.num_frames - - def get_samplerate(self): - return 44100 - - def get_channels(self): - return 1 - - def read_frames(self, frames_to_read): - if self.has_broken_header and self.seekpoint + frames_to_read > self.num_frames / 2: - raise IOError() - - num_frames_left = self.num_frames - self.seekpoint - if num_frames_left < frames_to_read: - will_read = num_frames_left - else: - will_read = frames_to_read - self.seekpoint += will_read - return numpy.random.random(will_read)*2 - 1 - diff --git a/grapher/spectrogram.py b/grapher/spectrogram.py deleted file mode 100644 index 0962842..0000000 --- a/grapher/spectrogram.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2010 Guillaume Pellerin -# Copyright (c) 2010 Olivier Guilyardi - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - - -from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter -from timeside.api import IGrapher -from timeside.grapher import * - - -class Spectrogram(Processor): - implements(IGrapher) - - FFT_SIZE = 0x400 - - @interfacedoc - def __init__(self, width=None, height=None, output=None, bg_color=None, color_scheme=None): - if width: - self.width = width - else: - self.width = 1500 - if height: - self.height = height - else: - self.height = 200 - self.bg_color = bg_color - self.color_scheme = color_scheme - self.filename = output - self.graph = None - - @staticmethod - @interfacedoc - def id(): - return "spectrogram" - - @staticmethod - @interfacedoc - def name(): - return "Spectrogram" - - @interfacedoc - def set_colors(self, background, scheme): - self.bg_color = background - self.color_scheme = scheme - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(Spectrogram, self).setup(channels, samplerate, nframes) - if self.graph: - self.graph = None - self.graph = SpectrogramImage(self.width, self.height, self.nframes(), self.samplerate(), self.FFT_SIZE, - bg_color=self.bg_color, color_scheme=self.color_scheme, filename=self.filename) - - @interfacedoc - def process(self, frames, eod=False): - self.graph.process(frames, eod) - return frames, eod - - @interfacedoc - def render(self): - if self.filename: - self.graph.save() - return self.graph.image - diff --git a/grapher/spectrogram_audiolab.py b/grapher/spectrogram_audiolab.py deleted file mode 100644 index be7143c..0000000 --- a/grapher/spectrogram_audiolab.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -from timeside.core import * -from timeside.api import IGrapher -from tempfile import NamedTemporaryFile -from timeside.grapher.core import * - -class SpectrogramGrapherAudiolab(Processor): - """Spectrogram graph driver (python style thanks to wav2png.py and scikits.audiolab)""" - - implements(IGrapher) - - bg_color = None - color_scheme = None - - @staticmethod - def id(): - return "spectrogram" - - def name(self): - return "Spectrogram (audiolab)" - - def set_colors(self, background=None, scheme=None): - self.bg_color = background - self.color_scheme = scheme - - def render(self, media_item, width=None, height=None, options=None): - """Generator that streams the spectrogram as a PNG image with a python method""" - - wav_file = media_item - pngFile = NamedTemporaryFile(suffix='.png') - - if not width == None: - image_width = width - else: - image_width = 1500 - if not height == None: - image_height = height - else: - image_height = 200 - - fft_size = 2048 - args = (wav_file, pngFile.name, image_width, image_height, fft_size, - self.bg_color, self.color_scheme) - create_spectrogram_png(*args) - - buffer = pngFile.read(0xFFFF) - while buffer: - yield buffer - buffer = pngFile.read(0xFFFF) - - pngFile.close() diff --git a/grapher/waveform.py b/grapher/waveform.py deleted file mode 100644 index 982192c..0000000 --- a/grapher/waveform.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2010 Guillaume Pellerin -# Copyright (c) 2010 Olivier Guilyardi - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - - -from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter -from timeside.api import IGrapher -from timeside.grapher import * - - -class Waveform(Processor): - implements(IGrapher) - - FFT_SIZE = 0x400 - - @interfacedoc - def __init__(self, width=None, height=None, output=None, bg_color=None, color_scheme=None): - if width: - self.width = width - else: - self.width = 1500 - if height: - self.height = height - else: - self.height = 200 - self.bg_color = bg_color - self.color_scheme = color_scheme - self.filename = output - self.graph = None - - @staticmethod - @interfacedoc - def id(): - return "waveform" - - @staticmethod - @interfacedoc - def name(): - return "Waveform" - - @interfacedoc - def set_colors(self, background, scheme): - self.bg_color = background - self.color_scheme = scheme - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(Waveform, self).setup(channels, samplerate, nframes) - if self.graph: - self.graph = None - self.graph = WaveformImage(self.width, self.height, self.nframes(), self.samplerate(), self.FFT_SIZE, - bg_color=self.bg_color, color_scheme=self.color_scheme, filename=self.filename) - - @interfacedoc - def process(self, frames, eod=False): - self.graph.process(frames, eod) - return frames, eod - - @interfacedoc - def render(self): - if self.filename: - self.graph.save() - return self.graph.image diff --git a/grapher/waveform_audiolab.py b/grapher/waveform_audiolab.py deleted file mode 100644 index ac07330..0000000 --- a/grapher/waveform_audiolab.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2007-2009 Guillaume Pellerin - -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Guillaume Pellerin - -from timeside.core import * -from timeside.api import IGrapher -from tempfile import NamedTemporaryFile -from timeside.grapher.core import * - -class WaveFormGrapherAudiolab(Processor): - """WaveForm graph driver (python style thanks to wav2png.py and scikits.audiolab)""" - - implements(IGrapher) - - bg_color = None - color_scheme = None - - @staticmethod - def id(): - return "waveform" - - def name(self): - return "Waveform (audiolab)" - - def set_colors(self, background=None, scheme=None): - self.bg_color = background - self.color_scheme = scheme - - def render(self, media_item, width=None, height=None, options=None): - """Generator that streams the waveform as a PNG image with a python method""" - - wav_file = media_item - pngFile = NamedTemporaryFile(suffix='.png') - - if not width == None: - image_width = width - else: - image_width = 1500 - if not height == None: - image_height = height - else: - image_height = 200 - - fft_size = 2048 - args = (wav_file, pngFile.name, image_width, image_height, fft_size, - self.bg_color, self.color_scheme) - create_wavform_png(*args) - - buffer = pngFile.read(0xFFFF) - while buffer: - yield buffer - buffer = pngFile.read(0xFFFF) - - pngFile.close() - diff --git a/metadata.py b/metadata.py deleted file mode 100644 index 86abf87..0000000 --- a/metadata.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2009 Parisson -# Copyright (c) 2007 Olivier Guilyardi -# Copyright (c) 2007-2009 Guillaume Pellerin -# -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -class Metadata(object): - pass - - diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..a4936d5 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +'''The setup and build script for the python-twitter library.''' + +__author__ = 'yomguy@parisson.com' +__version__ = '0.1-beta' + + +# The base package metadata to be used by both distutils and setuptools +METADATA = dict( + name = "timeside", + version = __version__, + py_modules = ['timeside'], + description='Web Audio Components', + author='Olivier Guilyardi, Paul Brossier, Guillaume Pellerin', + author_email='yomguy@parisson.com', + license='Gnu Public License V2', + url='http://code.google.com/p/timeside', + packages=['timeside'], + keywords='audio analyze transcode graph', + install_requires = ['setuptools',] + include_package_data = True, +) + + +def Main(): + # Use setuptools if available, otherwise fallback and use distutils + try: + import setuptools + setuptools.setup(**METADATA) + except ImportError: + import distutils.core + distutils.core.setup(**METADATA) + + +if __name__ == '__main__': + Main() diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 2a6b9db..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,140 +0,0 @@ - -import unittest -import sys -import time - -class TestCase(unittest.TestCase): - - def assertSameList(self, list1, list2): - "Test that two lists contain the same elements, in any order" - if len(list1) != len(list2): - self.fail("Lists length differ : %d != %d" % (len(list1), len(list2))) - - for item in list1: - if not item in list2: - self.fail("%s is not in list2" % str(item)) - - for item in list2: - if not item in list1: - self.fail("%s is not in list1" % str(item)) - -class _TextTestResult(unittest.TestResult): - """A test result class that can print formatted text results to a stream. - - Used by TextTestRunner. - """ - separator1 = '=' * 70 - separator2 = '-' * 70 - - def __init__(self, stream, descriptions, verbosity): - unittest.TestResult.__init__(self) - self.stream = stream - self.showAll = verbosity > 1 - self.dots = verbosity == 1 - self.descriptions = descriptions - self.currentTestCase = None - - def getDescription(self, test): - if self.descriptions: - return test.shortDescription() or str(test) - else: - return str(test) - - def startTest(self, test): - unittest.TestResult.startTest(self, test) - if self.showAll: - if self.currentTestCase != test.__class__: - self.currentTestCase = test.__class__ - self.stream.writeln() - self.stream.writeln("[%s]" % self.currentTestCase.__name__) - self.stream.write(" " + self.getDescription(test)) - self.stream.write(" ... ") - - def addSuccess(self, test): - unittest.TestResult.addSuccess(self, test) - if self.showAll: - self.stream.writeln("ok") - elif self.dots: - self.stream.write('.') - - def addError(self, test, err): - unittest.TestResult.addError(self, test, err) - if self.showAll: - self.stream.writeln("ERROR") - elif self.dots: - self.stream.write('E') - - def addFailure(self, test, err): - unittest.TestResult.addFailure(self, test, err) - if self.showAll: - self.stream.writeln("FAIL") - elif self.dots: - self.stream.write('F') - - def printErrors(self): - if self.dots or self.showAll: - self.stream.writeln() - self.printErrorList('ERROR', self.errors) - self.printErrorList('FAIL', self.failures) - - def printErrorList(self, flavour, errors): - for test, err in errors: - self.stream.writeln(self.separator1) - self.stream.writeln("%s: %s" % (flavour,self.getDescription(test))) - self.stream.writeln(self.separator2) - self.stream.writeln("%s" % err) - - -class _WritelnDecorator: - """Used to decorate file-like objects with a handy 'writeln' method""" - def __init__(self,stream): - self.stream = stream - - def __getattr__(self, attr): - return getattr(self.stream,attr) - - def writeln(self, arg=None): - if arg: self.write(arg) - self.write('\n') # text-mode streams translate to \r\n if needed - -class TestRunner: - """A test runner class that displays results in textual form. - - It prints out the names of tests as they are run, errors as they - occur, and a summary of the results at the end of the test run. - """ - def __init__(self, stream=sys.stderr, descriptions=1, verbosity=2): - self.stream = _WritelnDecorator(stream) - self.descriptions = descriptions - self.verbosity = verbosity - - def _makeResult(self): - return _TextTestResult(self.stream, self.descriptions, self.verbosity) - - def run(self, test): - "Run the given test case or test suite." - result = self._makeResult() - startTime = time.time() - test(result) - stopTime = time.time() - timeTaken = stopTime - startTime - result.printErrors() - self.stream.writeln(result.separator2) - run = result.testsRun - self.stream.writeln("Ran %d test%s in %.3fs" % - (run, run != 1 and "s" or "", timeTaken)) - self.stream.writeln() - if not result.wasSuccessful(): - self.stream.write("FAILED (") - failed, errored = map(len, (result.failures, result.errors)) - if failed: - self.stream.write("failures=%d" % failed) - if errored: - if failed: self.stream.write(", ") - self.stream.write("errors=%d" % errored) - self.stream.writeln(")") - else: - self.stream.writeln("OK") - return result - - diff --git a/tests/alltests.py b/tests/alltests.py deleted file mode 100644 index e368939..0000000 --- a/tests/alltests.py +++ /dev/null @@ -1,5 +0,0 @@ -from timeside.tests.testcomponent import * -from timeside.tests.testinputadapter import * -from timeside.tests import TestRunner - -unittest.main(testRunner=TestRunner()) diff --git a/tests/api/__init__.py b/tests/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/api/examples.py b/tests/api/examples.py deleted file mode 100644 index 418d2ca..0000000 --- a/tests/api/examples.py +++ /dev/null @@ -1,379 +0,0 @@ -# -*- coding: utf-8 -*- -from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter -from timeside.api import * -from timeside.grapher import * -from timeside import Metadata -from scikits import audiolab -import numpy - -class FileDecoder(Processor): - """A simple audiolab-based example decoder""" - implements(IDecoder) - - @staticmethod - @interfacedoc - def id(): - return "test_audiolabdec" - - @interfacedoc - def __init__(self, filename): - self.filename = filename - # The file has to be opened here so that nframes(), samplerate(), - # etc.. work before setup() is called. - self.file = audiolab.Sndfile(self.filename, 'r') - self.position = 0 - - @interfacedoc - def setup(self): - super(FileDecoder, self).setup() - if self.position != 0: - self.file.seek(0); - self.position = 0 - - def release(self): - super(FileDecoder, self).release() - if self.file: - self.file.close() - self.file = None - - @interfacedoc - def channels(self): - return self.file.channels - - @interfacedoc - def samplerate(self): - return self.file.samplerate - - @interfacedoc - def nframes(self): - return self.file.nframes - - @interfacedoc - def format(self): - return self.file.file_format - - @interfacedoc - def encoding(self): - return self.file.encoding - @interfacedoc - def resolution(self): - resolution = None - encoding = self.file.encoding - - if encoding == "pcm8": - resolution = 8 - - elif encoding == "pcm16": - resolution = 16 - elif encoding == "pcm32": - resolution = 32 - - return resolution - - @interfacedoc - def metadata(self): - #TODO - return Metadata() - - @interfacedoc - def process(self, frames=None, eod=False): - if frames: - raise Exception("Decoder doesn't accept input frames") - - buffersize = 0x10000 - - # Need this because audiolab raises a bogus exception when asked - # to read passed the end of the file - toread = self.nframes() - self.position - if toread > buffersize: - toread = buffersize - - frames = self.file.read_frames(toread) - eod = (toread < buffersize) - self.position += toread - - # audiolab returns a 1D array for 1 channel, need to reshape to 2D: - if frames.ndim == 1: - frames = frames.reshape(len(frames), 1) - - return frames, eod - -class MaxLevel(Processor): - implements(IValueAnalyzer) - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(MaxLevel, self).setup(channels, samplerate, nframes) - self.max_value = 0 - - @staticmethod - @interfacedoc - def id(): - return "test_maxlevel" - - @staticmethod - @interfacedoc - def name(): - return "Max level test analyzer" - - @staticmethod - @interfacedoc - def unit(): - # power? amplitude? - return "" - - def process(self, frames, eod=False): - max = frames.max() - if max > self.max_value: - self.max_value = max - - return frames, eod - - def result(self): - return self.max_value - -class Gain(Processor): - implements(IEffect) - - @interfacedoc - def __init__(self, gain=1.0): - self.gain = gain - - @staticmethod - @interfacedoc - def id(): - return "test_gain" - - @staticmethod - @interfacedoc - def name(): - return "Gain test effect" - - def process(self, frames, eod=False): - return numpy.multiply(frames, self.gain), eod - -class WavEncoder(Processor): - implements(IEncoder) - - def __init__(self, output): - self.file = None - if isinstance(output, basestring): - self.filename = output - else: - raise Exception("Streaming not supported") - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(WavEncoder, self).setup(channels, samplerate, nframes) - if self.file: - self.file.close() - - format = audiolab.Format("wav", "pcm16") - self.file = audiolab.Sndfile(self.filename, 'w', format=format, channels=channels, - samplerate=samplerate) - - @staticmethod - @interfacedoc - def id(): - return "test_wavenc" - - @staticmethod - @interfacedoc - def description(): - return "Hackish wave encoder" - - @staticmethod - @interfacedoc - def file_extension(): - return "wav" - - @staticmethod - @interfacedoc - def mime_type(): - return "audio/x-wav" - - @interfacedoc - def set_metadata(self, metadata): - #TODO - pass - - @interfacedoc - def process(self, frames, eod=False): - self.file.write_frames(frames) - if eod: - self.file.close() - self.file = None - - return frames, eod - - -class Waveform(Processor): - implements(IGrapher) - - FFT_SIZE = 0x400 - - @interfacedoc - def __init__(self, width=None, height=None, output=None, bg_color=None, color_scheme=None): - if width: - self.width = width - else: - self.width = 1500 - if height: - self.height = height - else: - self.height = 200 - self.bg_color = bg_color - self.color_scheme = color_scheme - self.filename = output - self.graph = None - - @staticmethod - @interfacedoc - def id(): - return "test_waveform" - - @staticmethod - @interfacedoc - def name(): - return "Waveform test" - - @interfacedoc - def set_colors(self, background, scheme): - self.bg_color = background - self.color_scheme = scheme - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(Waveform, self).setup(channels, samplerate, nframes) - if self.graph: - self.graph = None - self.graph = WaveformImage(self.width, self.height, self.nframes(), self.samplerate(), self.FFT_SIZE, - bg_color=self.bg_color, color_scheme=self.color_scheme, filename=self.filename) - - @interfacedoc - def process(self, frames, eod=False): - self.graph.process(frames, eod) - return frames, eod - - @interfacedoc - def render(self): - if self.filename: - self.graph.save() - return self.graph.image - - -class Spectrogram(Processor): - implements(IGrapher) - - FFT_SIZE = 0x400 - - @interfacedoc - def __init__(self, width=None, height=None, output=None, bg_color=None, color_scheme=None): - if width: - self.width = width - else: - self.width = 1500 - if height: - self.height = height - else: - self.height = 200 - self.bg_color = bg_color - self.color_scheme = color_scheme - self.filename = output - self.graph = None - - @staticmethod - @interfacedoc - def id(): - return "test_spectrogram" - - @staticmethod - @interfacedoc - def name(): - return "Spectrogram test" - - @interfacedoc - def set_colors(self, background, scheme): - self.bg_color = background - self.color_scheme = scheme - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(Spectrogram, self).setup(channels, samplerate, nframes) - if self.graph: - self.graph = None - self.graph = SpectrogramImage(self.width, self.height, self.nframes(), self.samplerate(), self.FFT_SIZE, - bg_color=self.bg_color, color_scheme=self.color_scheme, filename=self.filename) - - @interfacedoc - def process(self, frames, eod=False): - self.graph.process(frames, eod) - return frames, eod - - @interfacedoc - def render(self): - if self.filename: - self.graph.save() - return self.graph.image - - -class Duration(Processor): - """A rather useless duration analyzer. Its only purpose is to test the - nframes characteristic.""" - implements(IValueAnalyzer) - - @interfacedoc - def setup(self, channels, samplerate, nframes): - if not nframes: - raise Exception("nframes argument required") - super(Duration, self).setup(channels, samplerate, nframes) - - @staticmethod - @interfacedoc - def id(): - return "test_duration" - - @staticmethod - @interfacedoc - def name(): - return "Duration analyzer" - - @staticmethod - @interfacedoc - def unit(): - return "seconds" - - def result(self): - return self.input_nframes / float(self.input_samplerate) - -class FixedInputProcessor(Processor): - """Processor which does absolutely nothing except illustrating the use - of the FixedInputSizeAdapter. It also tests things a bit.""" - - implements(IProcessor) - - BUFFER_SIZE = 1024 - - @staticmethod - @interfacedoc - def id(): - return "test_fixed" - - @interfacedoc - def setup(self, channels, samplerate, nframes): - super(FixedInputProcessor, self).setup(channels, samplerate, nframes) - self.adapter = FixedSizeInputAdapter(self.BUFFER_SIZE, channels, pad=True) - - @interfacedoc - def process(self, frames, eod=False): - for buffer, end in self.adapter.process(frames, eod): - # Test that the adapter is actually doing the job: - if len(buffer) != self.BUFFER_SIZE: - raise Exception("Bad buffer size from adapter") - - return frames, eod - - - - - diff --git a/tests/api/gstreamer.py b/tests/api/gstreamer.py deleted file mode 100644 index 897ac42..0000000 --- a/tests/api/gstreamer.py +++ /dev/null @@ -1,238 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2007-2009 Parisson -# Copyright (c) 2007 Olivier Guilyardi -# Copyright (c) 2007-2009 Guillaume Pellerin -# -# This file is part of TimeSide. - -# TimeSide is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. - -# TimeSide is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with TimeSide. If not, see . - -# Author: Paul Brossier - -from timeside.core import Processor, implements, interfacedoc -from timeside.api import IDecoder, IEncoder -from numpy import array, frombuffer, getbuffer, float32 - -import pygst -pygst.require('0.10') -import gst -import gobject -gobject.threads_init () - -class FileDecoder(Processor): - """ gstreamer-based decoder """ - implements(IDecoder) - - # duration ms, before discovery process times out - MAX_DISCOVERY_TIME = 3000 - - audioformat = None - audiochannels = None - audiorate = None - audionframes = None - mimetype = '' - - # IProcessor methods - - @staticmethod - @interfacedoc - def id(): - return "test_gstreamerdec" - - def setup(self, channels = None, samplerate = None, nframes = None): - # the output data format we want - caps = "audio/x-raw-float, width=32" - pipeline = gst.parse_launch('''uridecodebin uri=%s - ! audioconvert - ! %s - ! appsink name=sink sync=False ''' % (self.uri, caps)) - # store a pointer to appsink in our decoder object - self.sink = pipeline.get_by_name('sink') - # adjust length of emitted buffers - # self.sink.set_property('blocksize', 0x10000) - # start pipeline - pipeline.set_state(gst.STATE_PLAYING) - - @interfacedoc - def channels(self): - return self.audiochannels - - @interfacedoc - def samplerate(self): - return self.audiorate - - @interfacedoc - def nframes(self): - return self.audionframes - - @interfacedoc - def process(self, frames = None, eod = False): - try: - buf = self.sink.emit('pull-buffer') - except SystemError, e: - # should never happen - print 'SystemError', e - return array([0.]), True - if buf == None: - return array([0.]), True - return self.gst_buffer_to_numpy_array(buf), False - - @interfacedoc - def release(self): - # nothing to do for now - pass - - ## IDecoder methods - - @interfacedoc - def __init__(self, uri): - - # is this a file? - import os.path - if os.path.exists(uri): - # get the absolute path - uri = os.path.abspath(uri) - # first run the file/uri through the discover pipeline - self.discover(uri) - # and make a uri of it - from urllib import quote - self.uri = 'file://'+quote(uri) - - @interfacedoc - def format(self): - # TODO check - return self.mimetype - - @interfacedoc - def encoding(self): - # TODO check - return self.mimetype.split('/')[-1] - - @interfacedoc - def resolution(self): - # TODO check: width or depth? - return self.audiowidth - - @interfacedoc - def metadata(self): - # TODO check - return self.tags - - ## gst.extend discoverer - - def discover(self, path): - """ gstreamer based helper function to get file attributes """ - from gst.extend import discoverer - d = discoverer.Discoverer(path, timeout = self.MAX_DISCOVERY_TIME) - d.connect('discovered', self.discovered) - self.mainloop = gobject.MainLoop() - d.discover() - self.mainloop.run() - - def discovered(self, d, is_media): - """ gstreamer based helper executed upon discover() completion """ - if is_media and d.is_audio: - # copy the discoverer attributes to self - self.audiorate = d.audiorate - self.mimetype= d.mimetype - self.audiochannels = d.audiochannels - self.audiowidth = d.audiowidth - # conversion from time in nanoseconds to frames - from math import ceil - duration = d.audiorate * d.audiolength * 1.e-9 - self.audionframes = int (ceil ( duration ) ) - self.tags = d.tags - elif not d.is_audio: - print "error, no audio found!" - else: - print "fail", path - self.mainloop.quit() - - def gst_buffer_to_numpy_array(self, buf): - """ gstreamer buffer to numpy array conversion """ - chan = self.audiochannels - samples = frombuffer(buf.data, dtype=float32) - samples.resize([len(samples)/chan, chan]) - return samples - -class WavEncoder(Processor): - """ gstreamer-based encoder """ - implements(IEncoder) - - def __init__(self, output): - self.file = None - if isinstance(output, basestring): - self.filename = output - else: - raise Exception("Streaming not supported") - - @interfacedoc - def setup(self, channels=None, samplerate=None, nframes=None): - super(WavEncoder, self).setup(channels, samplerate, nframes) - # TODO open file for writing - # the output data format we want - pipeline = gst.parse_launch(''' appsrc name=src - ! audioconvert - ! wavenc - ! filesink location=%s ''' % self.filename) - # store a pointer to appsink in our encoder object - self.src = pipeline.get_by_name('src') - srccaps = gst.Caps("""audio/x-raw-float, - endianness=(int)1234, - channels=(int)%s, - width=(int)32, - rate=(int)%d""" % (int(channels), int(samplerate))) - self.src.set_property("caps", srccaps) - - # start pipeline - pipeline.set_state(gst.STATE_PLAYING) - self.pipeline = pipeline - - @staticmethod - @interfacedoc - def id(): - return "test_gstreamerenc" - - @staticmethod - @interfacedoc - def description(): - return "Gstreamer based encoder" - - @staticmethod - @interfacedoc - def file_extension(): - return "wav" - - @staticmethod - @interfacedoc - def mime_type(): - return "audio/x-wav" - - @interfacedoc - def set_metadata(self, metadata): - #TODO - pass - - @interfacedoc - def process(self, frames, eod=False): - buf = self.numpy_array_to_gst_buffer(frames) - self.src.emit('push-buffer', buf) - if eod: self.src.emit('end-of-stream') - return frames, eod - - def numpy_array_to_gst_buffer(self, frames): - """ gstreamer buffer to numpy array conversion """ - buf = gst.Buffer(getbuffer(frames)) - return buf diff --git a/tests/api/test_lolevel.py b/tests/api/test_lolevel.py deleted file mode 100644 index 786b141..0000000 --- a/tests/api/test_lolevel.py +++ /dev/null @@ -1,60 +0,0 @@ -from timeside.tests.api import examples - -use_gst = 0 -if use_gst: - from timeside.tests.api.gstreamer import FileDecoder, WavEncoder -else: - from timeside.tests.api.examples import FileDecoder, WavEncoder - -import sys -if len(sys.argv) > 1: - source = sys.argv[1] -else: - import os.path - source= os.path.join (os.path.dirname(__file__), "../samples/guitar.wav") - -Decoder = FileDecoder -print "Creating decoder with id=%s for: %s" % (Decoder.id(), source) -decoder = Decoder(source) -analyzer = examples.MaxLevel() -decoder.setup() -nchannels = decoder.channels() -samplerate = decoder.samplerate() -nframes = decoder.nframes() -analyzer.setup(nchannels, samplerate) - -print "Stats: duration=%f, nframes=%d, nchannels=%d, samplerate=%d, resolution=%d" % ( - nframes / float(samplerate), nframes, nchannels, samplerate, decoder.resolution()) - -while True: - frames, eod = decoder.process() - analyzer.process(frames, eod) - if eod: - break - -max_level = analyzer.result() -print "Max level: %f" % max_level - -destination = "normalized.wav" -Encoder = WavEncoder -print "Creating encoder with id=%s for: %s" % (Encoder.id(), destination) -encoder = Encoder(destination) - -gain = 1 -if max_level > 0: - gain = 0.9 / max_level - -effect = examples.Gain(gain) - -decoder.setup() -effect.setup(decoder.channels(), decoder.samplerate()) -encoder.setup(effect.channels(), effect.samplerate()) - -print "Applying effect id=%s with gain=%f" % (effect.id(), gain) - -while True: - frames, eod = decoder.process() - encoder.process(*effect.process(frames, eod)) - if eod: - break - diff --git a/tests/api/test_pipe.py b/tests/api/test_pipe.py deleted file mode 100644 index eaa0850..0000000 --- a/tests/api/test_pipe.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -from timeside.tests.api.examples import Gain -from timeside.core import * -from timeside.decoder import * -from timeside.analyzer import * -from timeside.encoder import * -from timeside.api import * -from sys import stdout - -import os.path -source = os.path.join(os.path.dirname(__file__), "../samples/guitar.wav") - -print "Normalizing %s" % source -decoder = FileDecoder(source) -maxlevel = MaxLevel() -duration = Duration() - -(decoder | maxlevel | duration).run() - -gain = 1 -if maxlevel.result() > 0: - gain = 0.9 / maxlevel.result() - -print "input maxlevel: %f" % maxlevel.result() -print "gain: %f" % gain -print "duration: %f %s" % (duration.result(), duration.unit()) - -gain = Gain(gain) -encoder = WavEncoder("normalized.wav") - -subpipe = gain | maxlevel - -(decoder | subpipe | encoder).run() - -print "output maxlevel: %f" % maxlevel.result() - - diff --git a/tests/api/test_pipe_spectrogram.py b/tests/api/test_pipe_spectrogram.py deleted file mode 100644 index 5ba90eb..0000000 --- a/tests/api/test_pipe_spectrogram.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -from timeside.core import * -from timeside.api import * -from timeside.decoder import * -from timeside.grapher import * - -image_file = '../results/img/spectrogram.png' -source = os.path.join(os.path.dirname(__file__), "../samples/sweep.wav") - -decoder = FileDecoder(source) -spectrogram = Spectrogram(width=1024, height=256, output=image_file, bg_color=(0,0,0), color_scheme='default') - -(decoder | spectrogram).run() - -print 'frames per pixel = ', spectrogram.graph.samples_per_pixel -print "render spectrogram to: %s" % image_file -spectrogram.render() - - diff --git a/tests/api/test_pipe_waveform.py b/tests/api/test_pipe_waveform.py deleted file mode 100644 index d21ecdc..0000000 --- a/tests/api/test_pipe_waveform.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- - -import os -from timeside.core import * -from timeside.api import * -from timeside.decoder import * -from timeside.grapher import * - -sample_dir = '../samples' -img_dir = '../results/img' -if not os.path.exists(img_dir): - os.mkdir(img_dir) - -test_dict = {'sweep.wav': 'waveform_wav.png', - 'sweep.flac': 'waveform_flac.png', - 'sweep.ogg': 'waveform_ogg.png', - 'sweep.mp3': 'waveform_mp3.png', - } - -for source, image in test_dict.iteritems(): - audio = os.path.join(os.path.dirname(__file__), sample_dir + os.sep + source) - image = img_dir + os.sep + image - print 'Test : decoder(%s) | waveform (%s)' % (source, image) - decoder = FileDecoder(audio) - waveform = Waveform(width=1024, height=256, output=image, bg_color=(0,0,0), color_scheme='default') - (decoder | waveform).run() - print 'frames per pixel = ', waveform.graph.samples_per_pixel - print "render waveform to: %s" % image - waveform.render() - - diff --git a/tests/listprocessors.py b/tests/listprocessors.py deleted file mode 100644 index 840c087..0000000 --- a/tests/listprocessors.py +++ /dev/null @@ -1,12 +0,0 @@ -import timeside - -def list_processors(interface, prefix=""): - print prefix + interface.__name__ - subinterfaces = interface.__subclasses__() - for i in subinterfaces: - list_processors(i, prefix + " ") - processors = timeside.processors(interface, False) - for p in processors: - print prefix + " %s [%s]" % (p.__name__, p.id()) - -list_processors(timeside.api.IProcessor) diff --git a/tests/samples/guitar.wav b/tests/samples/guitar.wav deleted file mode 100644 index b5a9e80..0000000 Binary files a/tests/samples/guitar.wav and /dev/null differ diff --git a/tests/samples/sweep.flac b/tests/samples/sweep.flac deleted file mode 100644 index decee06..0000000 Binary files a/tests/samples/sweep.flac and /dev/null differ diff --git a/tests/samples/sweep.mp3 b/tests/samples/sweep.mp3 deleted file mode 100644 index dc75fe0..0000000 Binary files a/tests/samples/sweep.mp3 and /dev/null differ diff --git a/tests/samples/sweep.ogg b/tests/samples/sweep.ogg deleted file mode 100644 index e30bf99..0000000 Binary files a/tests/samples/sweep.ogg and /dev/null differ diff --git a/tests/samples/sweep.wav b/tests/samples/sweep.wav deleted file mode 100644 index fc28a32..0000000 Binary files a/tests/samples/sweep.wav and /dev/null differ diff --git a/tests/samples/sweep_source.wav b/tests/samples/sweep_source.wav deleted file mode 100644 index 5dcf9ba..0000000 Binary files a/tests/samples/sweep_source.wav and /dev/null differ diff --git a/tests/testcomponent.py b/tests/testcomponent.py deleted file mode 100644 index 6638c30..0000000 --- a/tests/testcomponent.py +++ /dev/null @@ -1,166 +0,0 @@ - -from timeside.component import * -from timeside.tests import TestCase, TestRunner -import unittest - -__all__ = ['TestComponentArchitecture'] - -class TestComponentArchitecture(TestCase): - "Test the component and interface system" - - def testOneInterface(self): - "Test a component implementing one interface" - self.assertSameList(implementations(I1), [C1]) - - def testTwoInterfaces(self): - "Test a component implementing two interfaces" - self.assertSameList(implementations(I2), [C2]) - self.assertSameList(implementations(I3), [C2]) - - def testTwoImplementations(self): - "Test an interface implemented by two components" - self.assertSameList(implementations(I4), [C3, C4]) - - def testInterfaceInheritance(self): - "Test whether a component implements an interface's parent" - self.assertSameList(implementations(I5), [C5]) - - def testImplementationInheritance(self): - "Test that a component doesn't implement the interface implemented by its parent" - self.assertSameList(implementations(I7), [C6]) - - def testImplementationRedundancy(self): - "Test implementation redundancy across inheritance" - self.assertSameList(implementations(I8), [C8, C9]) - - def testAbstractImplementation(self): - "Test abstract implementation" - self.assertSameList(implementations(I11), []) - self.assertSameList(implementations(I11, abstract=True), [C11]) - - def testInterfaceDoc(self): - "Test @interfacedoc decorator" - self.assertEquals(C10.test.__doc__, "testdoc") - - def testInterfaceDocStatic(self): - "Test @interfacedoc decorator on static method" - self.assertEquals(C10.teststatic.__doc__, "teststaticdoc") - - def testIntefaceDocReversed(self): - "Test @interfacedoc on static method (decorators reversed)" - - try: - - class BogusDoc1(Component): - implements(I10) - - @interfacedoc - @staticmethod - def teststatic(self): - pass - - self.fail("No error raised with reversed decorators") - - except ComponentError: - pass - - def testInterfaceDocBadMethod(self): - "Test @interfacedoc with unexistant method in interface" - - try: - class BogusDoc2(Component): - implements(I10) - - @interfacedoc - def nosuchmethod(self): - pass - - self.fail("No error raised when decorating an unexistant method") - - except ComponentError: - pass - -class I1(Interface): - pass - -class I2(Interface): - pass - -class I3(Interface): - pass - -class I4(Interface): - pass - -class I5(Interface): - pass - -class I6(I5): - pass - -class I7(Interface): - pass - -class I8(Interface): - pass - -class I9(I8): - pass - -class I10(Interface): - def test(self): - """testdoc""" - - @staticmethod - def teststatic(self): - """teststaticdoc""" - -class I11(Interface): - pass - -class C1(Component): - implements(I1) - -class C2(Component): - implements(I2, I3) - -class C3(Component): - implements(I4) - -class C4(Component): - implements(I4) - -class C5(Component): - implements(I6) - -class C6(Component): - implements(I7) - -class C7(C6): - pass - -class C8(Component): - implements(I8) - -class C9(Component): - implements(I8, I9) - -class C10(Component): - implements(I10) - - @interfacedoc - def test(self): - pass - - @staticmethod - @interfacedoc - def teststatic(self): - pass - -class C11(Component): - abstract() - implements(I11) - -if __name__ == '__main__': - unittest.main(testRunner=TestRunner()) - diff --git a/tests/testinputadapter.py b/tests/testinputadapter.py deleted file mode 100644 index dca0f63..0000000 --- a/tests/testinputadapter.py +++ /dev/null @@ -1,71 +0,0 @@ -from timeside.core import FixedSizeInputAdapter -from timeside.tests import TestCase, TestRunner -import numpy -import unittest - -class TestFixedSizeInputAdapter(TestCase): - "Test the fixed-sized input adapter" - - def assertIOEquals(self, adapter, input, input_eod, output, output_eod=None): - output = output[:] - output.reverse() - _eod = None - for buffer, _eod in adapter.process(input, input_eod): - a = output.pop() - if not numpy.array_equiv(buffer, a): - self.fail("\n-- Actual --\n%s\n -- Expected -- \n%s\n" % (str(buffer), str(a))) - - if _eod != output_eod: - self.fail("eod do not match: %s != %s", (str(_eod), str(output_eod))) - - if output: - self.fail("trailing expected data: %s" % output) - - def setUp(self): - self.data = numpy.arange(44).reshape(2,22).transpose() - - def testTwoChannels(self): - "Test simple stream with two channels" - adapter = FixedSizeInputAdapter(4, 2) - - self.assertEquals(len(self.data), adapter.nframes(len(self.data))) - - self.assertIOEquals(adapter, self.data[0:1], False, []) - self.assertIOEquals(adapter, self.data[1:5], False, [self.data[0:4]], False) - self.assertIOEquals(adapter, self.data[5:12], False, [self.data[4:8], self.data[8:12]], False) - self.assertIOEquals(adapter, self.data[12:13], False, []) - self.assertIOEquals(adapter, self.data[13:14], False, []) - self.assertIOEquals(adapter, self.data[14:18], False, [self.data[12:16]], False) - self.assertIOEquals(adapter, self.data[18:20], False, [self.data[16:20]], False) - self.assertIOEquals(adapter, self.data[20:21], False, []) - self.assertIOEquals(adapter, self.data[21:22], True, [self.data[20:22]], True) - - def testPadding(self): - "Test automatic padding support" - adapter = FixedSizeInputAdapter(4, 2, pad=True) - - self.assertEquals(len(self.data) + 2, adapter.nframes(len(self.data))) - - self.assertIOEquals(adapter, self.data[0:21], False, - [self.data[0:4], self.data[4:8], self.data[8:12], self.data[12:16], self.data[16:20]], - False) - - self.assertIOEquals(adapter, self.data[21:22], True, [[ - [20, 42], - [21, 43], - [0, 0], - [0, 0] - ]], True) - - def testSizeMultiple(self): - "Test a stream which contain a multiple number of buffers" - adapter = FixedSizeInputAdapter(4, 2) - - self.assertIOEquals(adapter, self.data[0:20], True, - [self.data[0:4], self.data[4:8], self.data[8:12], self.data[12:16], self.data[16:20]], - True) - - -if __name__ == '__main__': - unittest.main(testRunner=TestRunner()) - diff --git a/timeside/__init__.py b/timeside/__init__.py new file mode 100644 index 0000000..81ad004 --- /dev/null +++ b/timeside/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from core import * +from metadata import Metadata +import decoder +import encoder +import analyzer +import grapher diff --git a/timeside/analyzer/__init__.py b/timeside/analyzer/__init__.py new file mode 100644 index 0000000..0bd2039 --- /dev/null +++ b/timeside/analyzer/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from timeside.analyzer.core import * +from timeside.analyzer.duration import * +from timeside.analyzer.max_level import * +#from timeside.analyzer.mean_level import * +#from timeside.analyzer.dc import * diff --git a/timeside/analyzer/core.py b/timeside/analyzer/core.py new file mode 100644 index 0000000..cbb9a17 --- /dev/null +++ b/timeside/analyzer/core.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Authors: +# Guillaume Pellerin + +pass + + diff --git a/timeside/analyzer/dc.py b/timeside/analyzer/dc.py new file mode 100644 index 0000000..e92cfd5 --- /dev/null +++ b/timeside/analyzer/dc.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +from timeside.analyzer.core import * +from timeside.api import IValueAnalyzer +import numpy + + +class MeanDCShift(Processor): + implements(IValueAnalyzer) + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(MeanDCShift, self).setup(channels, samplerate, nframes) + self.value = 0 + + @staticmethod + @interfacedoc + def id(): + return "dc" + + @staticmethod + @interfacedoc + def name(): + return "Mean DC shift" + + @staticmethod + @interfacedoc + def unit(): + return "%" + + def __str__(self): + return "%s %s" % (str(self.value), unit()) + + def process(self, frames, eod=False): + self.value = numpy.round(100*numpy.mean(samples),4) + return frames, eod + + def result(self): + return self.value + diff --git a/timeside/analyzer/duration.py b/timeside/analyzer/duration.py new file mode 100644 index 0000000..7c2269c --- /dev/null +++ b/timeside/analyzer/duration.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter +from timeside.analyzer.core import * +from timeside.api import IValueAnalyzer + + +class Duration(Processor): + """A rather useless duration analyzer. Its only purpose is to test the + nframes characteristic.""" + implements(IValueAnalyzer) + + @interfacedoc + def setup(self, channels, samplerate, nframes): + if not nframes: + raise Exception("nframes argument required") + super(Duration, self).setup(channels, samplerate, nframes) + + @staticmethod + @interfacedoc + def id(): + return "duration" + + @staticmethod + @interfacedoc + def name(): + return "Duration" + + @staticmethod + @interfacedoc + def unit(): + return "seconds" + + def result(self): + return self.input_nframes / float(self.input_samplerate) + \ No newline at end of file diff --git a/timeside/analyzer/max_level.py b/timeside/analyzer/max_level.py new file mode 100644 index 0000000..9ecc323 --- /dev/null +++ b/timeside/analyzer/max_level.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin +# Copyright (c) 2009 Olivier Guilyardi + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter +from timeside.analyzer.core import * +from timeside.api import IValueAnalyzer + + +class MaxLevel(Processor): + implements(IValueAnalyzer) + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(MaxLevel, self).setup(channels, samplerate, nframes) + self.max_value = 0 + + @staticmethod + @interfacedoc + def id(): + return "maxlevel" + + @staticmethod + @interfacedoc + def name(): + return "Max level" + + @staticmethod + @interfacedoc + def unit(): + # power? amplitude? + return "" + + def process(self, frames, eod=False): + max = frames.max() + if max > self.max_value: + self.max_value = max + + return frames, eod + + def result(self): + return self.max_value + + diff --git a/timeside/analyzer/mean_level.py b/timeside/analyzer/mean_level.py new file mode 100644 index 0000000..8b68324 --- /dev/null +++ b/timeside/analyzer/mean_level.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +from timeside.analyzer.core import * +from timeside.api import IValueAnalyzer +import numpy + + +class MeanLevel(Processor): + implements(IValueAnalyzer) + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(MeanLevel, self).setup(channels, samplerate, nframes) + self.value = -140 + + @staticmethod + @interfacedoc + def id(): + return "meanlevel" + + @staticmethod + @interfacedoc + def name(): + return "Mean RMS level" + + @staticmethod + @interfacedoc + def unit(): + return "dB" + + def __str__(self): + return "%s %s" % (str(self.value), unit()) + + def process(self, frames, eod=False): + max = numpy.round(20*numpy.log10(numpy.mean(numpy.sqrt(numpy.square(frames.max())))), 2) + if max > self.value: + self.value = max + + return frames, eod + + def result(self): + return self.value + diff --git a/timeside/analyzer/vamp/__init__.py b/timeside/analyzer/vamp/__init__.py new file mode 100644 index 0000000..1ded83d --- /dev/null +++ b/timeside/analyzer/vamp/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from timeside.analyzer.vamp.core import * diff --git a/timeside/analyzer/vamp/core.py b/timeside/analyzer/vamp/core.py new file mode 100644 index 0000000..49ae9a0 --- /dev/null +++ b/timeside/analyzer/vamp/core.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +from timeside.core import * +from tempfile import NamedTemporaryFile +from timeside.exceptions import SubProcessError +import os +import random +import subprocess +import signal +import time + +class VampCoreAnalyzer: + """Parent class for Vamp plugin drivers""" + + def __init__(self): + self.vamp_path = '/usr/lib/vamp/' + # needs vamp-examples package + self.host = 'vamp-simple-host' + self.buffer_size = 0xFFFF + + def id(self): + return "vamp_plugins" + + def name(self): + return "Vamp plugins" + + def unit(self): + return "" + + def get_plugins_list(self): + if os.path.exists(self.vamp_path): + args = ' --list-outputs' + command = self.host + args + #tmp_file = NamedTemporaryFile() + data = self.core_process(command, self.buffer_size) + text = '' + plugins = [] + for chunk in data: + text = text + chunk + lines = text.split('\n') + for line in lines: + if line != '': + struct = line.split(':') + struct = struct[1:] + plugins.append(struct) + return plugins + else: + return [] + + def get_wav_path(self, media_item): + return settings.MEDIA_ROOT + '/' + media_item.file + #return media_item + + def render(self, plugin, media_item): + self.wavFile = self.get_wav_path(media_item) + args = ' -s ' + ':'.join(plugin) + ' ' + str(self.wavFile) + command = command = self.host + args + data = self.core_process(command, self.buffer_size) + string = '' + values = {} + for chunk in data: + string = string + chunk + lines = string.split('\n') + for line in lines: + if line != '': + struct = line.split(':') + values[struct[0]] = struct[1] + return values + + def core_process(self, command, buffer_size): + """Encode and stream audio data through a generator""" + + __chunk = 0 + + try: + proc = subprocess.Popen(command, + shell = True, + bufsize = buffer_size, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + close_fds = True) + except: + raise SubProcessError('Command failure:', command, proc) + + # Core processing + while True: + __chunk = proc.stdout.read(buffer_size) + status = proc.poll() + if status != None and status != 0: + raise SubProcessError('Command failure:', command, proc) + if len(__chunk) == 0: + break + yield __chunk + + diff --git a/timeside/api.py b/timeside/api.py new file mode 100644 index 0000000..7ceaecf --- /dev/null +++ b/timeside/api.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2009 Parisson +# Copyright (c) 2007 Olivier Guilyardi +# Copyright (c) 2007-2009 Guillaume Pellerin +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +from timeside.component import Interface + +class IProcessor(Interface): + """Common processor interface""" + + @staticmethod + def id(): + """Short alphanumeric, lower-case string which uniquely identify this + processor, suitable for use as an HTTP/GET argument value, in filenames, + etc...""" + + # implementation: only letters and digits are allowed. An exception will + # be raised by MetaProcessor if the id is malformed or not unique amongst + # registered processors. + + def setup(self, channels=None, samplerate=None, nframes=None): + """Allocate internal resources and reset state, so that this processor is + ready for a new run. + + The channels, samplerate and/or nframes arguments may be required by + processors which accept input. An error will occur if any of + these arguments is passed to an output-only processor such as a decoder. + """ + + # implementations should always call the parent method + + def channels(self): + """Number of channels in the data returned by process(). May be different from + the number of channels passed to setup()""" + + def samplerate(self): + """Samplerate of the data returned by process(). May be different from + the samplerate passed to setup()""" + + def nframes(): + """The total number of frames that this processor can output, or None if + the duration is unknown.""" + + def process(self, frames=None, eod=False): + """Process input frames and return a (output_frames, eod) tuple. + Both input and output frames are 2D numpy arrays, where columns are + channels, and containing an undetermined number of frames. eod=True + means that the end-of-data has been reached. + + Output-only processors (such as decoders) will raise an exception if the + frames argument is not None. All processors (even encoders) return data, + even if that means returning the input unchanged. + + Warning: it is required to call setup() before this method.""" + + def release(self): + """Release resources owned by this processor. The processor cannot + be used anymore after calling this method.""" + + # implementations should always call the parent method + +class IEncoder(IProcessor): + """Encoder driver interface. Each encoder is expected to support a specific + format.""" + + def __init__(self, output): + """Create a new encoder. output can either be a filename or a python callback + function/method for streaming mode. + + The streaming callback prototype is: callback(data, eod) + Where data is a block of binary data of an undetermined size, and eod + True when end-of-data is reached.""" + + # implementation: the constructor must always accept the output argument. It may + # accept extra arguments such as bitrate, depth, etc.., but these must be optionnal + + @staticmethod + def format(): + """Return the encode/encoding format as a short string + Example: "MP3", "OGG", "AVI", ... + """ + + @staticmethod + def description(): + """Return a string describing what this encode format provides, is good + for, etc... The description is meant to help the end user decide what + format is good for him/her + """ + + @staticmethod + def file_extension(): + """Return the filename extension corresponding to this encode format""" + + @staticmethod + def mime_type(): + """Return the mime type corresponding to this encode format""" + + def set_metadata(self, metadata): + """Set the metadata to be embedded in the encoded output. + + In non-streaming mode, this method updates the metadata directly into the + output file, without re-encoding the audio data, provided this file already + exists. + + It isn't required to call this method, but if called, it must be before + process().""" + +class IDecoder(IProcessor): + """Decoder driver interface. Decoders are different of encoders in that + a given driver may support several input formats, hence this interface doesn't + export any static method, all informations are dynamic.""" + + def __init__(self, filename): + """Create a new decoder for filename.""" + # implementation: additional optionnal arguments are allowed + + def format(): + """Return a user-friendly file format string""" + + def encoding(): + """Return a user-friendly encoding string""" + + def resolution(): + """Return the sample width (8, 16, etc..) of original audio file/stream, + or None if not applicable/known""" + + def metadata(self): + """Return the metadata embedded into the encoded stream, if any.""" + +class IGrapher(IProcessor): + """Media item visualizer driver interface""" + + # implementation: graphers which need to know the total number of frames + # should raise an exception in setup() if the nframes argument is None + + def __init__(self, width, height): + """Create a new grapher. width and height are generally + in pixels but could be something else for eg. svg rendering, etc.. """ + + # implementation: additional optionnal arguments are allowed + + @staticmethod + def name(): + """Return the graph name, such as "Waveform", "Spectral view", + etc.. """ + + def set_colors(self, background=None, scheme=None): + """Set the colors used for image generation. background is a RGB tuple, + and scheme a a predefined color theme name""" + + def render(self): + """Return a PIL Image object visually representing all of the data passed + by repeatedly calling process()""" + +class IAnalyzer(IProcessor): + """Media item analyzer driver interface. This interface is abstract, it doesn't + describe a particular type of analyzer but is rather meant to group analyzers. + In particular, the way the result is returned may greatly vary from sub-interface + to sub-interface. For example the IValueAnalyzer returns a final single numeric + result at the end of the whole analysis. But some other analyzers may return + numpy arrays, and this, either at the end of the analysis, or from process() + for each block of data (as in Vamp).""" + + def __init__(self): + """Create a new analyzer.""" + # implementation: additional optionnal arguments are allowed + + @staticmethod + def name(): + """Return the analyzer name, such as "Mean Level", "Max level", + "Total length, etc.. """ + + @staticmethod + def unit(): + """Return the unit of the data such as "dB", "seconds", etc... """ + +class IValueAnalyzer(IAnalyzer): + """Interface for analyzers which return a single numeric value from result()""" + + def result(): + """Return the final result of the analysis performed over the data passed by + repeatedly calling process()""" + + def __str__(self): + """Return a human readable string containing both result and unit + ('5.30dB', '4.2s', etc...)""" + +class IEffect(IProcessor): + """Effect processor interface""" + + def __init__(self): + """Create a new effect.""" + # implementation: additional optionnal arguments are allowed + + @staticmethod + def name(): + """Return the effect name""" + diff --git a/timeside/component.py b/timeside/component.py new file mode 100644 index 0000000..ff20670 --- /dev/null +++ b/timeside/component.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2009 Olivier Guilyardi +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + + +# This file defines a generic object interface mechanism and +# a way to determine which components implements a given interface. +# +# For example, the following defines the Music class as implementing the +# listenable interface. +# +# class Listenable(Interface): +# pass +# +# class Music(Component): +# implements(Listenable) +# +# Several class can implements a such interface, and it is possible to +# discover which class implements it with implementations(): +# +# list_of_classes = implementations(Listenable) +# +# This mechanism support inheritance of interfaces: a class implementing a given +# interface is also considered to implement all the ascendants of this interface. +# +# However, inheritance is not supported for components. The descendants of a class +# implementing a given interface are not automatically considered to implement this +# interface too. + +__all__ = ['Component', 'MetaComponent', 'implements', 'abstract', + 'interfacedoc', 'Interface', 'implementations', 'ComponentError'] + +class Interface(object): + """Marker base class for interfaces.""" + +def implements(*interfaces): + """Registers the interfaces implemented by a component when placed in the + class header""" + MetaComponent.implements.extend(interfaces) + +def abstract(): + """Declare a component as abstract when placed in the class header""" + MetaComponent.abstract = True + +def implementations(interface, recurse=True, abstract=False): + """Returns the components implementing interface, and if recurse, any of + the descendants of interface. If abstract is True, also return the + abstract implementations.""" + result = [] + find_implementations(interface, recurse, abstract, result) + return result + +def interfacedoc(func): + if isinstance(func, staticmethod): + raise ComponentError("@interfacedoc can't handle staticmethod (try to put @staticmethod above @interfacedoc)") + + if not func.__doc__: + func.__doc__ = "@interfacedoc" + func._interfacedoc = True + return func + +class MetaComponent(type): + """Metaclass of the Component class, used mainly to register the interface + declared to be implemented by a component.""" + + implementations = [] + implements = [] + abstract = False + + def __new__(cls, name, bases, d): + new_class = type.__new__(cls, name, bases, d) + + # Register implementations + if MetaComponent.implements: + for i in MetaComponent.implements: + MetaComponent.implementations.append({ + 'interface': i, + 'class': new_class, + 'abstract': MetaComponent.abstract}) + + # Propagate @interfacedoc + for name in new_class.__dict__: + member = new_class.__dict__[name] + if isinstance(member, staticmethod): + member = getattr(new_class, name) + + if member.__doc__ == "@interfacedoc": + if_member = None + for i in MetaComponent.implements: + if hasattr(i, name): + if_member = getattr(i, name) + if not if_member: + raise ComponentError("@interfacedoc: %s.%s: no such member in implemented interfaces: %s" + % (new_class.__name__, name, str(MetaComponent.implements))) + member.__doc__ = if_member.__doc__ + + MetaComponent.implements = [] + MetaComponent.abstract = False + + return new_class + +class Component(object): + """Base class of all components""" + __metaclass__ = MetaComponent + +def extend_unique(list1, list2): + """Extend list1 with list2 as list.extend(), but doesn't append duplicates + to list1""" + for item in list2: + if item not in list1: + list1.append(item) + +def find_implementations(interface, recurse, abstract, result): + """Find implementations of an interface or of one of its descendants and + extend result with the classes found.""" + for item in MetaComponent.implementations: + if (item['interface'] == interface and (abstract or not item['abstract'])): + extend_unique(result, [item['class']]) + + if recurse: + subinterfaces = interface.__subclasses__() + if subinterfaces: + for i in subinterfaces: + find_implementations(i, recurse, abstract, result) + +class ComponentError(Exception): + pass diff --git a/timeside/core.py b/timeside/core.py new file mode 100644 index 0000000..f5b3c31 --- /dev/null +++ b/timeside/core.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2009 Olivier Guilyardi +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +from timeside.component import * +from timeside.api import IProcessor +from timeside.exceptions import Error, ApiError +import re +import numpy + +__all__ = ['Processor', 'MetaProcessor', 'implements', 'abstract', + 'interfacedoc', 'processors', 'get_processor', 'ProcessPipe', + 'FixedSizeInputAdapter'] + +_processors = {} + +class MetaProcessor(MetaComponent): + """Metaclass of the Processor class, used mainly for ensuring that processor + id's are wellformed and unique""" + + valid_id = re.compile("^[a-z][_a-z0-9]*$") + + def __new__(cls, name, bases, d): + new_class = MetaComponent.__new__(cls, name, bases, d) + if new_class in implementations(IProcessor): + id = str(new_class.id()) + if _processors.has_key(id): + raise ApiError("%s and %s have the same id: '%s'" + % (new_class.__name__, _processors[id].__name__, id)) + if not MetaProcessor.valid_id.match(id): + raise ApiError("%s has a malformed id: '%s'" + % (new_class.__name__, id)) + + _processors[id] = new_class + + return new_class + +class Processor(Component): + """Base component class of all processors""" + __metaclass__ = MetaProcessor + + abstract() + implements(IProcessor) + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + self.input_channels = channels + self.input_samplerate = samplerate + self.input_nframes = nframes + + # default channels(), samplerate() and nframes() implementations returns + # the input characteristics, but processors may change this behaviour by + # overloading those methods + @interfacedoc + def channels(self): + return self.input_channels + + @interfacedoc + def samplerate(self): + return self.input_samplerate + + @interfacedoc + def nframes(self): + return self.input_nframes + + @interfacedoc + def process(self, frames, eod): + return frames, eod + + @interfacedoc + def release(self): + pass + + def __del__(self): + self.release() + + def __or__(self, other): + return ProcessPipe(self, other) + +class FixedSizeInputAdapter(object): + """Utility to make it easier to write processors which require fixed-sized + input buffers.""" + + def __init__(self, buffer_size, channels, pad=False): + """Construct a new adapter: buffer_size is the desired buffer size in frames, + channels the number of channels, and pad indicates whether the last block should + be padded with zeros.""" + + self.buffer = numpy.empty((buffer_size, channels)) + self.buffer_size = buffer_size + self.len = 0 + self.pad = pad + + def nframes(self, input_nframes): + """Return the total number of frames that this adapter will output according to the + input_nframes argument""" + + nframes = input_nframes + if self.pad: + mod = input_nframes % self.buffer_size + if mod: + nframes += self.buffer_size - mod + + return nframes + + + def process(self, frames, eod): + """Returns an iterator over tuples of the form (buffer, eod) where buffer is a + fixed-sized block of data, and eod indicates whether this is the last block. + In case padding is deactivated the last block may be smaller than the buffer size. + """ + src_index = 0 + remaining = len(frames) + + while remaining: + space = self.buffer_size - self.len + copylen = remaining < space and remaining or space + src = frames[src_index:src_index + copylen] + if self.len == 0 and copylen == self.buffer_size: + # avoid unnecessary copy + buffer = src + else: + buffer = self.buffer + buffer[self.len:self.len + copylen] = src + + remaining -= copylen + src_index += copylen + self.len += copylen + + if self.len == self.buffer_size: + yield buffer, (eod and not remaining) + self.len = 0 + + if eod and self.len: + block = self.buffer + if self.pad: + self.buffer[self.len:self.buffer_size] = 0 + else: + block = self.buffer[0:self.len] + + yield block, True + self.len = 0 + +def processors(interface=IProcessor, recurse=True): + """Returns the processors implementing a given interface and, if recurse, + any of the descendants of this interface.""" + return implementations(interface, recurse) + + +def get_processor(processor_id): + """Return a processor by its id""" + if not _processors.has_key(processor_id): + raise Error("No processor registered with id: '%s'" + % processor_id) + + return _processors[processor_id] + +class ProcessPipe(object): + """Handle a pipe of processors""" + + def __init__(self, *others): + self.processors = [] + self |= others + + def __or__(self, other): + return ProcessPipe(self, other) + + def __ior__(self, other): + if isinstance(other, Processor): + self.processors.append(other) + elif isinstance(other, ProcessPipe): + self.processors.extend(other.processors) + else: + try: + iter(other) + except TypeError: + raise Error("Can not add this type of object to a pipe: %s", str(other)) + + for item in other: + self |= item + + return self + + def run(self): + """Setup/reset all processors in cascade and stream audio data along + the pipe. Also returns the pipe itself.""" + + source = self.processors[0] + items = self.processors[1:] + + # setup/reset processors and configure channels and samplerate throughout the pipe + source.setup() + last = source + for item in items: + item.setup(last.channels(), last.samplerate(), last.nframes()) + last = item + + # now stream audio data along the pipe + eod = False + while not eod: + frames, eod = source.process() + for item in items: + frames, eod = item.process(frames, eod) + + return self + diff --git a/timeside/decoder/__init__.py b/timeside/decoder/__init__.py new file mode 100644 index 0000000..9f37f75 --- /dev/null +++ b/timeside/decoder/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from timeside.decoder.core import * diff --git a/timeside/decoder/core.py b/timeside/decoder/core.py new file mode 100644 index 0000000..08d6e94 --- /dev/null +++ b/timeside/decoder/core.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +from timeside.core import * +import subprocess + +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2009 Parisson +# Copyright (c) 2007 Olivier Guilyardi +# Copyright (c) 2007-2009 Guillaume Pellerin +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Paul Brossier + +from timeside.core import Processor, implements, interfacedoc +from timeside.api import IDecoder +from numpy import array, frombuffer, getbuffer, float32 + +import pygst +pygst.require('0.10') +import gst +import gobject +gobject.threads_init () + + +class FileDecoder(Processor): + """ gstreamer-based decoder """ + implements(IDecoder) + + # duration ms, before discovery process times out + MAX_DISCOVERY_TIME = 3000 + + audioformat = None + audiochannels = None + audiorate = None + audionframes = None + mimetype = '' + + # IProcessor methods + + @staticmethod + @interfacedoc + def id(): + return "gstreamerdec" + + def setup(self, channels = None, samplerate = None, nframes = None): + # the output data format we want + caps = "audio/x-raw-float, width=32" + pipeline = gst.parse_launch('''uridecodebin uri=%s + ! audioconvert + ! %s + ! appsink name=sink sync=False ''' % (self.uri, caps)) + # store a pointer to appsink in our decoder object + self.sink = pipeline.get_by_name('sink') + # adjust length of emitted buffers + # self.sink.set_property('blocksize', 0x10000) + # start pipeline + pipeline.set_state(gst.STATE_PLAYING) + + @interfacedoc + def channels(self): + return self.audiochannels + + @interfacedoc + def samplerate(self): + return self.audiorate + + @interfacedoc + def nframes(self): + return self.audionframes + + @interfacedoc + def process(self, frames = None, eod = False): + try: + buf = self.sink.emit('pull-buffer') + except SystemError, e: + # should never happen + print 'SystemError', e + return array([0.]), True + if buf == None: + return array([0.]), True + return self.gst_buffer_to_numpy_array(buf), False + + @interfacedoc + def release(self): + # nothing to do for now + pass + + ## IDecoder methods + + @interfacedoc + def __init__(self, uri): + + # is this a file? + import os.path + if os.path.exists(uri): + # get the absolute path + uri = os.path.abspath(uri) + # first run the file/uri through the discover pipeline + self.discover(uri) + # and make a uri of it + from urllib import quote + self.uri = 'file://'+quote(uri) + + @interfacedoc + def format(self): + # TODO check + return self.mimetype + + @interfacedoc + def encoding(self): + # TODO check + return self.mimetype.split('/')[-1] + + @interfacedoc + def resolution(self): + # TODO check: width or depth? + return self.audiowidth + + @interfacedoc + def metadata(self): + # TODO check + return self.tags + + ## gst.extend discoverer + + def discover(self, path): + """ gstreamer based helper function to get file attributes """ + from gst.extend import discoverer + d = discoverer.Discoverer(path, timeout = self.MAX_DISCOVERY_TIME) + d.connect('discovered', self.discovered) + self.mainloop = gobject.MainLoop() + d.discover() + self.mainloop.run() + + def discovered(self, d, is_media): + """ gstreamer based helper executed upon discover() completion """ + if is_media and d.is_audio: + # copy the discoverer attributes to self + self.audiorate = d.audiorate + self.mimetype= d.mimetype + self.audiochannels = d.audiochannels + self.audiowidth = d.audiowidth + # conversion from time in nanoseconds to frames + from math import ceil + duration = d.audiorate * d.audiolength * 1.e-9 + self.audionframes = int (ceil ( duration ) ) + self.tags = d.tags + elif not d.is_audio: + print "error, no audio found!" + else: + print "fail", path + self.mainloop.quit() + + def gst_buffer_to_numpy_array(self, buf): + """ gstreamer buffer to numpy array conversion """ + chan = self.audiochannels + samples = frombuffer(buf.data, dtype=float32) + samples.resize([len(samples)/chan, chan]) + return samples + + +class SubProcessPipe: + + def __init__(self, command, stdin=None): + """Read media and stream data through a generator. + Taken from Telemeta (see http://telemeta.org)""" + + self.buffer_size = 0xFFFF + + if not stdin: + stdin = subprocess.PIPE + + self.proc = subprocess.Popen(command.encode('utf-8'), + shell = True, + bufsize = self.buffer_size, + stdin = stdin, + stdout = subprocess.PIPE, + close_fds = True) + + self.input = self.proc.stdin + self.output = self.proc.stdout + + +class DecoderSubProcessCore(Processor): + """Defines the main parts of the decoding tools : + paths, metadata parsing, data streaming thru system command""" + + def __init__(self): + self.command = 'ffmpeg -i "%s" -f wav - ' + + def process(self, source, options=None): + """Encode and stream audio data through a generator""" + + command = self.command % source + proc = SubProcessPipe(command) + return proc.output + + #while True: + #__chunk = proc.output.read(self.proc.buffer_size) + #status = proc.poll() + #if status != None and status != 0: + #raise ExportProcessError('Command failure:', command, proc) + #if len(__chunk) == 0: + #break + #yield __chunk + + + diff --git a/timeside/doc/img/timeside_schema.dia b/timeside/doc/img/timeside_schema.dia new file mode 100644 index 0000000..6431955 Binary files /dev/null and b/timeside/doc/img/timeside_schema.dia differ diff --git a/timeside/doc/img/timeside_schema.png b/timeside/doc/img/timeside_schema.png new file mode 100644 index 0000000..29f4bcb Binary files /dev/null and b/timeside/doc/img/timeside_schema.png differ diff --git a/timeside/encoder/__init__.py b/timeside/encoder/__init__.py new file mode 100644 index 0000000..7eadf71 --- /dev/null +++ b/timeside/encoder/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from timeside.encoder.core import * +#from timeside.encoder.ogg import * +from timeside.encoder.wav import * +#from timeside.encoder.mp3 import * +#from timeside.encoder.flac import * diff --git a/timeside/encoder/core.py b/timeside/encoder/core.py new file mode 100644 index 0000000..9786de1 --- /dev/null +++ b/timeside/encoder/core.py @@ -0,0 +1,64 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +from timeside.core import * +import subprocess + +class SubProcessPipe: + """Read media and stream data through a generator. + Taken from Telemeta (see http://telemeta.org)""" + + def __init__(self, command, stdin=None): + self.buffer_size = 0xFFFF + if not stdin: + stdin = subprocess.PIPE + + self.proc = subprocess.Popen(command.encode('utf-8'), + shell = True, + bufsize = self.buffer_size, + stdin = stdin, + stdout = subprocess.PIPE, + close_fds = True) + + self.input = self.proc.stdin + self.output = self.proc.stdout + +class EncoderSubProcessCore(Processor): + """Defines the main parts of the encoding tools : + paths, metadata parsing, data streaming thru system command""" + + def core_process(self, command, stdin): + """Encode and stream audio data through a generator""" + + proc = SubProcessPipe(command, stdin) + + while True: + __chunk = proc.output.read(proc.buffer_size) + #status = proc.poll() + #if status != None and status != 0: + #raise EncodeProcessError('Command failure:', command, proc) + if len(__chunk) == 0: + break + yield __chunk + + + diff --git a/timeside/encoder/flac.py b/timeside/encoder/flac.py new file mode 100644 index 0000000..0753441 --- /dev/null +++ b/timeside/encoder/flac.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from timeside.encoder.core import * +from timeside.api import IEncoder +from tempfile import NamedTemporaryFile + +class FlacEncoder(EncoderCore): + """Defines methods to encode to FLAC""" + + implements(IEncoder) + + def __init__(self): + self.quality_default = '-5' + self.dub2args_dict = {'creator': 'artist', + 'relation': 'album' + } + + @staticmethod + def id(): + return "flacenc" + + def format(self): + return 'FLAC' + + def file_extension(self): + return 'flac' + + def mime_type(self): + return 'audio/x-flac' + + def description(self): + return """ + Free Lossless Audio Codec (FLAC) is a file format for lossless audio + data compression. + """ + + def get_file_info(self): + try: + file1, file2 = os.popen4('metaflac --list "'+self.dest+'"') + info = [] + for line in file2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('EncoderError: metaflac is not installed or ' + \ + 'file does not exist.') + + def write_tags(self, file): + from mutagen.flac import FLAC + media = FLAC(file) + for tag in self.metadata: + name = tag[0] + value = clean_word(tag[1]) + if name in self.dub2args_dict.keys(): + name = self.dub2args_dict[name] + if name == 'comment': + media['DESCRIPTION'] = unicode(value) + else: + media[name] = unicode(value) + try: + media.save() + except: + raise IOError('EncoderError: cannot write tags.') + + + def get_args(self,options=None): + """Get process options and return arguments for the encoder""" + args = [] + if not options is None: + self.options = options + if not ('verbose' in self.options and self.options['verbose'] != '0'): + args.append('-s') + if 'flac_quality' in self.options: + args.append('-f ' + self.options['flac_quality']) + else: + args.append('-f ' + self.quality_default) + else: + args.append('-s -f ' + self.quality_default) + + #for tag in self.metadata.keys(): + #value = clean_word(self.metadata[tag]) + #args.append('-c %s="%s"' % (tag, value)) + #if tag in self.dub2args_dict.keys(): + #arg = self.dub2args_dict[tag] + #args.append('-c %s="%s"' % (arg, value)) + + return args + + def process(self, source, metadata, options=None): + buffer_size = 0xFFFF + self.metadata= metadata + self.options = options + args = self.get_args() + args = ' '.join(args) + ext = self.file_extension() + temp_file = NamedTemporaryFile() + command = 'flac %s - -o %s ' % (args, temp_file.name) + + stream = self.core_process(command, source) + + for __chunk in stream: + #temp_file.write(__chunk) + #temp_file.flush() + pass + + #self.write_tags(temp_file.name) + + while True: + __chunk = temp_file.read(buffer_size) + if len(__chunk) == 0: + break + yield __chunk + + temp_file.close() + + diff --git a/timeside/encoder/mp3.py b/timeside/encoder/mp3.py new file mode 100644 index 0000000..70f6dfb --- /dev/null +++ b/timeside/encoder/mp3.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Parisson SARL +# Copyright (c) 2006-2007 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from timeside.encoder.core import * +from timeside.api import IEncoder + + +class Mp3Encoder(EncoderCore): + """Defines methods to encode to MP3""" + + implements(IEncoder) + + def __init__(self): + self.bitrate_default = '192' + self.dub2id3_dict = {'title': 'TIT2', #title2 + 'creator': 'TCOM', #composer + 'creator': 'TPE1', #lead + 'identifier': 'UFID', #Unique ID... + 'identifier': 'TALB', #album + 'type': 'TCON', #genre + 'publisher': 'TPUB', #comment + #'date': 'TYER', #year + } + self.dub2args_dict = {'title': 'tt', #title2 + 'creator': 'ta', #composerS + 'relation': 'tl', #album + #'type': 'tg', #genre + 'publisher': 'tc', #comment + 'date': 'ty', #year + } + + @staticmethod + def id(): + return "mp3enc" + + def format(self): + return 'MP3' + + def file_extension(self): + return 'mp3' + + def mime_type(self): + return 'audio/mpeg' + + def description(self): + return """ + MPEG-1 Audio Layer 3, more commonly referred to as MP3, is a patented + digital audio encoding format using a form of lossy data compression. + """ + + def get_file_info(self): + try: + file_out1, file_out2 = os.popen4('mp3info "'+self.dest+'"') + info = [] + for line in file_out2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('EncoderError: file does not exist.') + + def write_tags(self): + """Write all ID3v2.4 tags by mapping dub2id3_dict dictionnary with the + respect of mutagen classes and methods""" + from mutagen import id3 + id3 = id3.ID3(self.dest) + for tag in self.metadata.keys(): + if tag in self.dub2id3_dict.keys(): + frame_text = self.dub2id3_dict[tag] + value = self.metadata[tag] + frame = mutagen.id3.Frames[frame_text](3,value) + try: + id3.add(frame) + except: + raise IOError('EncoderError: cannot tag "'+tag+'"') + try: + id3.save() + except: + raise IOError('EncoderError: cannot write tags') + + def get_args(self): + """Get process options and return arguments for the encoder""" + args = [] + if not self.options is None: + if not ( 'verbose' in self.options and self.options['verbose'] != '0' ): + args.append('-S') + if 'mp3_bitrate' in self.options: + args.append('-b ' + self.options['mp3_bitrate']) + else: + args.append('-b '+self.bitrate_default) + #Copyrights, etc.. + args.append('-c -o') + else: + args.append('-S -c --tt "unknown" -o') + + for tag in self.metadata: + name = tag[0] + value = clean_word(tag[1]) + if name in self.dub2args_dict.keys(): + arg = self.dub2args_dict[name] + args.append('--' + arg + ' "' + value + '"') + return args + + def process(self, source, metadata, options=None): + self.metadata = metadata + self.options = options + args = self.get_args() + args = ' '.join(args) + command = 'lame %s - -' % args + + stream = self.core_process(command, source) + for __chunk in stream: + yield __chunk + diff --git a/timeside/encoder/ogg.py b/timeside/encoder/ogg.py new file mode 100644 index 0000000..566c529 --- /dev/null +++ b/timeside/encoder/ogg.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from timeside.encoder.core import * +from timeside.api import IEncoder + +class OggVorbisEncoder(EncoderCore): + """Defines methods to encode to OGG Vorbis""" + + implements(IEncoder) + + def __init__(self): + self.bitrate_default = '192' + self.dub2args_dict = {'creator': 'artist', + 'relation': 'album' + } + + @staticmethod + def id(): + return "oggenc" + + def format(self): + return 'OggVorbis' + + def file_extension(self): + return 'ogg' + + def mime_type(self): + return 'application/ogg' + + def description(self): + return """ + Vorbis is a free software / open source project headed by the Xiph.Org + Foundation (formerly Xiphophorus company). The project produces an audio + format specification and software implementation (codec) for lossy audio + compression. Vorbis is most commonly used in conjunction with the Ogg + container format and it is therefore often referred to as Ogg Vorbis. + (source Wikipedia) + """ + + def get_file_info(self, file): + try: + file_out1, file_out2 = os.popen4('ogginfo "' + file + '"') + info = [] + for line in file_out2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except: + raise IOError('EncoderError: file does not exist.') + + def write_tags(self, file): + from mutagen.oggvorbis import OggVorbis + media = OggVorbis(file) + for tag in self.metadata.keys(): + media[tag] = str(self.metadata[tag]) + media.save() + + def get_args(self): + """Get process options and return arguments for the encoder""" + args = [] + if not self.options is None: + if not ('verbose' in self.options and self.options['verbose'] != '0'): + args.append('-Q ') + if 'ogg_bitrate' in self.options: + args.append('-b '+self.options['ogg_bitrate']) + elif 'ogg_quality' in self.options: + args.append('-q '+self.options['ogg_quality']) + else: + args.append('-b '+self.bitrate_default) + else: + args.append('-Q -b '+self.bitrate_default) + + for tag in self.metadata: + name = tag[0] + value = clean_word(tag[1]) + args.append('-c %s="%s"' % (name, value)) + if name in self.dub2args_dict.keys(): + arg = self.dub2args_dict[name] + args.append('-c %s="%s"' % (arg, value)) + return args + + def process(self, source, metadata, options=None): + self.metadata = metadata + self.options = options + args = self.get_args() + args = ' '.join(args) + command = 'oggenc %s -' % args + + stream = self.core_process(command, source) + for __chunk in stream: + yield __chunk + + diff --git a/timeside/encoder/wav.py b/timeside/encoder/wav.py new file mode 100644 index 0000000..d790aaa --- /dev/null +++ b/timeside/encoder/wav.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2009 Parisson +# Copyright (c) 2007 Olivier Guilyardi +# Copyright (c) 2007-2009 Guillaume Pellerin +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Paul Brossier + +from timeside.core import Processor, implements, interfacedoc +from timeside.api import IEncoder +from numpy import array, frombuffer, getbuffer, float32 + +import pygst +pygst.require('0.10') +import gst +import gobject +gobject.threads_init () + + +class WavEncoder(Processor): + """ gstreamer-based encoder """ + implements(IEncoder) + + def __init__(self, output): + self.file = None + if isinstance(output, basestring): + self.filename = output + else: + raise Exception("Streaming not supported") + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(WavEncoder, self).setup(channels, samplerate, nframes) + # TODO open file for writing + # the output data format we want + pipeline = gst.parse_launch(''' appsrc name=src + ! audioconvert + ! wavenc + ! filesink location=%s ''' % self.filename) + # store a pointer to appsink in our encoder object + self.src = pipeline.get_by_name('src') + srccaps = gst.Caps("""audio/x-raw-float, + endianness=(int)1234, + channels=(int)%s, + width=(int)32, + rate=(int)%d""" % (int(channels), int(samplerate))) + self.src.set_property("caps", srccaps) + + # start pipeline + pipeline.set_state(gst.STATE_PLAYING) + self.pipeline = pipeline + + @staticmethod + @interfacedoc + def id(): + return "gstreamerenc" + + @staticmethod + @interfacedoc + def description(): + return "Gstreamer based encoder" + + @staticmethod + @interfacedoc + def file_extension(): + return "wav" + + @staticmethod + @interfacedoc + def mime_type(): + return "audio/x-wav" + + @interfacedoc + def set_metadata(self, metadata): + #TODO + pass + + @interfacedoc + def process(self, frames, eod=False): + buf = self.numpy_array_to_gst_buffer(frames) + self.src.emit('push-buffer', buf) + if eod: self.src.emit('end-of-stream') + return frames, eod + + def numpy_array_to_gst_buffer(self, frames): + """ gstreamer buffer to numpy array conversion """ + buf = gst.Buffer(getbuffer(frames)) + return buf diff --git a/timeside/exceptions.py b/timeside/exceptions.py new file mode 100644 index 0000000..40b4dcd --- /dev/null +++ b/timeside/exceptions.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2009 Olivier Guilyardi +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +class Error(Exception): + """Exception base class for errors in TimeSide.""" + +class ApiError(Exception): + """Exception base class for errors in TimeSide.""" + +class SubProcessError(Error): + """Exception for reporting errors from a subprocess""" + + def __init__(self, message, command, subprocess): + self.message = message + self.command = str(command) + self.subprocess = subprocess + + def __str__(self): + if self.subprocess.stderr != None: + error = self.subprocess.stderr.read() + else: + error = '' + return "%s ; command: %s; error: %s" % (self.message, + self.command, + error) diff --git a/timeside/grapher/__init__.py b/timeside/grapher/__init__.py new file mode 100644 index 0000000..616ebf6 --- /dev/null +++ b/timeside/grapher/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from timeside.grapher.core import * +from timeside.grapher.waveform import * +from timeside.grapher.spectrogram import * diff --git a/timeside/grapher/core.py b/timeside/grapher/core.py new file mode 100644 index 0000000..36bc0bd --- /dev/null +++ b/timeside/grapher/core.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# wav2png.py -- converts wave files to wave file and spectrogram images +# Copyright (C) 2008 MUSIC TECHNOLOGY GROUP (MTG) +# UNIVERSITAT POMPEU FABRA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# Authors: +# Bram de Jong +# Guillaume Pellerin + + +import optparse, math, sys +import ImageFilter, ImageChops, Image, ImageDraw, ImageColor +import numpy +from timeside.core import FixedSizeInputAdapter + + +color_schemes = { + 'default': { + 'waveform': [(50,0,200), (0,220,80), (255,224,0), (255,0,0)], + 'spectrogram': [(0, 0, 0), (58/4,68/4,65/4), (80/2,100/2,153/2), (90,180,100), + (224,224,44), (255,60,30), (255,255,255)] + }, + 'iso': { + 'waveform': [(0,0,255), (0,255,255), (255,255,0), (255,0,0)], + 'spectrogram': [(0, 0, 0), (58/4,68/4,65/4), (80/2,100/2,153/2), (90,180,100), + (224,224,44), (255,60,30), (255,255,255)] + }, + 'purple': { + 'waveform': [(173,173,173), (147,149,196), (77,80,138), (108,66,0)], + 'spectrogram': [(0, 0, 0), (58/4,68/4,65/4), (80/2,100/2,153/2), (90,180,100), + (224,224,44), (255,60,30), (255,255,255)] + } +} + + +class Spectrum(object): + """ FFT based frequency analysis of audio frames.""" + + def __init__(self, fft_size, nframes, samplerate, lower, higher, window_function=numpy.ones): + self.fft_size = fft_size + self.window = window_function(self.fft_size) + self.spectrum_range = None + self.lower = lower + self.higher = higher + self.lower_log = math.log10(self.lower) + self.higher_log = math.log10(self.higher) + self.clip = lambda val, low, high: min(high, max(low, val)) + self.nframes = nframes + self.samplerate = samplerate + self.spectrum_adapter = FixedSizeInputAdapter(self.fft_size, 1, pad=True) + + def process(self, frames, eod, spec_range=120.0): + """ Returns a tuple containing the spectral centroid and the spectrum (dB scales) of the input audio frames. + An adapter is used to fix the buffer length and then provide fixed FFT window sizes.""" + + for buffer, end in self.spectrum_adapter.process(frames, eod): + samples = buffer[:,0].copy() + if end: + break + + #samples = numpy.concatenate((numpy.zeros(self.fft_size/2), samples), axis=1) + samples *= self.window + fft = numpy.fft.fft(samples) + spectrum = numpy.abs(fft[:fft.shape[0] / 2 + 1]) / float(self.fft_size) # normalized abs(FFT) between 0 and 1 + length = numpy.float64(spectrum.shape[0]) + + # scale the db spectrum from [- spec_range db ... 0 db] > [0..1] + db_spectrum = ((20*(numpy.log10(spectrum + 1e-30))).clip(-spec_range, 0.0) + spec_range)/spec_range + energy = spectrum.sum() + spectral_centroid = 0 + + if energy > 1e-20: + # calculate the spectral centroid + if self.spectrum_range == None: + self.spectrum_range = numpy.arange(length) + + spectral_centroid = (spectrum * self.spectrum_range).sum() / (energy * (length - 1)) * self.samplerate * 0.5 + # clip > log10 > scale between 0 and 1 + spectral_centroid = (math.log10(self.clip(spectral_centroid, self.lower, self.higher)) - self.lower_log) / (self.higher_log - self.lower_log) + + return (spectral_centroid, db_spectrum) + + +def interpolate_colors(colors, flat=False, num_colors=256): + """ Given a list of colors, create a larger list of colors interpolating + the first one. If flatten is True a list of numers will be returned. If + False, a list of (r,g,b) tuples. num_colors is the number of colors wanted + in the final list """ + + palette = [] + + for i in range(num_colors): + index = (i * (len(colors) - 1))/(num_colors - 1.0) + index_int = int(index) + alpha = index - float(index_int) + + if alpha > 0: + r = (1.0 - alpha) * colors[index_int][0] + alpha * colors[index_int + 1][0] + g = (1.0 - alpha) * colors[index_int][1] + alpha * colors[index_int + 1][1] + b = (1.0 - alpha) * colors[index_int][2] + alpha * colors[index_int + 1][2] + else: + r = (1.0 - alpha) * colors[index_int][0] + g = (1.0 - alpha) * colors[index_int][1] + b = (1.0 - alpha) * colors[index_int][2] + + if flat: + palette.extend((int(r), int(g), int(b))) + else: + palette.append((int(r), int(g), int(b))) + + return palette + + +class WaveformImage(object): + """ Builds a PIL image representing a waveform of the audio stream. + Adds pixels iteratively thanks to the adapter providing fixed size frame buffers. + Peaks are colored relative to the spectral centroids of each frame packet. """ + + def __init__(self, image_width, image_height, nframes, samplerate, fft_size, bg_color=(0,0,0), color_scheme='default', filename=None): + self.image_width = image_width + self.image_height = image_height + self.nframes = nframes + self.samplerate = samplerate + self.fft_size = fft_size + self.filename = filename + + self.bg_color = bg_color + self.color_scheme = color_scheme + + if isinstance(color_scheme, dict): + colors = color_scheme['waveform'] + else: + colors = color_schemes[self.color_scheme]['waveform'] + + self.color_lookup = interpolate_colors(colors) + + self.samples_per_pixel = self.nframes / float(self.image_width) + self.buffer_size = int(round(self.samples_per_pixel, 0)) + self.pixels_adapter = FixedSizeInputAdapter(self.buffer_size, 1, pad=False) + self.pixels_adapter_nframes = self.pixels_adapter.nframes(self.nframes) + + self.lower = 800 + self.higher = 12000 + self.spectrum = Spectrum(self.fft_size, self.nframes, self.samplerate, self.lower, self.higher, numpy.hanning) + + self.image = Image.new("RGB", (self.image_width, self.image_height), self.bg_color) + self.pixel = self.image.load() + self.draw = ImageDraw.Draw(self.image) + self.previous_x, self.previous_y = None, None + self.frame_cursor = 0 + self.pixel_cursor = 0 + + def peaks(self, samples): + """ Find the minimum and maximum peak of the samples. + Returns that pair in the order they were found. + So if min was found first, it returns (min, max) else the other way around. """ + + max_index = numpy.argmax(samples) + max_value = samples[max_index] + + min_index = numpy.argmin(samples) + min_value = samples[min_index] + + if min_index < max_index: + return (min_value, max_value) + else: + return (max_value, min_value) + + def color_from_value(self, value): + """ given a value between 0 and 1, return an (r,g,b) tuple """ + + return ImageColor.getrgb("hsl(%d,%d%%,%d%%)" % (int( (1.0 - value) * 360 ), 80, 50)) + + def draw_peaks(self, x, peaks, spectral_centroid): + """ draw 2 peaks at x using the spectral_centroid for color """ + + y1 = self.image_height * 0.5 - peaks[0] * (self.image_height - 4) * 0.5 + y2 = self.image_height * 0.5 - peaks[1] * (self.image_height - 4) * 0.5 + + line_color = self.color_lookup[int(spectral_centroid*255.0)] + + if self.previous_y != None: + self.draw.line([self.previous_x, self.previous_y, x, y1, x, y2], line_color) + else: + self.draw.line([x, y1, x, y2], line_color) + + self.previous_x, self.previous_y = x, y2 + + self.draw_anti_aliased_pixels(x, y1, y2, line_color) + + def draw_anti_aliased_pixels(self, x, y1, y2, color): + """ vertical anti-aliasing at y1 and y2 """ + + y_max = max(y1, y2) + y_max_int = int(y_max) + alpha = y_max - y_max_int + + if alpha > 0.0 and alpha < 1.0 and y_max_int + 1 < self.image_height: + current_pix = self.pixel[x, y_max_int + 1] + + r = int((1-alpha)*current_pix[0] + alpha*color[0]) + g = int((1-alpha)*current_pix[1] + alpha*color[1]) + b = int((1-alpha)*current_pix[2] + alpha*color[2]) + + self.pixel[x, y_max_int + 1] = (r,g,b) + + y_min = min(y1, y2) + y_min_int = int(y_min) + alpha = 1.0 - (y_min - y_min_int) + + if alpha > 0.0 and alpha < 1.0 and y_min_int - 1 >= 0: + current_pix = self.pixel[x, y_min_int - 1] + + r = int((1-alpha)*current_pix[0] + alpha*color[0]) + g = int((1-alpha)*current_pix[1] + alpha*color[1]) + b = int((1-alpha)*current_pix[2] + alpha*color[2]) + + self.pixel[x, y_min_int - 1] = (r,g,b) + + def process(self, frames, eod): + if len(frames) != 1: + buffer = frames[:,0].copy() + buffer.shape = (len(buffer),1) + (spectral_centroid, db_spectrum) = self.spectrum.process(buffer, True) + for samples, end in self.pixels_adapter.process(buffer, eod): + if self.pixel_cursor < self.image_width: + peaks = self.peaks(samples) + self.draw_peaks(self.pixel_cursor, peaks, spectral_centroid) + self.pixel_cursor += 1 + + def save(self): + """ Apply last 2D transforms and write all pixels to the file. """ + + # middle line (0 for none) + a = 1 + + for x in range(self.image_width): + self.pixel[x, self.image_height/2] = tuple(map(lambda p: p+a, self.pixel[x, self.image_height/2])) + self.image.save(self.filename) + + +class SpectrogramImage(object): + """ Builds a PIL image representing a spectrogram of the audio stream (level vs. frequency vs. time). + Adds pixels iteratively thanks to the adapter providing fixed size frame buffers.""" + + def __init__(self, image_width, image_height, nframes, samplerate, fft_size, bg_color=None, color_scheme='default', filename=None): + self.image_width = image_width + self.image_height = image_height + self.nframes = nframes + self.samplerate = samplerate + self.fft_size = fft_size + self.filename = filename + self.color_scheme = color_scheme + + self.image = Image.new("P", (self.image_height, self.image_width)) + colors = color_schemes[self.color_scheme]['spectrogram'] + self.image.putpalette(interpolate_colors(colors, True)) + + self.samples_per_pixel = self.nframes / float(self.image_width) + self.buffer_size = int(round(self.samples_per_pixel, 0)) + self.pixels_adapter = FixedSizeInputAdapter(self.buffer_size, 1, pad=False) + self.pixels_adapter_nframes = self.pixels_adapter.nframes(self.nframes) + + self.lower = 100 + self.higher = 22050 + self.spectrum = Spectrum(self.fft_size, self.nframes, self.samplerate, self.lower, self.higher, numpy.hanning) + + # generate the lookup which translates y-coordinate to fft-bin + self.y_to_bin = [] + f_min = float(self.lower) + f_max = float(self.higher) + y_min = math.log10(f_min) + y_max = math.log10(f_max) + for y in range(self.image_height): + freq = math.pow(10.0, y_min + y / (image_height - 1.0) *(y_max - y_min)) + bin = freq / 22050.0 * (self.fft_size/2 + 1) + + if bin < self.fft_size/2: + alpha = bin - int(bin) + + self.y_to_bin.append((int(bin), alpha * 255)) + + # this is a bit strange, but using image.load()[x,y] = ... is + # a lot slower than using image.putadata and then rotating the image + # so we store all the pixels in an array and then create the image when saving + self.pixels = [] + self.pixel_cursor = 0 + + def draw_spectrum(self, x, spectrum): + for (index, alpha) in self.y_to_bin: + self.pixels.append( int( ((255.0-alpha) * spectrum[index] + alpha * spectrum[index + 1] )) ) + + for y in range(len(self.y_to_bin), self.image_height): + self.pixels.append(0) + + def process(self, frames, eod): + if len(frames) != 1: + buffer = frames[:,0].copy() + buffer.shape = (len(buffer),1) + + # FIXME : breaks spectrum linearity + for samples, end in self.pixels_adapter.process(buffer, eod): + if self.pixel_cursor < self.image_width: + (spectral_centroid, db_spectrum) = self.spectrum.process(samples, True) + self.draw_spectrum(self.pixel_cursor, db_spectrum) + self.pixel_cursor += 1 + + def save(self): + """ Apply last 2D transforms and write all pixels to the file. """ + self.image.putdata(self.pixels) + self.image.transpose(Image.ROTATE_90).save(self.filename) + + +class Noise(object): + """A class that mimics audiolab.sndfile but generates noise instead of reading + a wave file. Additionally it can be told to have a "broken" header and thus crashing + in the middle of the file. Also useful for testing ultra-short files of 20 samples.""" + + def __init__(self, num_frames, has_broken_header=False): + self.seekpoint = 0 + self.num_frames = num_frames + self.has_broken_header = has_broken_header + + def seek(self, seekpoint): + self.seekpoint = seekpoint + + def get_nframes(self): + return self.num_frames + + def get_samplerate(self): + return 44100 + + def get_channels(self): + return 1 + + def read_frames(self, frames_to_read): + if self.has_broken_header and self.seekpoint + frames_to_read > self.num_frames / 2: + raise IOError() + + num_frames_left = self.num_frames - self.seekpoint + if num_frames_left < frames_to_read: + will_read = num_frames_left + else: + will_read = frames_to_read + self.seekpoint += will_read + return numpy.random.random(will_read)*2 - 1 + diff --git a/timeside/grapher/spectrogram.py b/timeside/grapher/spectrogram.py new file mode 100644 index 0000000..0962842 --- /dev/null +++ b/timeside/grapher/spectrogram.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2010 Guillaume Pellerin +# Copyright (c) 2010 Olivier Guilyardi + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + + +from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter +from timeside.api import IGrapher +from timeside.grapher import * + + +class Spectrogram(Processor): + implements(IGrapher) + + FFT_SIZE = 0x400 + + @interfacedoc + def __init__(self, width=None, height=None, output=None, bg_color=None, color_scheme=None): + if width: + self.width = width + else: + self.width = 1500 + if height: + self.height = height + else: + self.height = 200 + self.bg_color = bg_color + self.color_scheme = color_scheme + self.filename = output + self.graph = None + + @staticmethod + @interfacedoc + def id(): + return "spectrogram" + + @staticmethod + @interfacedoc + def name(): + return "Spectrogram" + + @interfacedoc + def set_colors(self, background, scheme): + self.bg_color = background + self.color_scheme = scheme + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(Spectrogram, self).setup(channels, samplerate, nframes) + if self.graph: + self.graph = None + self.graph = SpectrogramImage(self.width, self.height, self.nframes(), self.samplerate(), self.FFT_SIZE, + bg_color=self.bg_color, color_scheme=self.color_scheme, filename=self.filename) + + @interfacedoc + def process(self, frames, eod=False): + self.graph.process(frames, eod) + return frames, eod + + @interfacedoc + def render(self): + if self.filename: + self.graph.save() + return self.graph.image + diff --git a/timeside/grapher/spectrogram_audiolab.py b/timeside/grapher/spectrogram_audiolab.py new file mode 100644 index 0000000..be7143c --- /dev/null +++ b/timeside/grapher/spectrogram_audiolab.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +from timeside.core import * +from timeside.api import IGrapher +from tempfile import NamedTemporaryFile +from timeside.grapher.core import * + +class SpectrogramGrapherAudiolab(Processor): + """Spectrogram graph driver (python style thanks to wav2png.py and scikits.audiolab)""" + + implements(IGrapher) + + bg_color = None + color_scheme = None + + @staticmethod + def id(): + return "spectrogram" + + def name(self): + return "Spectrogram (audiolab)" + + def set_colors(self, background=None, scheme=None): + self.bg_color = background + self.color_scheme = scheme + + def render(self, media_item, width=None, height=None, options=None): + """Generator that streams the spectrogram as a PNG image with a python method""" + + wav_file = media_item + pngFile = NamedTemporaryFile(suffix='.png') + + if not width == None: + image_width = width + else: + image_width = 1500 + if not height == None: + image_height = height + else: + image_height = 200 + + fft_size = 2048 + args = (wav_file, pngFile.name, image_width, image_height, fft_size, + self.bg_color, self.color_scheme) + create_spectrogram_png(*args) + + buffer = pngFile.read(0xFFFF) + while buffer: + yield buffer + buffer = pngFile.read(0xFFFF) + + pngFile.close() diff --git a/timeside/grapher/waveform.py b/timeside/grapher/waveform.py new file mode 100644 index 0000000..982192c --- /dev/null +++ b/timeside/grapher/waveform.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2010 Guillaume Pellerin +# Copyright (c) 2010 Olivier Guilyardi + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + + +from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter +from timeside.api import IGrapher +from timeside.grapher import * + + +class Waveform(Processor): + implements(IGrapher) + + FFT_SIZE = 0x400 + + @interfacedoc + def __init__(self, width=None, height=None, output=None, bg_color=None, color_scheme=None): + if width: + self.width = width + else: + self.width = 1500 + if height: + self.height = height + else: + self.height = 200 + self.bg_color = bg_color + self.color_scheme = color_scheme + self.filename = output + self.graph = None + + @staticmethod + @interfacedoc + def id(): + return "waveform" + + @staticmethod + @interfacedoc + def name(): + return "Waveform" + + @interfacedoc + def set_colors(self, background, scheme): + self.bg_color = background + self.color_scheme = scheme + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(Waveform, self).setup(channels, samplerate, nframes) + if self.graph: + self.graph = None + self.graph = WaveformImage(self.width, self.height, self.nframes(), self.samplerate(), self.FFT_SIZE, + bg_color=self.bg_color, color_scheme=self.color_scheme, filename=self.filename) + + @interfacedoc + def process(self, frames, eod=False): + self.graph.process(frames, eod) + return frames, eod + + @interfacedoc + def render(self): + if self.filename: + self.graph.save() + return self.graph.image diff --git a/timeside/grapher/waveform_audiolab.py b/timeside/grapher/waveform_audiolab.py new file mode 100644 index 0000000..ac07330 --- /dev/null +++ b/timeside/grapher/waveform_audiolab.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2007-2009 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +from timeside.core import * +from timeside.api import IGrapher +from tempfile import NamedTemporaryFile +from timeside.grapher.core import * + +class WaveFormGrapherAudiolab(Processor): + """WaveForm graph driver (python style thanks to wav2png.py and scikits.audiolab)""" + + implements(IGrapher) + + bg_color = None + color_scheme = None + + @staticmethod + def id(): + return "waveform" + + def name(self): + return "Waveform (audiolab)" + + def set_colors(self, background=None, scheme=None): + self.bg_color = background + self.color_scheme = scheme + + def render(self, media_item, width=None, height=None, options=None): + """Generator that streams the waveform as a PNG image with a python method""" + + wav_file = media_item + pngFile = NamedTemporaryFile(suffix='.png') + + if not width == None: + image_width = width + else: + image_width = 1500 + if not height == None: + image_height = height + else: + image_height = 200 + + fft_size = 2048 + args = (wav_file, pngFile.name, image_width, image_height, fft_size, + self.bg_color, self.color_scheme) + create_wavform_png(*args) + + buffer = pngFile.read(0xFFFF) + while buffer: + yield buffer + buffer = pngFile.read(0xFFFF) + + pngFile.close() + diff --git a/timeside/metadata.py b/timeside/metadata.py new file mode 100644 index 0000000..86abf87 --- /dev/null +++ b/timeside/metadata.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2009 Parisson +# Copyright (c) 2007 Olivier Guilyardi +# Copyright (c) 2007-2009 Guillaume Pellerin +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +class Metadata(object): + pass + + diff --git a/timeside/tests/__init__.py b/timeside/tests/__init__.py new file mode 100644 index 0000000..2a6b9db --- /dev/null +++ b/timeside/tests/__init__.py @@ -0,0 +1,140 @@ + +import unittest +import sys +import time + +class TestCase(unittest.TestCase): + + def assertSameList(self, list1, list2): + "Test that two lists contain the same elements, in any order" + if len(list1) != len(list2): + self.fail("Lists length differ : %d != %d" % (len(list1), len(list2))) + + for item in list1: + if not item in list2: + self.fail("%s is not in list2" % str(item)) + + for item in list2: + if not item in list1: + self.fail("%s is not in list1" % str(item)) + +class _TextTestResult(unittest.TestResult): + """A test result class that can print formatted text results to a stream. + + Used by TextTestRunner. + """ + separator1 = '=' * 70 + separator2 = '-' * 70 + + def __init__(self, stream, descriptions, verbosity): + unittest.TestResult.__init__(self) + self.stream = stream + self.showAll = verbosity > 1 + self.dots = verbosity == 1 + self.descriptions = descriptions + self.currentTestCase = None + + def getDescription(self, test): + if self.descriptions: + return test.shortDescription() or str(test) + else: + return str(test) + + def startTest(self, test): + unittest.TestResult.startTest(self, test) + if self.showAll: + if self.currentTestCase != test.__class__: + self.currentTestCase = test.__class__ + self.stream.writeln() + self.stream.writeln("[%s]" % self.currentTestCase.__name__) + self.stream.write(" " + self.getDescription(test)) + self.stream.write(" ... ") + + def addSuccess(self, test): + unittest.TestResult.addSuccess(self, test) + if self.showAll: + self.stream.writeln("ok") + elif self.dots: + self.stream.write('.') + + def addError(self, test, err): + unittest.TestResult.addError(self, test, err) + if self.showAll: + self.stream.writeln("ERROR") + elif self.dots: + self.stream.write('E') + + def addFailure(self, test, err): + unittest.TestResult.addFailure(self, test, err) + if self.showAll: + self.stream.writeln("FAIL") + elif self.dots: + self.stream.write('F') + + def printErrors(self): + if self.dots or self.showAll: + self.stream.writeln() + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + def printErrorList(self, flavour, errors): + for test, err in errors: + self.stream.writeln(self.separator1) + self.stream.writeln("%s: %s" % (flavour,self.getDescription(test))) + self.stream.writeln(self.separator2) + self.stream.writeln("%s" % err) + + +class _WritelnDecorator: + """Used to decorate file-like objects with a handy 'writeln' method""" + def __init__(self,stream): + self.stream = stream + + def __getattr__(self, attr): + return getattr(self.stream,attr) + + def writeln(self, arg=None): + if arg: self.write(arg) + self.write('\n') # text-mode streams translate to \r\n if needed + +class TestRunner: + """A test runner class that displays results in textual form. + + It prints out the names of tests as they are run, errors as they + occur, and a summary of the results at the end of the test run. + """ + def __init__(self, stream=sys.stderr, descriptions=1, verbosity=2): + self.stream = _WritelnDecorator(stream) + self.descriptions = descriptions + self.verbosity = verbosity + + def _makeResult(self): + return _TextTestResult(self.stream, self.descriptions, self.verbosity) + + def run(self, test): + "Run the given test case or test suite." + result = self._makeResult() + startTime = time.time() + test(result) + stopTime = time.time() + timeTaken = stopTime - startTime + result.printErrors() + self.stream.writeln(result.separator2) + run = result.testsRun + self.stream.writeln("Ran %d test%s in %.3fs" % + (run, run != 1 and "s" or "", timeTaken)) + self.stream.writeln() + if not result.wasSuccessful(): + self.stream.write("FAILED (") + failed, errored = map(len, (result.failures, result.errors)) + if failed: + self.stream.write("failures=%d" % failed) + if errored: + if failed: self.stream.write(", ") + self.stream.write("errors=%d" % errored) + self.stream.writeln(")") + else: + self.stream.writeln("OK") + return result + + diff --git a/timeside/tests/alltests.py b/timeside/tests/alltests.py new file mode 100644 index 0000000..e368939 --- /dev/null +++ b/timeside/tests/alltests.py @@ -0,0 +1,5 @@ +from timeside.tests.testcomponent import * +from timeside.tests.testinputadapter import * +from timeside.tests import TestRunner + +unittest.main(testRunner=TestRunner()) diff --git a/timeside/tests/api/__init__.py b/timeside/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/timeside/tests/api/examples.py b/timeside/tests/api/examples.py new file mode 100644 index 0000000..418d2ca --- /dev/null +++ b/timeside/tests/api/examples.py @@ -0,0 +1,379 @@ +# -*- coding: utf-8 -*- +from timeside.core import Processor, implements, interfacedoc, FixedSizeInputAdapter +from timeside.api import * +from timeside.grapher import * +from timeside import Metadata +from scikits import audiolab +import numpy + +class FileDecoder(Processor): + """A simple audiolab-based example decoder""" + implements(IDecoder) + + @staticmethod + @interfacedoc + def id(): + return "test_audiolabdec" + + @interfacedoc + def __init__(self, filename): + self.filename = filename + # The file has to be opened here so that nframes(), samplerate(), + # etc.. work before setup() is called. + self.file = audiolab.Sndfile(self.filename, 'r') + self.position = 0 + + @interfacedoc + def setup(self): + super(FileDecoder, self).setup() + if self.position != 0: + self.file.seek(0); + self.position = 0 + + def release(self): + super(FileDecoder, self).release() + if self.file: + self.file.close() + self.file = None + + @interfacedoc + def channels(self): + return self.file.channels + + @interfacedoc + def samplerate(self): + return self.file.samplerate + + @interfacedoc + def nframes(self): + return self.file.nframes + + @interfacedoc + def format(self): + return self.file.file_format + + @interfacedoc + def encoding(self): + return self.file.encoding + @interfacedoc + def resolution(self): + resolution = None + encoding = self.file.encoding + + if encoding == "pcm8": + resolution = 8 + + elif encoding == "pcm16": + resolution = 16 + elif encoding == "pcm32": + resolution = 32 + + return resolution + + @interfacedoc + def metadata(self): + #TODO + return Metadata() + + @interfacedoc + def process(self, frames=None, eod=False): + if frames: + raise Exception("Decoder doesn't accept input frames") + + buffersize = 0x10000 + + # Need this because audiolab raises a bogus exception when asked + # to read passed the end of the file + toread = self.nframes() - self.position + if toread > buffersize: + toread = buffersize + + frames = self.file.read_frames(toread) + eod = (toread < buffersize) + self.position += toread + + # audiolab returns a 1D array for 1 channel, need to reshape to 2D: + if frames.ndim == 1: + frames = frames.reshape(len(frames), 1) + + return frames, eod + +class MaxLevel(Processor): + implements(IValueAnalyzer) + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(MaxLevel, self).setup(channels, samplerate, nframes) + self.max_value = 0 + + @staticmethod + @interfacedoc + def id(): + return "test_maxlevel" + + @staticmethod + @interfacedoc + def name(): + return "Max level test analyzer" + + @staticmethod + @interfacedoc + def unit(): + # power? amplitude? + return "" + + def process(self, frames, eod=False): + max = frames.max() + if max > self.max_value: + self.max_value = max + + return frames, eod + + def result(self): + return self.max_value + +class Gain(Processor): + implements(IEffect) + + @interfacedoc + def __init__(self, gain=1.0): + self.gain = gain + + @staticmethod + @interfacedoc + def id(): + return "test_gain" + + @staticmethod + @interfacedoc + def name(): + return "Gain test effect" + + def process(self, frames, eod=False): + return numpy.multiply(frames, self.gain), eod + +class WavEncoder(Processor): + implements(IEncoder) + + def __init__(self, output): + self.file = None + if isinstance(output, basestring): + self.filename = output + else: + raise Exception("Streaming not supported") + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(WavEncoder, self).setup(channels, samplerate, nframes) + if self.file: + self.file.close() + + format = audiolab.Format("wav", "pcm16") + self.file = audiolab.Sndfile(self.filename, 'w', format=format, channels=channels, + samplerate=samplerate) + + @staticmethod + @interfacedoc + def id(): + return "test_wavenc" + + @staticmethod + @interfacedoc + def description(): + return "Hackish wave encoder" + + @staticmethod + @interfacedoc + def file_extension(): + return "wav" + + @staticmethod + @interfacedoc + def mime_type(): + return "audio/x-wav" + + @interfacedoc + def set_metadata(self, metadata): + #TODO + pass + + @interfacedoc + def process(self, frames, eod=False): + self.file.write_frames(frames) + if eod: + self.file.close() + self.file = None + + return frames, eod + + +class Waveform(Processor): + implements(IGrapher) + + FFT_SIZE = 0x400 + + @interfacedoc + def __init__(self, width=None, height=None, output=None, bg_color=None, color_scheme=None): + if width: + self.width = width + else: + self.width = 1500 + if height: + self.height = height + else: + self.height = 200 + self.bg_color = bg_color + self.color_scheme = color_scheme + self.filename = output + self.graph = None + + @staticmethod + @interfacedoc + def id(): + return "test_waveform" + + @staticmethod + @interfacedoc + def name(): + return "Waveform test" + + @interfacedoc + def set_colors(self, background, scheme): + self.bg_color = background + self.color_scheme = scheme + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(Waveform, self).setup(channels, samplerate, nframes) + if self.graph: + self.graph = None + self.graph = WaveformImage(self.width, self.height, self.nframes(), self.samplerate(), self.FFT_SIZE, + bg_color=self.bg_color, color_scheme=self.color_scheme, filename=self.filename) + + @interfacedoc + def process(self, frames, eod=False): + self.graph.process(frames, eod) + return frames, eod + + @interfacedoc + def render(self): + if self.filename: + self.graph.save() + return self.graph.image + + +class Spectrogram(Processor): + implements(IGrapher) + + FFT_SIZE = 0x400 + + @interfacedoc + def __init__(self, width=None, height=None, output=None, bg_color=None, color_scheme=None): + if width: + self.width = width + else: + self.width = 1500 + if height: + self.height = height + else: + self.height = 200 + self.bg_color = bg_color + self.color_scheme = color_scheme + self.filename = output + self.graph = None + + @staticmethod + @interfacedoc + def id(): + return "test_spectrogram" + + @staticmethod + @interfacedoc + def name(): + return "Spectrogram test" + + @interfacedoc + def set_colors(self, background, scheme): + self.bg_color = background + self.color_scheme = scheme + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(Spectrogram, self).setup(channels, samplerate, nframes) + if self.graph: + self.graph = None + self.graph = SpectrogramImage(self.width, self.height, self.nframes(), self.samplerate(), self.FFT_SIZE, + bg_color=self.bg_color, color_scheme=self.color_scheme, filename=self.filename) + + @interfacedoc + def process(self, frames, eod=False): + self.graph.process(frames, eod) + return frames, eod + + @interfacedoc + def render(self): + if self.filename: + self.graph.save() + return self.graph.image + + +class Duration(Processor): + """A rather useless duration analyzer. Its only purpose is to test the + nframes characteristic.""" + implements(IValueAnalyzer) + + @interfacedoc + def setup(self, channels, samplerate, nframes): + if not nframes: + raise Exception("nframes argument required") + super(Duration, self).setup(channels, samplerate, nframes) + + @staticmethod + @interfacedoc + def id(): + return "test_duration" + + @staticmethod + @interfacedoc + def name(): + return "Duration analyzer" + + @staticmethod + @interfacedoc + def unit(): + return "seconds" + + def result(self): + return self.input_nframes / float(self.input_samplerate) + +class FixedInputProcessor(Processor): + """Processor which does absolutely nothing except illustrating the use + of the FixedInputSizeAdapter. It also tests things a bit.""" + + implements(IProcessor) + + BUFFER_SIZE = 1024 + + @staticmethod + @interfacedoc + def id(): + return "test_fixed" + + @interfacedoc + def setup(self, channels, samplerate, nframes): + super(FixedInputProcessor, self).setup(channels, samplerate, nframes) + self.adapter = FixedSizeInputAdapter(self.BUFFER_SIZE, channels, pad=True) + + @interfacedoc + def process(self, frames, eod=False): + for buffer, end in self.adapter.process(frames, eod): + # Test that the adapter is actually doing the job: + if len(buffer) != self.BUFFER_SIZE: + raise Exception("Bad buffer size from adapter") + + return frames, eod + + + + + diff --git a/timeside/tests/api/gstreamer.py b/timeside/tests/api/gstreamer.py new file mode 100644 index 0000000..897ac42 --- /dev/null +++ b/timeside/tests/api/gstreamer.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007-2009 Parisson +# Copyright (c) 2007 Olivier Guilyardi +# Copyright (c) 2007-2009 Guillaume Pellerin +# +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Paul Brossier + +from timeside.core import Processor, implements, interfacedoc +from timeside.api import IDecoder, IEncoder +from numpy import array, frombuffer, getbuffer, float32 + +import pygst +pygst.require('0.10') +import gst +import gobject +gobject.threads_init () + +class FileDecoder(Processor): + """ gstreamer-based decoder """ + implements(IDecoder) + + # duration ms, before discovery process times out + MAX_DISCOVERY_TIME = 3000 + + audioformat = None + audiochannels = None + audiorate = None + audionframes = None + mimetype = '' + + # IProcessor methods + + @staticmethod + @interfacedoc + def id(): + return "test_gstreamerdec" + + def setup(self, channels = None, samplerate = None, nframes = None): + # the output data format we want + caps = "audio/x-raw-float, width=32" + pipeline = gst.parse_launch('''uridecodebin uri=%s + ! audioconvert + ! %s + ! appsink name=sink sync=False ''' % (self.uri, caps)) + # store a pointer to appsink in our decoder object + self.sink = pipeline.get_by_name('sink') + # adjust length of emitted buffers + # self.sink.set_property('blocksize', 0x10000) + # start pipeline + pipeline.set_state(gst.STATE_PLAYING) + + @interfacedoc + def channels(self): + return self.audiochannels + + @interfacedoc + def samplerate(self): + return self.audiorate + + @interfacedoc + def nframes(self): + return self.audionframes + + @interfacedoc + def process(self, frames = None, eod = False): + try: + buf = self.sink.emit('pull-buffer') + except SystemError, e: + # should never happen + print 'SystemError', e + return array([0.]), True + if buf == None: + return array([0.]), True + return self.gst_buffer_to_numpy_array(buf), False + + @interfacedoc + def release(self): + # nothing to do for now + pass + + ## IDecoder methods + + @interfacedoc + def __init__(self, uri): + + # is this a file? + import os.path + if os.path.exists(uri): + # get the absolute path + uri = os.path.abspath(uri) + # first run the file/uri through the discover pipeline + self.discover(uri) + # and make a uri of it + from urllib import quote + self.uri = 'file://'+quote(uri) + + @interfacedoc + def format(self): + # TODO check + return self.mimetype + + @interfacedoc + def encoding(self): + # TODO check + return self.mimetype.split('/')[-1] + + @interfacedoc + def resolution(self): + # TODO check: width or depth? + return self.audiowidth + + @interfacedoc + def metadata(self): + # TODO check + return self.tags + + ## gst.extend discoverer + + def discover(self, path): + """ gstreamer based helper function to get file attributes """ + from gst.extend import discoverer + d = discoverer.Discoverer(path, timeout = self.MAX_DISCOVERY_TIME) + d.connect('discovered', self.discovered) + self.mainloop = gobject.MainLoop() + d.discover() + self.mainloop.run() + + def discovered(self, d, is_media): + """ gstreamer based helper executed upon discover() completion """ + if is_media and d.is_audio: + # copy the discoverer attributes to self + self.audiorate = d.audiorate + self.mimetype= d.mimetype + self.audiochannels = d.audiochannels + self.audiowidth = d.audiowidth + # conversion from time in nanoseconds to frames + from math import ceil + duration = d.audiorate * d.audiolength * 1.e-9 + self.audionframes = int (ceil ( duration ) ) + self.tags = d.tags + elif not d.is_audio: + print "error, no audio found!" + else: + print "fail", path + self.mainloop.quit() + + def gst_buffer_to_numpy_array(self, buf): + """ gstreamer buffer to numpy array conversion """ + chan = self.audiochannels + samples = frombuffer(buf.data, dtype=float32) + samples.resize([len(samples)/chan, chan]) + return samples + +class WavEncoder(Processor): + """ gstreamer-based encoder """ + implements(IEncoder) + + def __init__(self, output): + self.file = None + if isinstance(output, basestring): + self.filename = output + else: + raise Exception("Streaming not supported") + + @interfacedoc + def setup(self, channels=None, samplerate=None, nframes=None): + super(WavEncoder, self).setup(channels, samplerate, nframes) + # TODO open file for writing + # the output data format we want + pipeline = gst.parse_launch(''' appsrc name=src + ! audioconvert + ! wavenc + ! filesink location=%s ''' % self.filename) + # store a pointer to appsink in our encoder object + self.src = pipeline.get_by_name('src') + srccaps = gst.Caps("""audio/x-raw-float, + endianness=(int)1234, + channels=(int)%s, + width=(int)32, + rate=(int)%d""" % (int(channels), int(samplerate))) + self.src.set_property("caps", srccaps) + + # start pipeline + pipeline.set_state(gst.STATE_PLAYING) + self.pipeline = pipeline + + @staticmethod + @interfacedoc + def id(): + return "test_gstreamerenc" + + @staticmethod + @interfacedoc + def description(): + return "Gstreamer based encoder" + + @staticmethod + @interfacedoc + def file_extension(): + return "wav" + + @staticmethod + @interfacedoc + def mime_type(): + return "audio/x-wav" + + @interfacedoc + def set_metadata(self, metadata): + #TODO + pass + + @interfacedoc + def process(self, frames, eod=False): + buf = self.numpy_array_to_gst_buffer(frames) + self.src.emit('push-buffer', buf) + if eod: self.src.emit('end-of-stream') + return frames, eod + + def numpy_array_to_gst_buffer(self, frames): + """ gstreamer buffer to numpy array conversion """ + buf = gst.Buffer(getbuffer(frames)) + return buf diff --git a/timeside/tests/api/test_lolevel.py b/timeside/tests/api/test_lolevel.py new file mode 100644 index 0000000..786b141 --- /dev/null +++ b/timeside/tests/api/test_lolevel.py @@ -0,0 +1,60 @@ +from timeside.tests.api import examples + +use_gst = 0 +if use_gst: + from timeside.tests.api.gstreamer import FileDecoder, WavEncoder +else: + from timeside.tests.api.examples import FileDecoder, WavEncoder + +import sys +if len(sys.argv) > 1: + source = sys.argv[1] +else: + import os.path + source= os.path.join (os.path.dirname(__file__), "../samples/guitar.wav") + +Decoder = FileDecoder +print "Creating decoder with id=%s for: %s" % (Decoder.id(), source) +decoder = Decoder(source) +analyzer = examples.MaxLevel() +decoder.setup() +nchannels = decoder.channels() +samplerate = decoder.samplerate() +nframes = decoder.nframes() +analyzer.setup(nchannels, samplerate) + +print "Stats: duration=%f, nframes=%d, nchannels=%d, samplerate=%d, resolution=%d" % ( + nframes / float(samplerate), nframes, nchannels, samplerate, decoder.resolution()) + +while True: + frames, eod = decoder.process() + analyzer.process(frames, eod) + if eod: + break + +max_level = analyzer.result() +print "Max level: %f" % max_level + +destination = "normalized.wav" +Encoder = WavEncoder +print "Creating encoder with id=%s for: %s" % (Encoder.id(), destination) +encoder = Encoder(destination) + +gain = 1 +if max_level > 0: + gain = 0.9 / max_level + +effect = examples.Gain(gain) + +decoder.setup() +effect.setup(decoder.channels(), decoder.samplerate()) +encoder.setup(effect.channels(), effect.samplerate()) + +print "Applying effect id=%s with gain=%f" % (effect.id(), gain) + +while True: + frames, eod = decoder.process() + encoder.process(*effect.process(frames, eod)) + if eod: + break + diff --git a/timeside/tests/api/test_pipe.py b/timeside/tests/api/test_pipe.py new file mode 100644 index 0000000..eaa0850 --- /dev/null +++ b/timeside/tests/api/test_pipe.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from timeside.tests.api.examples import Gain +from timeside.core import * +from timeside.decoder import * +from timeside.analyzer import * +from timeside.encoder import * +from timeside.api import * +from sys import stdout + +import os.path +source = os.path.join(os.path.dirname(__file__), "../samples/guitar.wav") + +print "Normalizing %s" % source +decoder = FileDecoder(source) +maxlevel = MaxLevel() +duration = Duration() + +(decoder | maxlevel | duration).run() + +gain = 1 +if maxlevel.result() > 0: + gain = 0.9 / maxlevel.result() + +print "input maxlevel: %f" % maxlevel.result() +print "gain: %f" % gain +print "duration: %f %s" % (duration.result(), duration.unit()) + +gain = Gain(gain) +encoder = WavEncoder("normalized.wav") + +subpipe = gain | maxlevel + +(decoder | subpipe | encoder).run() + +print "output maxlevel: %f" % maxlevel.result() + + diff --git a/timeside/tests/api/test_pipe_spectrogram.py b/timeside/tests/api/test_pipe_spectrogram.py new file mode 100644 index 0000000..5ba90eb --- /dev/null +++ b/timeside/tests/api/test_pipe_spectrogram.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +import os +from timeside.core import * +from timeside.api import * +from timeside.decoder import * +from timeside.grapher import * + +image_file = '../results/img/spectrogram.png' +source = os.path.join(os.path.dirname(__file__), "../samples/sweep.wav") + +decoder = FileDecoder(source) +spectrogram = Spectrogram(width=1024, height=256, output=image_file, bg_color=(0,0,0), color_scheme='default') + +(decoder | spectrogram).run() + +print 'frames per pixel = ', spectrogram.graph.samples_per_pixel +print "render spectrogram to: %s" % image_file +spectrogram.render() + + diff --git a/timeside/tests/api/test_pipe_waveform.py b/timeside/tests/api/test_pipe_waveform.py new file mode 100644 index 0000000..d21ecdc --- /dev/null +++ b/timeside/tests/api/test_pipe_waveform.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +import os +from timeside.core import * +from timeside.api import * +from timeside.decoder import * +from timeside.grapher import * + +sample_dir = '../samples' +img_dir = '../results/img' +if not os.path.exists(img_dir): + os.mkdir(img_dir) + +test_dict = {'sweep.wav': 'waveform_wav.png', + 'sweep.flac': 'waveform_flac.png', + 'sweep.ogg': 'waveform_ogg.png', + 'sweep.mp3': 'waveform_mp3.png', + } + +for source, image in test_dict.iteritems(): + audio = os.path.join(os.path.dirname(__file__), sample_dir + os.sep + source) + image = img_dir + os.sep + image + print 'Test : decoder(%s) | waveform (%s)' % (source, image) + decoder = FileDecoder(audio) + waveform = Waveform(width=1024, height=256, output=image, bg_color=(0,0,0), color_scheme='default') + (decoder | waveform).run() + print 'frames per pixel = ', waveform.graph.samples_per_pixel + print "render waveform to: %s" % image + waveform.render() + + diff --git a/timeside/tests/listprocessors.py b/timeside/tests/listprocessors.py new file mode 100644 index 0000000..840c087 --- /dev/null +++ b/timeside/tests/listprocessors.py @@ -0,0 +1,12 @@ +import timeside + +def list_processors(interface, prefix=""): + print prefix + interface.__name__ + subinterfaces = interface.__subclasses__() + for i in subinterfaces: + list_processors(i, prefix + " ") + processors = timeside.processors(interface, False) + for p in processors: + print prefix + " %s [%s]" % (p.__name__, p.id()) + +list_processors(timeside.api.IProcessor) diff --git a/timeside/tests/samples/guitar.wav b/timeside/tests/samples/guitar.wav new file mode 100644 index 0000000..b5a9e80 Binary files /dev/null and b/timeside/tests/samples/guitar.wav differ diff --git a/timeside/tests/samples/sweep.flac b/timeside/tests/samples/sweep.flac new file mode 100644 index 0000000..decee06 Binary files /dev/null and b/timeside/tests/samples/sweep.flac differ diff --git a/timeside/tests/samples/sweep.mp3 b/timeside/tests/samples/sweep.mp3 new file mode 100644 index 0000000..dc75fe0 Binary files /dev/null and b/timeside/tests/samples/sweep.mp3 differ diff --git a/timeside/tests/samples/sweep.ogg b/timeside/tests/samples/sweep.ogg new file mode 100644 index 0000000..e30bf99 Binary files /dev/null and b/timeside/tests/samples/sweep.ogg differ diff --git a/timeside/tests/samples/sweep.wav b/timeside/tests/samples/sweep.wav new file mode 100644 index 0000000..fc28a32 Binary files /dev/null and b/timeside/tests/samples/sweep.wav differ diff --git a/timeside/tests/samples/sweep_source.wav b/timeside/tests/samples/sweep_source.wav new file mode 100644 index 0000000..5dcf9ba Binary files /dev/null and b/timeside/tests/samples/sweep_source.wav differ diff --git a/timeside/tests/testcomponent.py b/timeside/tests/testcomponent.py new file mode 100644 index 0000000..6638c30 --- /dev/null +++ b/timeside/tests/testcomponent.py @@ -0,0 +1,166 @@ + +from timeside.component import * +from timeside.tests import TestCase, TestRunner +import unittest + +__all__ = ['TestComponentArchitecture'] + +class TestComponentArchitecture(TestCase): + "Test the component and interface system" + + def testOneInterface(self): + "Test a component implementing one interface" + self.assertSameList(implementations(I1), [C1]) + + def testTwoInterfaces(self): + "Test a component implementing two interfaces" + self.assertSameList(implementations(I2), [C2]) + self.assertSameList(implementations(I3), [C2]) + + def testTwoImplementations(self): + "Test an interface implemented by two components" + self.assertSameList(implementations(I4), [C3, C4]) + + def testInterfaceInheritance(self): + "Test whether a component implements an interface's parent" + self.assertSameList(implementations(I5), [C5]) + + def testImplementationInheritance(self): + "Test that a component doesn't implement the interface implemented by its parent" + self.assertSameList(implementations(I7), [C6]) + + def testImplementationRedundancy(self): + "Test implementation redundancy across inheritance" + self.assertSameList(implementations(I8), [C8, C9]) + + def testAbstractImplementation(self): + "Test abstract implementation" + self.assertSameList(implementations(I11), []) + self.assertSameList(implementations(I11, abstract=True), [C11]) + + def testInterfaceDoc(self): + "Test @interfacedoc decorator" + self.assertEquals(C10.test.__doc__, "testdoc") + + def testInterfaceDocStatic(self): + "Test @interfacedoc decorator on static method" + self.assertEquals(C10.teststatic.__doc__, "teststaticdoc") + + def testIntefaceDocReversed(self): + "Test @interfacedoc on static method (decorators reversed)" + + try: + + class BogusDoc1(Component): + implements(I10) + + @interfacedoc + @staticmethod + def teststatic(self): + pass + + self.fail("No error raised with reversed decorators") + + except ComponentError: + pass + + def testInterfaceDocBadMethod(self): + "Test @interfacedoc with unexistant method in interface" + + try: + class BogusDoc2(Component): + implements(I10) + + @interfacedoc + def nosuchmethod(self): + pass + + self.fail("No error raised when decorating an unexistant method") + + except ComponentError: + pass + +class I1(Interface): + pass + +class I2(Interface): + pass + +class I3(Interface): + pass + +class I4(Interface): + pass + +class I5(Interface): + pass + +class I6(I5): + pass + +class I7(Interface): + pass + +class I8(Interface): + pass + +class I9(I8): + pass + +class I10(Interface): + def test(self): + """testdoc""" + + @staticmethod + def teststatic(self): + """teststaticdoc""" + +class I11(Interface): + pass + +class C1(Component): + implements(I1) + +class C2(Component): + implements(I2, I3) + +class C3(Component): + implements(I4) + +class C4(Component): + implements(I4) + +class C5(Component): + implements(I6) + +class C6(Component): + implements(I7) + +class C7(C6): + pass + +class C8(Component): + implements(I8) + +class C9(Component): + implements(I8, I9) + +class C10(Component): + implements(I10) + + @interfacedoc + def test(self): + pass + + @staticmethod + @interfacedoc + def teststatic(self): + pass + +class C11(Component): + abstract() + implements(I11) + +if __name__ == '__main__': + unittest.main(testRunner=TestRunner()) + diff --git a/timeside/tests/testinputadapter.py b/timeside/tests/testinputadapter.py new file mode 100644 index 0000000..dca0f63 --- /dev/null +++ b/timeside/tests/testinputadapter.py @@ -0,0 +1,71 @@ +from timeside.core import FixedSizeInputAdapter +from timeside.tests import TestCase, TestRunner +import numpy +import unittest + +class TestFixedSizeInputAdapter(TestCase): + "Test the fixed-sized input adapter" + + def assertIOEquals(self, adapter, input, input_eod, output, output_eod=None): + output = output[:] + output.reverse() + _eod = None + for buffer, _eod in adapter.process(input, input_eod): + a = output.pop() + if not numpy.array_equiv(buffer, a): + self.fail("\n-- Actual --\n%s\n -- Expected -- \n%s\n" % (str(buffer), str(a))) + + if _eod != output_eod: + self.fail("eod do not match: %s != %s", (str(_eod), str(output_eod))) + + if output: + self.fail("trailing expected data: %s" % output) + + def setUp(self): + self.data = numpy.arange(44).reshape(2,22).transpose() + + def testTwoChannels(self): + "Test simple stream with two channels" + adapter = FixedSizeInputAdapter(4, 2) + + self.assertEquals(len(self.data), adapter.nframes(len(self.data))) + + self.assertIOEquals(adapter, self.data[0:1], False, []) + self.assertIOEquals(adapter, self.data[1:5], False, [self.data[0:4]], False) + self.assertIOEquals(adapter, self.data[5:12], False, [self.data[4:8], self.data[8:12]], False) + self.assertIOEquals(adapter, self.data[12:13], False, []) + self.assertIOEquals(adapter, self.data[13:14], False, []) + self.assertIOEquals(adapter, self.data[14:18], False, [self.data[12:16]], False) + self.assertIOEquals(adapter, self.data[18:20], False, [self.data[16:20]], False) + self.assertIOEquals(adapter, self.data[20:21], False, []) + self.assertIOEquals(adapter, self.data[21:22], True, [self.data[20:22]], True) + + def testPadding(self): + "Test automatic padding support" + adapter = FixedSizeInputAdapter(4, 2, pad=True) + + self.assertEquals(len(self.data) + 2, adapter.nframes(len(self.data))) + + self.assertIOEquals(adapter, self.data[0:21], False, + [self.data[0:4], self.data[4:8], self.data[8:12], self.data[12:16], self.data[16:20]], + False) + + self.assertIOEquals(adapter, self.data[21:22], True, [[ + [20, 42], + [21, 43], + [0, 0], + [0, 0] + ]], True) + + def testSizeMultiple(self): + "Test a stream which contain a multiple number of buffers" + adapter = FixedSizeInputAdapter(4, 2) + + self.assertIOEquals(adapter, self.data[0:20], True, + [self.data[0:4], self.data[4:8], self.data[8:12], self.data[12:16], self.data[16:20]], + True) + + +if __name__ == '__main__': + unittest.main(testRunner=TestRunner()) + diff --git a/timeside/tools/grapher_scheme.py b/timeside/tools/grapher_scheme.py new file mode 100644 index 0000000..4549767 --- /dev/null +++ b/timeside/tools/grapher_scheme.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +class GrapherScheme: + + def __init__(self): + + self.color_scheme = { + 'waveform': [ + (50,0,200), (0,220,80), (255,224,0), (255,0,0) + ], + 'spectrogram': [ + (0, 0, 0), (58/4,68/4,65/4), (80/2,100/2,153/2), (90,180,100), (224,224,44), (255,60,30), (255,255,255) + ]} + + self.width = 2048 + self.height = 128 + self.bg_color = (255,255,255) + self.force = True + diff --git a/timeside/tools/waveform_batch.py b/timeside/tools/waveform_batch.py new file mode 100644 index 0000000..bba5036 --- /dev/null +++ b/timeside/tools/waveform_batch.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2009-2010 Guillaume Pellerin + +# This file is part of TimeSide. + +# TimeSide is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. + +# TimeSide is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with TimeSide. If not, see . + +# Author: Guillaume Pellerin + +version = '0.1-beta' + +import os +import sys +from timeside.core import * +from timeside.decoder import * +from timeside.grapher import * +from grapher_scheme import * + + +class Media2Waveform(object): + + def __init__(self, media_dir, img_dir): + self.root_dir = media_dir + self.img_dir = img_dir + self.scheme = GrapherScheme() + self.width = self.scheme.width + self.height = self.scheme.height + self.bg_color = self.scheme.bg_color + self.color_scheme = self.scheme.color_scheme + self.force = self.scheme.force + + self.media_list = self.get_media_list() + if not os.path.exists(self.img_dir): + os.mkdir(self.img_dir) + self.path_dict = self.get_path_dict() + + def get_media_list(self): + media_list = [] + for root, dirs, files in os.walk(self.root_dir): + if root: + for file in files: + ext = file.split('.')[-1] + if ext == 'mp3' or ext == 'MP3': + media_list.append(root+os.sep+file) + return media_list + + def get_path_dict(self): + path_dict = {} + for media in self.media_list: + name = os.path.splitext(media) + name = name[0].split(os.sep)[-1] + path_dict[media] = self.img_dir + os.sep + name + '.png' + return path_dict + + def process(self): + for source, image in self.path_dict.iteritems(): + if not os.path.exists(image) or self.force: + print 'Rendering ', source, ' to ', image, '...' + audio = os.path.join(os.path.dirname(__file__), source) + decoder = FileDecoder(audio) + waveform = Waveform(width=self.width, height=self.height, output=image, + bg_color=self.bg_color, color_scheme=self.color_scheme) + (decoder | waveform).run() + print 'frames per pixel = ', waveform.graph.samples_per_pixel + waveform.render() + + +if __name__ == '__main__': + if len(sys.argv) <= 2: + print """ + Usage : python waveform_batch.py /path/to/media_dir /path/to/img_dir + + Dependencies : timeside, python, python-numpy, python-gst0.10, gstreamer0.10-plugins-base + See http://code.google.com/p/timeside/ for more information. + """ + else: + media_dir = sys.argv[-2] + img_dir = sys.argv[-1] + m = Media2Waveform(media_dir, img_dir) + m.process() diff --git a/timeside/ui/README b/timeside/ui/README new file mode 100644 index 0000000..0f9d21d --- /dev/null +++ b/timeside/ui/README @@ -0,0 +1,20 @@ +=============================== +TimeSide - Web Audio Components +=============================== + +TimeSide UI Dependencies +======================== + +* SoundManager 2 >= 2.91 : http://www.schillmania.com/projects/soundmanager2 +* jQuery => 1.2.6 : http://www.jquery.com +* jsGraphics => 3.03 http://www.walterzorn.com/jsgraphics/jsgraphics_e.htm + +Licensing +========= + +Copyright (c) 2008-2009 Samalyse +Author: Olivier Guilyardi + +TimeSide is released under the terms of the GNU General Public License +version 2. Please see the LICENSE file for details. + diff --git a/timeside/ui/css/timeside.css b/timeside/ui/css/timeside.css new file mode 100755 index 0000000..cadc79a --- /dev/null +++ b/timeside/ui/css/timeside.css @@ -0,0 +1,29 @@ +/* FIXME: These CSS styles are essential and non intrusive. They should be + * dynamically assigned in javascript, and this file removed. */ + +.ts-player .ts-ruler .ts-section .ts-canvas { + position: relative; +} + +.ts-player .ts-wave { + position: relative; + clear: both; + overflow: hidden; +} + +.ts-player .ts-wave .ts-image-container { + position: relative; +} + +.ts-player .ts-wave .ts-image { + position: absolute; + left: 0; + clear: both; +} + +.ts-player .ts-wave .ts-image-canvas { + position: absolute; + z-index: 100; + overflow: hidden; +} + diff --git a/timeside/ui/demo/index.html b/timeside/ui/demo/index.html new file mode 100755 index 0000000..f3977e2 --- /dev/null +++ b/timeside/ui/demo/index.html @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +

TimeSide Player

+ +
+
+
+
+ +

+ + + + +
+ Skin: + +

+ + diff --git a/timeside/ui/demo/waveform.png b/timeside/ui/demo/waveform.png new file mode 100755 index 0000000..75e3f9d Binary files /dev/null and b/timeside/ui/demo/waveform.png differ diff --git a/timeside/ui/lib/firebug-lite-compressed.js b/timeside/ui/lib/firebug-lite-compressed.js new file mode 100644 index 0000000..df2601c --- /dev/null +++ b/timeside/ui/lib/firebug-lite-compressed.js @@ -0,0 +1,2 @@ +(function(_scope){_scope.pi=Object(3.14159265358979323846);var pi=_scope.pi;pi.version=1.0;pi.env={ie:/MSIE/i.test(navigator.userAgent),ie6:/MSIE 6/i.test(navigator.userAgent),ie7:/MSIE 7/i.test(navigator.userAgent),ie8:/MSIE 8/i.test(navigator.userAgent),firefox:/Firefox/i.test(navigator.userAgent),opera:/Opera/i.test(navigator.userAgent),webkit:/Webkit/i.test(navigator.userAgent)};pi.util={IsArray:function(_object){return _object&&_object!=window&&(_object instanceof Array||(typeof _object.length=="number"&&typeof _object.item=="function"))},IsHash:function(_object){return _object&&typeof _object=="object"&&(_object==window||_object instanceof Object)&&!_object.nodeName&&!pi.util.IsArray(_object)},DOMContentLoaded:[],AddEvent:function(_element,_eventName,_fn,_useCapture){_element[pi.env.ie.toggle("attachEvent","addEventListener")](pi.env.ie.toggle("on","")+_eventName,_fn,_useCapture||false);return pi.util.AddEvent.curry(this,_element)},RemoveEvent:function(_element,_eventName,_fn,_useCapture){return _element[pi.env.ie.toggle("detachEvent","removeEventListener")](pi.env.ie.toggle("on","")+_eventName,_fn,_useCapture||false)},GetWindowSize:function(){return{height:pi.env.ie?Math.max(document.documentElement.clientHeight,document.body.clientHeight):window.innerHeight,width:pi.env.ie?Math.max(document.documentElement.clientWidth,document.body.clientWidth):window.innerWidth}},Include:function(_url,_callback){var script=new pi.element("script").attribute.set("src",_url),callback=_callback||new Function,done=false,head=pi.get.byTag("head")[0];script.environment.getElement().onload=script.environment.getElement().onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){callback.call(this);done=true;head.removeChild(script.environment.getElement())}};script.insert(head)},Element:{addClass:function(_element,_class){if(!pi.util.Element.hasClass(_element,_class))pi.util.Element.setClass(_element,pi.util.Element.getClass(_element)+" "+_class)},getClass:function(_element){return _element.getAttribute(pi.env.ie.toggle("className","class"))||""},hasClass:function(_element,_class){return pi.util.Element.getClass(_element).split(" ").indexOf(_class)>-1},removeClass:function(_element,_class){if(pi.util.Element.hasClass(_element,_class))pi.util.Element.setClass(_element,pi.util.Element.getClass(_element,_class).split(" ").removeValue(_class).join(" "))},setClass:function(_element,_value){_element.setAttribute(pi.env.ie.toggle("className","class"),_value)},toggleClass:function(){if(pi.util.Element.hasClass.apply(this,arguments))pi.util.Element.removeClass.apply(this,arguments);else pi.util.Element.addClass.apply(this,arguments)},getOpacity:function(_styleObject){var styleObject=_styleObject;if(!pi.env.ie)return styleObject["opacity"];var alpha=styleObject["filter"].match(/opacity\=(\d+)/i);return alpha?alpha[1]/100:1},setOpacity:function(_element,_value){if(!pi.env.ie)return pi.util.Element.addStyle(_element,{"opacity":_value});_value*=100;pi.util.Element.addStyle(_element,{"filter":"alpha(opacity="+_value+")"});return this._parent_},getPosition:function(_element){var parent=_element,offsetLeft=0,offsetTop=0,view=pi.util.Element.getView(_element);while(parent&&parent!=document.body&&parent!=document.firstChild){offsetLeft+=parseInt(parent.offsetLeft);offsetTop+=parseInt(parent.offsetTop);parent=parent.offsetParent};return{"bottom":view["bottom"],"left":view["left"],"marginTop":view["marginTop"],"marginLeft":view["marginLeft"],"offsetLeft":offsetLeft,"offsetTop":offsetTop,"position":view["position"],"right":view["right"],"top":view["top"],"z-index":view["zIndex"]}},getSize:function(_element){var view=pi.util.Element.getView(_element);return{"height":view["height"],"offsetHeight":_element.offsetHeight,"offsetWidth":_element.offsetWidth,"width":view["width"]}},addStyle:function(_element,_style){for(var key in _style){key=key=="float"?pi.env.ie.toggle("styleFloat","cssFloat"):key;if(key=="opacity"&&pi.env.ie){pi.util.Element.setOpacity(_element,_style[key]);continue}_element.style[key]=_style[key]}},getStyle:function(_element,_property){_property=_property=="float"?pi.env.ie.toggle("styleFloat","cssFloat"):_property;if(_property=="opacity"&&pi.env.ie)return pi.util.Element.getOpacity(_element.style);return typeof _property=="string"?_element.style[_property]:_element.style},getView:function(_element,_property){var view=document.defaultView?document.defaultView.getComputedStyle(_element,null):_element.currentStyle;_property=_property=="float"?pi.env.ie.toggle("styleFloat","cssFloat"):_property;if(_property=="opacity"&&pi.env.ie)return pi.util.Element.getOpacity(_element,view);return typeof _property=="string"?view[_property]:view}},CloneObject:function(_object,_fn){var tmp={};for(var key in _object){if(pi.util.IsArray(_object[key])){tmp[key]=Array.prototype.clone.apply(_object[key])}else if(pi.util.IsHash(_object[key])){tmp[key]=pi.util.CloneObject(_object[key]);if(_fn)_fn.call(tmp,key,_object)}else tmp[key]=_object[key]}return tmp},MergeObjects:function(_object,_source){for(var key in _source){var value=_source[key];if(pi.util.IsArray(_source[key])){if(pi.util.IsArray(_object[key])){Array.prototype.push.apply(_source[key],_object[key])}else value=_source[key].clone()}else if(pi.util.IsHash(_source[key])){if(pi.util.IsHash(_object[key])){value=pi.util.MergeObjects(_object[key],_source[key])}else{value=pi.util.CloneObject(_source[key])}}_object[key]=value};return _object}};pi.get=function(){return document.getElementById(arguments[0])};pi.get.byTag=function(){return document.getElementsByTagName(arguments[0])};pi.get.byClass=function(){return document.getElementsByClassName.apply(document,arguments)};pi.base=function(){this.body={};this.constructor=null;this.build=function(_skipClonning){var base=this,skipClonning=_skipClonning||false,_private={},fn=function(){var _p=pi.util.CloneObject(_private);if(!skipClonning){for(var key in this){if(pi.util.IsArray(this[key])){this[key]=Array.prototype.clone.apply(this[key])}else if(pi.util.IsHash(this[key])){this[key]=pi.util.CloneObject(this[key],function(_key,_object){this[_key]._parent_=this});this[key]._parent_=this}}};base.createAccessors(_p,this);if(base.constructor)return base.constructor.apply(this,arguments);return this};this.movePrivateMembers(this.body,_private);if(this.constructor){fn["$Constructor"]=this.constructor}fn.prototype=this.body;return fn};this.createAccessors=function(_p,_branch){var getter=function(_property){return this[_property]},setter=function(_property,_value){this[_property]=_value;return _branch._parent_||_branch};for(var name in _p){var isPrivate=name.substring(0,1)=="_",title=name.substring(1,2).toUpperCase()+name.substring(2);if(isPrivate){_branch["get"+title]=getter.curry(_p,name);_branch["set"+title]=setter.curry(_p,name)}else if(pi.util.IsHash(_p[name])){if(!_branch[name])_branch[name]={};this.createAccessors(_p[name],_branch[name])}}};this.movePrivateMembers=function(_object,_branch){for(var name in _object){var isPrivate=name.substring(0,1)=="_";if(isPrivate){_branch[name]=_object[name];delete _object[name]}else if(pi.util.IsHash(_object[name])){_branch[name]={};this.movePrivateMembers(_object[name],_branch[name])}}}};Function.prototype.extend=function(_prototype,_skipClonning){var object=new pi.base,superClass=this;if(_prototype["$Constructor"]){object.constructor=_prototype["$Constructor"];delete _prototype["$Constructor"]};object.body=superClass==pi.base?_prototype:pi.util.MergeObjects(_prototype,superClass.prototype,2);object.constructor=object.constructor||function(){if(superClass!=pi.base)superClass.apply(this,arguments)};return object.build(_skipClonning)};Function.prototype.curry=function(_scope){var fn=this,scope=_scope||window,args=Array.prototype.slice.call(arguments,1);return function(){return fn.apply(scope,args.concat(Array.prototype.slice.call(arguments,0)))}};pi.element=pi.base.extend({"$Constructor":function(_tag){this.environment.setElement(document.createElement(_tag||"DIV"));this.environment.getElement().pi=this;return this},"clean":function(){var childs=this.child.get();while(childs.length){childs[0].parentNode.removeChild(childs[0])}},"clone":function(_deep){return this.environment.getElement().cloneNode(_deep)},"insert":function(_element){_element=_element.environment?_element.environment.getElement():_element;_element.appendChild(this.environment.getElement());return this},"insertAfter":function(_referenceElement){_referenceElement=_referenceElement.environment?_referenceElement.environment.getElement():_referenceElement;_referenceElement.nextSibling?this.insertBefore(_referenceElement.nextSibling):this.insert(_referenceElement.parentNode);return this},"insertBefore":function(_referenceElement){_referenceElement=_referenceElement.environment?_referenceElement.environment.getElement():_referenceElement;_referenceElement.parentNode.insertBefore(this.environment.getElement(),_referenceElement);return this},"query":function(_expression,_resultType,namespaceResolver,_result){return pi.xpath(_expression,_resultType||"ORDERED_NODE_SNAPSHOT_TYPE",this.environment.getElement(),_namespaceResolver,_result)},"remove":function(){this.environment.getParent().removeChild(this.environment.getElement())},"update":function(_value){["TEXTAREA","INPUT"].indexOf(this.environment.getName())>-1?(this.environment.getElement().value=_value):(this.environment.getElement().innerHTML=_value);return this},"attribute":{"getAll":function(_name){return this._parent_.environment.getElement().attributes},"clear":function(_name){this.set(_name,"");return this._parent_},"get":function(_name){return this._parent_.environment.getElement().getAttribute(_name)},"has":function(_name){return pi.env.ie?(this.get(_name)!=null):this._parent_.environment.getElement().hasAttribute(_name)},"remove":function(_name){this._parent_.environment.getElement().removeAttribute(_name);return this._parent_},"set":function(_name,_value){this._parent_.environment.getElement().setAttribute(_name,_value);return this._parent_},"addClass":function(_classes){for(var i=0;i-1)callback[i].fn.apply(this)}}};pi.xhr=pi.xhr.build();pi.xhr.get=function(_url,_returnPiObject){var request=new pi.xhr();request.environment.setAsync(false);request.environment.setUrl(_url);request.send();return _returnPiObject?request:request.environment.getApi()};pi.xpath=function(_expression,_resultType,_contextNode,_namespaceResolver,_result){var contextNode=_contextNode||document,expression=_expression||"",namespaceResolver=_namespaceResolver||null,result=_result||null,resultType=_resultType||"ANY_TYPE";return document.evaluate(expression,contextNode,namespaceResolver,XPathResult[resultType],result)};Array.prototype.clone=function(){var tmp=[];Array.prototype.push.apply(tmp,this);tmp.forEach(function(item,index,object){if(item instanceof Array)object[index]=object[index].clone()});return tmp};Array.prototype.count=function(_value){var count=0;this.forEach(function(){count+=Number(arguments[0]==_value)});return count};Array.prototype.forEach=Array.prototype.forEach||function(_function){for(var i=0;i9?87:48));return((this-remain)/_system).base(_system)+String.fromCharCode(remain+(remain>9?87:48))};Number.prototype.decimal=function(_system){var result=0,digit=String(this).split("");for(var i=0;i58)?digit[i].charCodeAt(0)-87:digit[i]);result+=digit[i]*(Math.pow(_system,digit.length-1-i))}return result};Number.prototype.range=function(_pattern){for(var value=String(this),isFloat=/\./i.test(value),i=isFloat.toggle(parseInt(value.split(".")[0]),0),end=parseInt(value.split(".")[isFloat.toggle(1,0)]),array=[];i=0;i--)str="\\u{0}{1}".format(String(obj[i].charCodeAt(0).base(16)).leftpad(4,"0"),str);return str};pi.util.AddEvent(pi.env.ie?window:document,pi.env.ie?"load":"DOMContentLoaded",function(){for(var i=0;i>>"));el.left.console.input=new pi.element("INPUT").attribute.set("type","text").attribute.addClass("Input").event.addListener("keydown",listen.consoleTextbox).insert(new pi.element("DIV").attribute.addClass("InputContainer").insert(el.left.console.container));el.right.console={};el.right.console.container=new pi.element("DIV").attribute.addClass("Console Container").insert(el.right.container);el.right.console.mlButton=new pi.element("A").attribute.addClass("MLButton CloseML").event.addListener("click",d.console.toggleML).insert(el.right.console.container);el.right.console.input=new pi.element("TEXTAREA").attribute.addClass("Input").insert(el.right.console.container);el.right.console.run=new pi.element("A").attribute.addClass("Button").event.addListener("click",listen.runMultiline).update("Run").insert(el.right.console.container);el.right.console.clear=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.clean.curry(window,el.right.console.input)).update("Clear").insert(el.right.console.container);el.button.console={};el.button.console.container=new pi.element("DIV").attribute.addClass("ButtonSet").insert(el.button.container);el.button.console.clear=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.clean.curry(window,el.left.console.monitor)).update("Clear").insert(el.button.console.container);el.left.html={};el.left.html.container=new pi.element("DIV").attribute.addClass("HTML").insert(el.left.container);el.right.html={};el.right.html.container=new pi.element("DIV").attribute.addClass("HTML Container").insert(el.right.container);el.right.html.nav={};el.right.html.nav.container=new pi.element("DIV").attribute.addClass("Nav").insert(el.right.html.container);el.right.html.nav.computedStyle=new pi.element("A").attribute.addClass("Tab Selected").event.addListener("click",d.html.navigate.curry(firebug,"computedStyle")).update("Computed Style").insert(el.right.html.nav.container);if(!pi.env.ie6)el.right.html.nav.dom=new pi.element("A").attribute.addClass("Tab").event.addListener("click",d.html.navigate.curry(firebug,"dom")).update("DOM").insert(el.right.html.nav.container);el.right.html.content=new pi.element("DIV").attribute.addClass("Content").insert(el.right.html.container);el.button.html={};el.button.html.container=new pi.element("DIV").attribute.addClass("ButtonSet HTML").insert(el.button.container);el.left.css={};el.left.css.container=new pi.element("DIV").attribute.addClass("CSS").insert(el.left.container);el.right.css={};el.right.css.container=new pi.element("DIV").attribute.addClass("CSS Container").insert(el.right.container);el.right.css.nav={};el.right.css.nav.container=new pi.element("DIV").attribute.addClass("Nav").insert(el.right.css.container);el.right.css.nav.runCSS=new pi.element("A").attribute.addClass("Tab Selected").update("Run CSS").insert(el.right.css.nav.container);el.right.css.mlButton=new pi.element("A").attribute.addClass("MLButton CloseML").event.addListener("click",d.console.toggleML).insert(el.right.css.container);el.right.css.input=new pi.element("TEXTAREA").attribute.addClass("Input").insert(el.right.css.container);el.right.css.run=new pi.element("A").attribute.addClass("Button").event.addListener("click",listen.runCSS).update("Run").insert(el.right.css.container);el.right.css.clear=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.clean.curry(window,el.right.css.input)).update("Clear").insert(el.right.css.container);el.button.css={};el.button.css.container=new pi.element("DIV").attribute.addClass("ButtonSet CSS").insert(el.button.container);el.button.css.selectbox=new pi.element("SELECT").event.addListener("change",listen.cssSelectbox).insert(el.button.css.container);el.left.scripts={};el.left.scripts.container=new pi.element("DIV").attribute.addClass("Scripts").insert(el.left.container);el.right.scripts={};el.right.scripts.container=new pi.element("DIV").attribute.addClass("Scripts Container").insert(el.right.container);el.button.scripts={};el.button.scripts.container=new pi.element("DIV").attribute.addClass("ButtonSet Scripts").insert(el.button.container);el.button.scripts.selectbox=new pi.element("SELECT").event.addListener("change",listen.scriptsSelectbox).insert(el.button.scripts.container);el.button.scripts.lineNumbers=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.scripts.toggleLineNumbers).update("Show Line Numbers").insert(el.button.scripts.container);el.left.dom={};el.left.dom.container=new pi.element("DIV").attribute.addClass("DOM").insert(el.left.container);el.right.dom={};el.right.dom.container=new pi.element("DIV").attribute.addClass("DOM Container").insert(el.right.container);el.button.dom={};el.button.dom.container=new pi.element("DIV").attribute.addClass("ButtonSet DOM").insert(el.button.container);el.button.dom.label=new pi.element("LABEL").update("Object Path:").insert(el.button.dom.container);el.button.dom.textbox=new pi.element("INPUT").event.addListener("keydown",listen.domTextbox).update("window").insert(el.button.dom.container);el.left.str={};el.left.str.container=new pi.element("DIV").attribute.addClass("STR").insert(el.left.container);el.right.str={};el.right.str.container=new pi.element("DIV").attribute.addClass("STR").insert(el.left.container);el.button.str={};el.button.str.container=new pi.element("DIV").attribute.addClass("ButtonSet XHR").insert(el.button.container);el.button.str.watch=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.navigate.curry(window,"xhr")).update("Back").insert(el.button.str.container);el.left.xhr={};el.left.xhr.container=new pi.element("DIV").attribute.addClass("XHR").insert(el.left.container);el.right.xhr={};el.right.xhr.container=new pi.element("DIV").attribute.addClass("XHR").insert(el.left.container);el.button.xhr={};el.button.xhr.container=new pi.element("DIV").attribute.addClass("ButtonSet XHR").insert(el.button.container);el.button.xhr.label=new pi.element("LABEL").update("XHR Path:").insert(el.button.xhr.container);el.button.xhr.textbox=new pi.element("INPUT").event.addListener("keydown",listen.xhrTextbox).insert(el.button.xhr.container);el.button.xhr.watch=new pi.element("A").attribute.addClass("Button").event.addListener("click",listen.addXhrObject).update("Watch").insert(el.button.xhr.container);if(pi.env.ie6){var buttons=[el.button.inspect,el.button.close,el.button.inspect,el.button.console.clear,el.right.console.run,el.right.console.clear,el.right.css.run,el.right.css.clear];for(var i=0;i>> console.dir("+_value+")");d.dom.open(_value,d.console.addLine())}},addLine:function(){with(firebug){return new pi.element("DIV").attribute.addClass("Row").insert(el.left.console.monitor)}},openObject:function(_index){with(firebug){d.dom.open(env.objCn[_index],el.left.dom.container,pi.env.ie);d.navigate("dom")}},historyIndex:0,history:[],log:function(_values){with(firebug){if(env.init==false){env.ctmp.push(arguments);return}var value="";for(var i=0;i0?" ":"")+d.highlight(arguments[i],false,false,true)}d.console.addLine().update(value);d.console.scroll()}},print:function(_cmd,_text){with(firebug){d.console.addLine().attribute.addClass("Arrow").update(">>> "+_cmd);d.console.addLine().update(d.highlight(_text,false,false,true));d.console.scroll();d.console.historyIndex=d.console.history.push(_cmd)}},run:function(cmd){with(firebug){if(cmd.length==0)return;el.left.console.input.environment.getElement().value="";try{var result=eval.call(window,cmd);d.console.print(cmd,result)}catch(e){d.console.addLine().attribute.addClass("Arrow").update(">>> "+cmd);if(!pi.env.ff){d.console.scroll();return d.console.addLine().attribute.addClass("Error").update("Error: "+(e.description||e),true)}if(e.fileName==null){d.console.addLine().attribute.addClass("Error").update("Error: "+e.message,true)}var fileName=e.fileName.split("\/").getLastItem();d.console.addLine().attribute.addClass("Error").update("Error: "+e.message+" ("+fileName+","+e.lineNumber+")",true);d.console.scroll()}d.console.scroll()}},scroll:function(){with(firebug){el.left.console.monitor.environment.getElement().parentNode.scrollTop=Math.abs(el.left.console.monitor.environment.getSize().offsetHeight-200)}},toggleML:function(){with(firebug){var open=!env.ml;env.ml=!env.ml;d.navigateRightColumn("console",open);el[open?"left":"right"].console.mlButton.environment.addStyle({display:"none"});el[!open?"left":"right"].console.mlButton.environment.addStyle({display:"block"});el.left.console.monitor.environment.addStyle({"height":(open?233:210)+"px"});el.left.console.mlButton.attribute[(open?"add":"remove")+"Class"]("CloseML")}}},css:{index:-1,open:function(_index){with(firebug){var item=document.styleSheets[_index];var uri=item.href;if(uri.indexOf("http:\/\/")>-1&&getDomain(uri)!=document.domain){el.left.css.container.update("Access to restricted URI denied");return}var rules=item[pi.env.ie?"rules":"cssRules"];var str="";for(var i=0;i";for(var i=0;i<_css.length;i++){var item=_css[i];str+="
"+item.replace(/(.+\:)(.+)/,"$1$2;")+"
"}str+="
}
";return str}},refresh:function(){with(firebug){el.button.css.selectbox.update("");var collection=document.styleSheets;for(var i=0;i-1){if(_value==null){return"null"}if(["boolean","number"].indexOf(typeof _value)>-1){return""+_value+""}if(typeof _value=="function"){return"function()"}return"\""+(!_inObject&&!_inArray?_value:_value.substring(0,35)).replace(/\n/g,"\\n").replace(/\s/g," ").replace(/>/g,">").replace(/"}else if(isElement){if(_value.nodeType==3)return d.highlight(_value.nodeValue);if(_inArray||_inObject){var result=""+_value.nodeName.toLowerCase();if(_value.getAttribute&&_value.getAttribute("id"))result+="#"+_value.getAttribute("id")+"";var elClass=_value.getAttribute?_value.getAttribute(pi.env.ie?"className":"class"):"";if(elClass)result+="."+elClass.split(" ")[0]+"";return result+""}var result="<"+_value.nodeName.toLowerCase()+"";if(_value.attributes)for(var i=0;i<_value.attributes.length;i++){var item=_value.attributes[i];if(pi.env.ie&&Boolean(item.nodeValue)==false)continue;result+=" "+item.nodeName+"=\""+item.nodeValue+"\""}result+=">";return result}else if(isArray||["object","array"].indexOf(typeof _value)>-1){var result="";if(isArray||_value instanceof Array){if(_inObject)return"["+_value.length+"]";result+="[ ";for(var i=0;i<_value.length;i++){if((_inObject||_inArray)&&pi.env.ie&&i>3)break;result+=(i>0?", ":"")+d.highlight(_value[i],false,true,true)}result+=" ]";return result}if(_inObject)return"Object";result+="Object";var i=0;for(var key in _value){var value=_value[key];if((_inObject||_inArray)&&pi.env.ie&&i>3)break;result+=" "+key+"="+d.highlight(value,true);i++};result+="";return result}else{if(_inObject)return""+_value+"";return _value}}},html:{nIndex:"computedStyle",current:null,highlight:function(_element,_clear,_event){with(firebug){if(_clear){el.bgInspector.environment.addStyle({"display":"none"});return}d.inspector.inspect(_element,true)}},inspect:function(_element){var el=_element,map=[],parent=_element;while(parent){map.push(parent);if(parent==document.body)break;parent=parent.parentNode}map=map.reverse();with(firebug){d.inspector.toggle();var parentLayer=el.left.html.container.child.get()[1].childNodes[1].pi;for(var t=0;map[t];){if(t==map.length-1){var link=parentLayer.environment.getElement().previousSibling.pi;link.attribute.addClass("Selected");if(d.html.current)d.html.current[1].attribute.removeClass("Selected");d.html.current=[_element,link];return;t}parentLayer=d.html.openHtmlTree(map[t],parentLayer,map[t+1]);t++}}},navigate:function(_index,_element){with(firebug){el.right.html.nav[d.html.nIndex].attribute.removeClass("Selected");el.right.html.nav[_index].attribute.addClass("Selected");d.html.nIndex=_index;d.html.openProperties()}},openHtmlTree:function(_element,_parent,_returnParentElementByElement,_event){with(firebug){var element=_element||document.documentElement,parent=_parent||el.left.html.container,returnParentEl=_returnParentElementByElement||null,returnParentVal=null;if(parent!=el.left.html.container){var nodeLink=parent.environment.getParent().pi.child.get()[0].pi;if(d.html.current)d.html.current[1].attribute.removeClass("Selected");nodeLink.attribute.addClass("Selected");d.html.current=[_element,nodeLink];d.html.openProperties()}if(element.childNodes&&(element.childNodes.length==0||(element.childNodes.length==1&&element.childNodes[0].nodeType==3)))return;parent.clean();if(parent.opened&&Boolean(_returnParentElementByElement)==false){parent.opened=false;parent.environment.getParent().pi.child.get()[0].pi.attribute.removeClass("Open");return}if(parent!=el.left.html.container){parent.environment.getParent().pi.child.get()[0].pi.attribute.addClass("Open");parent.opened=true}for(var i=0;i"));continue}else if(item.childNodes&&item.childNodes.length==0)continue;link.attribute.addClass("ParentLink")}return returnParentVal}},openProperties:function(){with(firebug){var index=d.html.nIndex;var node=d.html.current[0];d.clean(el.right.html.content);var str="";switch(index){case"computedStyle":var property=["opacity","filter","azimuth","background","backgroundAttachment","backgroundColor","backgroundImage","backgroundPosition","backgroundRepeat","border","borderCollapse","borderColor","borderSpacing","borderStyle","borderTop","borderRight","borderBottom","borderLeft","borderTopColor","borderRightColor","borderBottomColor","borderLeftColor","borderTopStyle","borderRightStyle","borderBottomStyle","borderLeftStyle","borderTopWidth","borderRightWidth","borderBottomWidth","borderLeftWidth","borderWidth","bottom","captionSide","clear","clip","color","content","counterIncrement","counterReset","cue","cueAfter","cueBefore","cursor","direction","display","elevation","emptyCells","cssFloat","font","fontFamily","fontSize","fontSizeAdjust","fontStretch","fontStyle","fontVariant","fontWeight","height","left","letterSpacing","lineHeight","listStyle","listStyleImage","listStylePosition","listStyleType","margin","marginTop","marginRight","marginBottom","marginLeft","markerOffset","marks","maxHeight","maxWidth","minHeight","minWidth","orphans","outline","outlineColor","outlineStyle","outlineWidth","overflow","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","page","pageBreakAfter","pageBreakBefore","pageBreakInside","pause","pauseAfter","pauseBefore","pitch","pitchRange","playDuring","position","quotes","richness","right","size","speak","speakHeader","speakNumeral","speakPunctuation","speechRate","stress","tableLayout","textAlign","textDecoration","textIndent","textShadow","textTransform","top","unicodeBidi","verticalAlign","visibility","voiceFamily","volume","whiteSpace","widows","width","wordSpacing","zIndex"].sort();var view=document.defaultView?document.defaultView.getComputedStyle(node,null):node.currentStyle;for(var i=0;i
"+item+"
"+d.highlight(view[item])+"
"}el.right.html.content.update(str);break;case"dom":d.dom.open(node,el.right.html.content,pi.env.ie);break}}}},inspector:{enabled:false,el:null,inspect:function(_element,_bgInspector){var el=_element,top=el.offsetTop,left=el.offsetLeft,parent=_element.offsetParent;while(Boolean(parent)&&parent!=document.firstChild){top+=parent.offsetTop;left+=parent.offsetLeft;parent=parent.offsetParent;if(parent==document.body)break};with(firebug){el[_bgInspector?"bgInspector":"borderInspector"].environment.addStyle({"width":_element.offsetWidth+"px","height":_element.offsetHeight+"px","top":top-(_bgInspector?0:2)+"px","left":left-(_bgInspector?0:2)+"px","display":"block"});if(!_bgInspector){d.inspector.el=_element}}},toggle:function(){with(firebug){d.inspector.enabled=!d.inspector.enabled;el.button.inspect.attribute[(d.inspector.enabled?"add":"remove")+"Class"]("Enabled");if(d.inspector.enabled==false){el.borderInspector.environment.addStyle({"display":"none"});d.inspector.el=null}else if(pi.env.dIndex!="html"){d.navigate("html")}}}},scripts:{index:-1,lineNumbers:false,open:function(_index){with(firebug){d.scripts.index=_index;el.left.scripts.container.update("");var script=document.getElementsByTagName("script")[_index],uri=script.src||document.location.href,source;if(uri.indexOf("http:\/\/")>-1&&getDomain(uri)!=document.domain){el.left.scripts.container.update("Access to restricted URI denied");return}if(uri!=document.location.href){source=env.cache[uri]||pi.xhr.get(uri).responseText;env.cache[uri]=source}else source=script.innerHTML;source=source.replace(/\n|\t|<|>/g,function(_ch){return({"<":"<",">":">","\t":"        ","\n":"
"})[_ch]});if(!d.scripts.lineNumbers)el.left.scripts.container.child.add(new pi.element("DIV").attribute.addClass("CodeContainer").update(source));else{source=source.split("
");for(var i=0;i"))}}},xhr:{objects:[],addObject:function(){with(firebug){for(var i=0;id.console.historyIndex?d.console.history[d.console.historyIndex]:"")}},cssSelectbox:function(){with(firebug){d.css.open(el.button.css.selectbox.environment.getElement().selectedIndex)}},domTextbox:function(_event){with(firebug){if(_event.keyCode==13){d.dom.open(eval(el.button.dom.textbox.environment.getElement().value),el.left.dom.container)}}},inspector:function(){with(firebug){d.html.inspect(d.inspector.el)}},keyboard:function(_event){with(firebug){if(_event.keyCode==27&&d.inspector.enabled)d.inspector.toggle()}},mouse:function(_event){with(firebug){var target=_event[pi.env.ie?"srcElement":"target"];if(d.inspector.enabled&&target!=document.body&&target!=document.firstChild&&target!=document.childNodes[1]&&target!=el.borderInspector.environment.getElement()&&target!=el.main.environment.getElement()&&target.offsetParent!=el.main.environment.getElement())d.inspector.inspect(target)}},runMultiline:function(){with(firebug){d.console.run.call(window,el.right.console.input.environment.getElement().value)}},runCSS:function(){with(firebug){var source=el.right.css.input.environment.getElement().value.replace(/\n|\t/g,"").split("}");for(var i=0;i0?collection[0]:document.body.appendChild(document.createElement("style"));if(!item.match(/.+\{.+\}/))continue;if(pi.env.ie)style.styleSheet.addRule(rule[0],rule[1]);else style.sheet.insertRule(rule,style.sheet.cssRules.length)}}},scriptsSelectbox:function(){with(firebug){d.scripts.open(parseInt(el.button.scripts.selectbox.environment.getElement().value))}},xhrTextbox:function(_event){with(firebug){if(_event.keyCode==13){d.xhr.addObject.apply(window,el.button.xhr.textbox.environment.getElement().value.split(","))}}}}};window.console=firebug.d.console;pi.util.AddEvent(window,"resize",firebug.d.refreshSize);pi.util.AddEvent(document,"mousemove",firebug.listen.mouse);pi.util.AddEvent(document,"keydown",firebug.listen.keyboard);pi.util.DOMContentLoaded.push(firebug.init); \ No newline at end of file diff --git a/timeside/ui/lib/jquery.js b/timeside/ui/lib/jquery.js new file mode 100755 index 0000000..88e661e --- /dev/null +++ b/timeside/ui/lib/jquery.js @@ -0,0 +1,3549 @@ +(function(){ +/* + * jQuery 1.2.6 - New Wave Javascript + * + * Copyright (c) 2008 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $ + * $Rev: 5685 $ + */ + +// Map over jQuery in case of overwrite +var _jQuery = window.jQuery, +// Map over the $ in case of overwrite + _$ = window.$; + +var jQuery = window.jQuery = window.$ = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); +}; + +// A simple way to check for HTML strings or ID strings +// (both of which we optimize for) +var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/, + +// Is it a simple selector + isSimple = /^.[^:#\[\.]*$/, + +// Will speed up references to undefined, and allows munging its name. + undefined; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this[0] = selector; + this.length = 1; + return this; + } + // Handle HTML strings + if ( typeof selector == "string" ) { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) + selector = jQuery.clean( [ match[1] ], context ); + + // HANDLE: $("#id") + else { + var elem = document.getElementById( match[3] ); + + // Make sure an element was located + if ( elem ){ + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id != match[3] ) + return jQuery().find( selector ); + + // Otherwise, we inject the element directly into the jQuery object + return jQuery( elem ); + } + selector = []; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return jQuery( context ).find( selector ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) + return jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector ); + + return this.setArray(jQuery.makeArray(selector)); + }, + + // The current version of jQuery being used + jquery: "1.2.6", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // The number of elements contained in the matched element set + length: 0, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == undefined ? + + // Return a 'clean' array + jQuery.makeArray( this ) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + // Build a new jQuery matched element set + var ret = jQuery( elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function( elems ) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply( this, elems ); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + var ret = -1; + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem && elem.jquery ? elem[0] : elem + , this ); + }, + + attr: function( name, value, type ) { + var options = name; + + // Look for the case where we're accessing a style value + if ( name.constructor == String ) + if ( value === undefined ) + return this[0] && jQuery[ type || "attr" ]( this[0], name ); + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i){ + // Set all the styles + for ( name in options ) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop( this, options[ name ], type, i, name ) + ); + }); + }, + + css: function( key, value ) { + // ignore negative width and height values + if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) + value = undefined; + return this.attr( key, value, "curCSS" ); + }, + + text: function( text ) { + if ( typeof text != "object" && text != null ) + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + + var ret = ""; + + jQuery.each( text || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text( [ this ] ); + }); + }); + + return ret; + }, + + wrapAll: function( html ) { + if ( this[0] ) + // The elements to wrap the target around + jQuery( html, this[0].ownerDocument ) + .clone() + .insertBefore( this[0] ) + .map(function(){ + var elem = this; + + while ( elem.firstChild ) + elem = elem.firstChild; + + return elem; + }) + .append(this); + + return this; + }, + + wrapInner: function( html ) { + return this.each(function(){ + jQuery( this ).contents().wrapAll( html ); + }); + }, + + wrap: function( html ) { + return this.each(function(){ + jQuery( this ).wrapAll( html ); + }); + }, + + append: function() { + return this.domManip(arguments, true, false, function(elem){ + if (this.nodeType == 1) + this.appendChild( elem ); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, true, function(elem){ + if (this.nodeType == 1) + this.insertBefore( elem, this.firstChild ); + }); + }, + + before: function() { + return this.domManip(arguments, false, false, function(elem){ + this.parentNode.insertBefore( elem, this ); + }); + }, + + after: function() { + return this.domManip(arguments, false, true, function(elem){ + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + }, + + end: function() { + return this.prevObject || jQuery( [] ); + }, + + find: function( selector ) { + var elems = jQuery.map(this, function(elem){ + return jQuery.find( selector, elem ); + }); + + return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ? + jQuery.unique( elems ) : + elems ); + }, + + clone: function( events ) { + // Do the clone + var ret = this.map(function(){ + if ( jQuery.browser.msie && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var clone = this.cloneNode(true), + container = document.createElement("div"); + container.appendChild(clone); + return jQuery.clean([container.innerHTML])[0]; + } else + return this.cloneNode(true); + }); + + // Need to set the expando to null on the cloned set if it exists + // removeData doesn't work here, IE removes it from the original as well + // this is primarily for IE but the data expando shouldn't be copied over in any browser + var clone = ret.find("*").andSelf().each(function(){ + if ( this[ expando ] != undefined ) + this[ expando ] = null; + }); + + // Copy the events from the original to the clone + if ( events === true ) + this.find("*").andSelf().each(function(i){ + if (this.nodeType == 3) + return; + var events = jQuery.data( this, "events" ); + + for ( var type in events ) + for ( var handler in events[ type ] ) + jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data ); + }); + + // Return the cloned set + return ret; + }, + + filter: function( selector ) { + return this.pushStack( + jQuery.isFunction( selector ) && + jQuery.grep(this, function(elem, i){ + return selector.call( elem, i ); + }) || + + jQuery.multiFilter( selector, this ) ); + }, + + not: function( selector ) { + if ( selector.constructor == String ) + // test special case where just one selector is passed in + if ( isSimple.test( selector ) ) + return this.pushStack( jQuery.multiFilter( selector, this, true ) ); + else + selector = jQuery.multiFilter( selector, this ); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; + }); + }, + + add: function( selector ) { + return this.pushStack( jQuery.unique( jQuery.merge( + this.get(), + typeof selector == 'string' ? + jQuery( selector ) : + jQuery.makeArray( selector ) + ))); + }, + + is: function( selector ) { + return !!selector && jQuery.multiFilter( selector, this ).length > 0; + }, + + hasClass: function( selector ) { + return this.is( "." + selector ); + }, + + val: function( value ) { + if ( value == undefined ) { + + if ( this.length ) { + var elem = this[0]; + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if ( index < 0 ) + return null; + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery.browser.msie && !option.attributes.value.specified ? option.text : option.value; + + // We don't need an array for one selects + if ( one ) + return value; + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + + // Everything else, we just grab the value + } else + return (this[0].value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + if( value.constructor == Number ) + value += ''; + + return this.each(function(){ + if ( this.nodeType != 1 ) + return; + + if ( value.constructor == Array && /radio|checkbox/.test( this.type ) ) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(value); + + jQuery( "option", this ).each(function(){ + this.selected = (jQuery.inArray( this.value, values ) >= 0 || + jQuery.inArray( this.text, values ) >= 0); + }); + + if ( !values.length ) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function( value ) { + return value == undefined ? + (this[0] ? + this[0].innerHTML : + null) : + this.empty().append( value ); + }, + + replaceWith: function( value ) { + return this.after( value ).remove(); + }, + + eq: function( i ) { + return this.slice( i, i + 1 ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ) ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function(elem, i){ + return callback.call( elem, i, elem ); + })); + }, + + andSelf: function() { + return this.add( this.prevObject ); + }, + + data: function( key, value ){ + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + if ( data === undefined && this.length ) + data = jQuery.data( this[0], key ); + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } else + return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){ + jQuery.data( this, key, value ); + }); + }, + + removeData: function( key ){ + return this.each(function(){ + jQuery.removeData( this, key ); + }); + }, + + domManip: function( args, table, reverse, callback ) { + var clone = this.length > 1, elems; + + return this.each(function(){ + if ( !elems ) { + elems = jQuery.clean( args, this.ownerDocument ); + + if ( reverse ) + elems.reverse(); + } + + var obj = this; + + if ( table && jQuery.nodeName( this, "table" ) && jQuery.nodeName( elems[0], "tr" ) ) + obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") ); + + var scripts = jQuery( [] ); + + jQuery.each(elems, function(){ + var elem = clone ? + jQuery( this ).clone( true )[0] : + this; + + // execute all scripts after the elements have been injected + if ( jQuery.nodeName( elem, "script" ) ) + scripts = scripts.add( elem ); + else { + // Remove any inner scripts for later evaluation + if ( elem.nodeType == 1 ) + scripts = scripts.add( jQuery( "script", elem ).remove() ); + + // Inject the elements into the document + callback.call( obj, elem ); + } + }); + + scripts.each( evalScript ); + }); + } +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +function evalScript( i, elem ) { + if ( elem.src ) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + + if ( elem.parentNode ) + elem.parentNode.removeChild( elem ); +} + +function now(){ + return +new Date; +} + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if ( target.constructor == Boolean ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target != "object" && typeof target != "function" ) + target = {}; + + // extend jQuery itself if only one argument is passed + if ( length == i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) + // Extend the base object + for ( var name in options ) { + var src = target[ name ], copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) + continue; + + // Recurse if we're merging object values + if ( deep && copy && typeof copy == "object" && !copy.nodeType ) + target[ name ] = jQuery.extend( deep, + // Never move original objects, clone them + src || ( copy.length != null ? [ ] : { } ) + , copy ); + + // Don't bring in undefined values + else if ( copy !== undefined ) + target[ name ] = copy; + + } + + // Return the modified object + return target; +}; + +var expando = "jQuery" + now(), uuid = 0, windowData = {}, + // exclude the following css properties to add px + exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + // cache defaultView + defaultView = document.defaultView || {}; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning this function. + isFunction: function( fn ) { + return !!fn && typeof fn != "string" && !fn.nodeName && + fn.constructor != Array && /^[\s[]?function/.test( fn + "" ); + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function( elem ) { + return elem.documentElement && !elem.body || + elem.tagName && elem.ownerDocument && !elem.ownerDocument.body; + }, + + // Evalulates a script in a global context + globalEval: function( data ) { + data = jQuery.trim( data ); + + if ( data ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if ( jQuery.browser.msie ) + script.text = data; + else + script.appendChild( document.createTextNode( data ) ); + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + cache: {}, + + data: function( elem, name, data ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ]; + + // Compute a unique ID for the element + if ( !id ) + id = elem[ expando ] = ++uuid; + + // Only generate the data cache if we're + // trying to access or manipulate it + if ( name && !jQuery.cache[ id ] ) + jQuery.cache[ id ] = {}; + + // Prevent overriding the named cache with undefined values + if ( data !== undefined ) + jQuery.cache[ id ][ name ] = data; + + // Return the named cache data, or the ID for the element + return name ? + jQuery.cache[ id ][ name ] : + id; + }, + + removeData: function( elem, name ) { + elem = elem == window ? + windowData : + elem; + + var id = elem[ expando ]; + + // If we want to remove a specific section of the element's data + if ( name ) { + if ( jQuery.cache[ id ] ) { + // Remove the section of cache data + delete jQuery.cache[ id ][ name ]; + + // If we've removed all the data, remove the element's cache + name = ""; + + for ( name in jQuery.cache[ id ] ) + break; + + if ( !name ) + jQuery.removeData( elem ); + } + + // Otherwise, we want to remove all of the element's data + } else { + // Clean up the element expando + try { + delete elem[ expando ]; + } catch(e){ + // IE has trouble directly removing the expando + // but it's ok with using removeAttribute + if ( elem.removeAttribute ) + elem.removeAttribute( expando ); + } + + // Completely remove the data cache + delete jQuery.cache[ id ]; + } + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, length = object.length; + + if ( args ) { + if ( length == undefined ) { + for ( name in object ) + if ( callback.apply( object[ name ], args ) === false ) + break; + } else + for ( ; i < length; ) + if ( callback.apply( object[ i++ ], args ) === false ) + break; + + // A special, fast, case for the most common use of each + } else { + if ( length == undefined ) { + for ( name in object ) + if ( callback.call( object[ name ], name, object[ name ] ) === false ) + break; + } else + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + } + + return object; + }, + + prop: function( elem, value, type, i, name ) { + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, i ); + + // Handle passing in a number to a CSS property + return value && value.constructor == Number && type == "curCSS" && !exclude.test( name ) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, classNames ) { + jQuery.each((classNames || "").split(/\s+/), function(i, className){ + if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, classNames ) { + if (elem.nodeType == 1) + elem.className = classNames != undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className){ + return !jQuery.className.has( classNames, className ); + }).join(" ") : + ""; + }, + + // internal only, use hasClass("class") + has: function( elem, className ) { + return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) + elem.style[ name ] = old[ name ]; + }, + + css: function( elem, name, force ) { + if ( name == "width" || name == "height" ) { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + var padding = 0, border = 0; + jQuery.each( which, function() { + padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + }); + val -= Math.round(padding + border); + } + + if ( jQuery(elem).is(":visible") ) + getWH(); + else + jQuery.swap( elem, props, getWH ); + + return Math.max(0, val); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret, style = elem.style; + + // A helper method for determining if an element's values are broken + function color( elem ) { + if ( !jQuery.browser.safari ) + return false; + + // defaultView is cached + var ret = defaultView.getComputedStyle( elem, null ); + return !ret || ret.getPropertyValue("color") == ""; + } + + // We need to handle opacity special in IE + if ( name == "opacity" && jQuery.browser.msie ) { + ret = jQuery.attr( style, "opacity" ); + + return ret == "" ? + "1" : + ret; + } + // Opera sometimes will give the wrong display answer, this fixes it, see #2037 + if ( jQuery.browser.opera && name == "display" ) { + var save = style.outline; + style.outline = "0 solid black"; + style.outline = save; + } + + // Make sure we're using the right name for getting the float value + if ( name.match( /float/i ) ) + name = styleFloat; + + if ( !force && style && style[ name ] ) + ret = style[ name ]; + + else if ( defaultView.getComputedStyle ) { + + // Only "float" is needed here + if ( name.match( /float/i ) ) + name = "float"; + + name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + + var computedStyle = defaultView.getComputedStyle( elem, null ); + + if ( computedStyle && !color( elem ) ) + ret = computedStyle.getPropertyValue( name ); + + // If the element isn't reporting its values properly in Safari + // then some display: none elements are involved + else { + var swap = [], stack = [], a = elem, i = 0; + + // Locate all of the parent display: none elements + for ( ; a && color(a); a = a.parentNode ) + stack.unshift(a); + + // Go through and make them visible, but in reverse + // (It would be better if we knew the exact display type that they had) + for ( ; i < stack.length; i++ ) + if ( color( stack[ i ] ) ) { + swap[ i ] = stack[ i ].style.display; + stack[ i ].style.display = "block"; + } + + // Since we flip the display style, we have to handle that + // one special, otherwise get the value + ret = name == "display" && swap[ stack.length - 1 ] != null ? + "none" : + ( computedStyle && computedStyle.getPropertyValue( name ) ) || ""; + + // Finally, revert the display styles back + for ( i = 0; i < swap.length; i++ ) + if ( swap[ i ] != null ) + stack[ i ].style.display = swap[ i ]; + } + + // We should always get a number back from opacity + if ( name == "opacity" && ret == "" ) + ret = "1"; + + } else if ( elem.currentStyle ) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter){ + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + + return ret; + }, + + clean: function( elems, context ) { + var ret = []; + context = context || document; + // !context.createElement fails in IE with an error but returns typeof 'object' + if (typeof context.createElement == 'undefined') + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + jQuery.each(elems, function(i, elem){ + if ( !elem ) + return; + + if ( elem.constructor == Number ) + elem += ''; + + // Convert html string into DOM nodes + if ( typeof elem == "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = jQuery.trim( elem ).toLowerCase(), div = context.createElement("div"); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
" ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and - - - - - - - - - - - - - - - - - - -

TimeSide Player

- -
-
-
-
- -

- - - - -
- Skin: - -

- - diff --git a/ui/demo/waveform.png b/ui/demo/waveform.png deleted file mode 100755 index 75e3f9d..0000000 Binary files a/ui/demo/waveform.png and /dev/null differ diff --git a/ui/lib/firebug-lite-compressed.js b/ui/lib/firebug-lite-compressed.js deleted file mode 100644 index df2601c..0000000 --- a/ui/lib/firebug-lite-compressed.js +++ /dev/null @@ -1,2 +0,0 @@ -(function(_scope){_scope.pi=Object(3.14159265358979323846);var pi=_scope.pi;pi.version=1.0;pi.env={ie:/MSIE/i.test(navigator.userAgent),ie6:/MSIE 6/i.test(navigator.userAgent),ie7:/MSIE 7/i.test(navigator.userAgent),ie8:/MSIE 8/i.test(navigator.userAgent),firefox:/Firefox/i.test(navigator.userAgent),opera:/Opera/i.test(navigator.userAgent),webkit:/Webkit/i.test(navigator.userAgent)};pi.util={IsArray:function(_object){return _object&&_object!=window&&(_object instanceof Array||(typeof _object.length=="number"&&typeof _object.item=="function"))},IsHash:function(_object){return _object&&typeof _object=="object"&&(_object==window||_object instanceof Object)&&!_object.nodeName&&!pi.util.IsArray(_object)},DOMContentLoaded:[],AddEvent:function(_element,_eventName,_fn,_useCapture){_element[pi.env.ie.toggle("attachEvent","addEventListener")](pi.env.ie.toggle("on","")+_eventName,_fn,_useCapture||false);return pi.util.AddEvent.curry(this,_element)},RemoveEvent:function(_element,_eventName,_fn,_useCapture){return _element[pi.env.ie.toggle("detachEvent","removeEventListener")](pi.env.ie.toggle("on","")+_eventName,_fn,_useCapture||false)},GetWindowSize:function(){return{height:pi.env.ie?Math.max(document.documentElement.clientHeight,document.body.clientHeight):window.innerHeight,width:pi.env.ie?Math.max(document.documentElement.clientWidth,document.body.clientWidth):window.innerWidth}},Include:function(_url,_callback){var script=new pi.element("script").attribute.set("src",_url),callback=_callback||new Function,done=false,head=pi.get.byTag("head")[0];script.environment.getElement().onload=script.environment.getElement().onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){callback.call(this);done=true;head.removeChild(script.environment.getElement())}};script.insert(head)},Element:{addClass:function(_element,_class){if(!pi.util.Element.hasClass(_element,_class))pi.util.Element.setClass(_element,pi.util.Element.getClass(_element)+" "+_class)},getClass:function(_element){return _element.getAttribute(pi.env.ie.toggle("className","class"))||""},hasClass:function(_element,_class){return pi.util.Element.getClass(_element).split(" ").indexOf(_class)>-1},removeClass:function(_element,_class){if(pi.util.Element.hasClass(_element,_class))pi.util.Element.setClass(_element,pi.util.Element.getClass(_element,_class).split(" ").removeValue(_class).join(" "))},setClass:function(_element,_value){_element.setAttribute(pi.env.ie.toggle("className","class"),_value)},toggleClass:function(){if(pi.util.Element.hasClass.apply(this,arguments))pi.util.Element.removeClass.apply(this,arguments);else pi.util.Element.addClass.apply(this,arguments)},getOpacity:function(_styleObject){var styleObject=_styleObject;if(!pi.env.ie)return styleObject["opacity"];var alpha=styleObject["filter"].match(/opacity\=(\d+)/i);return alpha?alpha[1]/100:1},setOpacity:function(_element,_value){if(!pi.env.ie)return pi.util.Element.addStyle(_element,{"opacity":_value});_value*=100;pi.util.Element.addStyle(_element,{"filter":"alpha(opacity="+_value+")"});return this._parent_},getPosition:function(_element){var parent=_element,offsetLeft=0,offsetTop=0,view=pi.util.Element.getView(_element);while(parent&&parent!=document.body&&parent!=document.firstChild){offsetLeft+=parseInt(parent.offsetLeft);offsetTop+=parseInt(parent.offsetTop);parent=parent.offsetParent};return{"bottom":view["bottom"],"left":view["left"],"marginTop":view["marginTop"],"marginLeft":view["marginLeft"],"offsetLeft":offsetLeft,"offsetTop":offsetTop,"position":view["position"],"right":view["right"],"top":view["top"],"z-index":view["zIndex"]}},getSize:function(_element){var view=pi.util.Element.getView(_element);return{"height":view["height"],"offsetHeight":_element.offsetHeight,"offsetWidth":_element.offsetWidth,"width":view["width"]}},addStyle:function(_element,_style){for(var key in _style){key=key=="float"?pi.env.ie.toggle("styleFloat","cssFloat"):key;if(key=="opacity"&&pi.env.ie){pi.util.Element.setOpacity(_element,_style[key]);continue}_element.style[key]=_style[key]}},getStyle:function(_element,_property){_property=_property=="float"?pi.env.ie.toggle("styleFloat","cssFloat"):_property;if(_property=="opacity"&&pi.env.ie)return pi.util.Element.getOpacity(_element.style);return typeof _property=="string"?_element.style[_property]:_element.style},getView:function(_element,_property){var view=document.defaultView?document.defaultView.getComputedStyle(_element,null):_element.currentStyle;_property=_property=="float"?pi.env.ie.toggle("styleFloat","cssFloat"):_property;if(_property=="opacity"&&pi.env.ie)return pi.util.Element.getOpacity(_element,view);return typeof _property=="string"?view[_property]:view}},CloneObject:function(_object,_fn){var tmp={};for(var key in _object){if(pi.util.IsArray(_object[key])){tmp[key]=Array.prototype.clone.apply(_object[key])}else if(pi.util.IsHash(_object[key])){tmp[key]=pi.util.CloneObject(_object[key]);if(_fn)_fn.call(tmp,key,_object)}else tmp[key]=_object[key]}return tmp},MergeObjects:function(_object,_source){for(var key in _source){var value=_source[key];if(pi.util.IsArray(_source[key])){if(pi.util.IsArray(_object[key])){Array.prototype.push.apply(_source[key],_object[key])}else value=_source[key].clone()}else if(pi.util.IsHash(_source[key])){if(pi.util.IsHash(_object[key])){value=pi.util.MergeObjects(_object[key],_source[key])}else{value=pi.util.CloneObject(_source[key])}}_object[key]=value};return _object}};pi.get=function(){return document.getElementById(arguments[0])};pi.get.byTag=function(){return document.getElementsByTagName(arguments[0])};pi.get.byClass=function(){return document.getElementsByClassName.apply(document,arguments)};pi.base=function(){this.body={};this.constructor=null;this.build=function(_skipClonning){var base=this,skipClonning=_skipClonning||false,_private={},fn=function(){var _p=pi.util.CloneObject(_private);if(!skipClonning){for(var key in this){if(pi.util.IsArray(this[key])){this[key]=Array.prototype.clone.apply(this[key])}else if(pi.util.IsHash(this[key])){this[key]=pi.util.CloneObject(this[key],function(_key,_object){this[_key]._parent_=this});this[key]._parent_=this}}};base.createAccessors(_p,this);if(base.constructor)return base.constructor.apply(this,arguments);return this};this.movePrivateMembers(this.body,_private);if(this.constructor){fn["$Constructor"]=this.constructor}fn.prototype=this.body;return fn};this.createAccessors=function(_p,_branch){var getter=function(_property){return this[_property]},setter=function(_property,_value){this[_property]=_value;return _branch._parent_||_branch};for(var name in _p){var isPrivate=name.substring(0,1)=="_",title=name.substring(1,2).toUpperCase()+name.substring(2);if(isPrivate){_branch["get"+title]=getter.curry(_p,name);_branch["set"+title]=setter.curry(_p,name)}else if(pi.util.IsHash(_p[name])){if(!_branch[name])_branch[name]={};this.createAccessors(_p[name],_branch[name])}}};this.movePrivateMembers=function(_object,_branch){for(var name in _object){var isPrivate=name.substring(0,1)=="_";if(isPrivate){_branch[name]=_object[name];delete _object[name]}else if(pi.util.IsHash(_object[name])){_branch[name]={};this.movePrivateMembers(_object[name],_branch[name])}}}};Function.prototype.extend=function(_prototype,_skipClonning){var object=new pi.base,superClass=this;if(_prototype["$Constructor"]){object.constructor=_prototype["$Constructor"];delete _prototype["$Constructor"]};object.body=superClass==pi.base?_prototype:pi.util.MergeObjects(_prototype,superClass.prototype,2);object.constructor=object.constructor||function(){if(superClass!=pi.base)superClass.apply(this,arguments)};return object.build(_skipClonning)};Function.prototype.curry=function(_scope){var fn=this,scope=_scope||window,args=Array.prototype.slice.call(arguments,1);return function(){return fn.apply(scope,args.concat(Array.prototype.slice.call(arguments,0)))}};pi.element=pi.base.extend({"$Constructor":function(_tag){this.environment.setElement(document.createElement(_tag||"DIV"));this.environment.getElement().pi=this;return this},"clean":function(){var childs=this.child.get();while(childs.length){childs[0].parentNode.removeChild(childs[0])}},"clone":function(_deep){return this.environment.getElement().cloneNode(_deep)},"insert":function(_element){_element=_element.environment?_element.environment.getElement():_element;_element.appendChild(this.environment.getElement());return this},"insertAfter":function(_referenceElement){_referenceElement=_referenceElement.environment?_referenceElement.environment.getElement():_referenceElement;_referenceElement.nextSibling?this.insertBefore(_referenceElement.nextSibling):this.insert(_referenceElement.parentNode);return this},"insertBefore":function(_referenceElement){_referenceElement=_referenceElement.environment?_referenceElement.environment.getElement():_referenceElement;_referenceElement.parentNode.insertBefore(this.environment.getElement(),_referenceElement);return this},"query":function(_expression,_resultType,namespaceResolver,_result){return pi.xpath(_expression,_resultType||"ORDERED_NODE_SNAPSHOT_TYPE",this.environment.getElement(),_namespaceResolver,_result)},"remove":function(){this.environment.getParent().removeChild(this.environment.getElement())},"update":function(_value){["TEXTAREA","INPUT"].indexOf(this.environment.getName())>-1?(this.environment.getElement().value=_value):(this.environment.getElement().innerHTML=_value);return this},"attribute":{"getAll":function(_name){return this._parent_.environment.getElement().attributes},"clear":function(_name){this.set(_name,"");return this._parent_},"get":function(_name){return this._parent_.environment.getElement().getAttribute(_name)},"has":function(_name){return pi.env.ie?(this.get(_name)!=null):this._parent_.environment.getElement().hasAttribute(_name)},"remove":function(_name){this._parent_.environment.getElement().removeAttribute(_name);return this._parent_},"set":function(_name,_value){this._parent_.environment.getElement().setAttribute(_name,_value);return this._parent_},"addClass":function(_classes){for(var i=0;i-1)callback[i].fn.apply(this)}}};pi.xhr=pi.xhr.build();pi.xhr.get=function(_url,_returnPiObject){var request=new pi.xhr();request.environment.setAsync(false);request.environment.setUrl(_url);request.send();return _returnPiObject?request:request.environment.getApi()};pi.xpath=function(_expression,_resultType,_contextNode,_namespaceResolver,_result){var contextNode=_contextNode||document,expression=_expression||"",namespaceResolver=_namespaceResolver||null,result=_result||null,resultType=_resultType||"ANY_TYPE";return document.evaluate(expression,contextNode,namespaceResolver,XPathResult[resultType],result)};Array.prototype.clone=function(){var tmp=[];Array.prototype.push.apply(tmp,this);tmp.forEach(function(item,index,object){if(item instanceof Array)object[index]=object[index].clone()});return tmp};Array.prototype.count=function(_value){var count=0;this.forEach(function(){count+=Number(arguments[0]==_value)});return count};Array.prototype.forEach=Array.prototype.forEach||function(_function){for(var i=0;i9?87:48));return((this-remain)/_system).base(_system)+String.fromCharCode(remain+(remain>9?87:48))};Number.prototype.decimal=function(_system){var result=0,digit=String(this).split("");for(var i=0;i58)?digit[i].charCodeAt(0)-87:digit[i]);result+=digit[i]*(Math.pow(_system,digit.length-1-i))}return result};Number.prototype.range=function(_pattern){for(var value=String(this),isFloat=/\./i.test(value),i=isFloat.toggle(parseInt(value.split(".")[0]),0),end=parseInt(value.split(".")[isFloat.toggle(1,0)]),array=[];i=0;i--)str="\\u{0}{1}".format(String(obj[i].charCodeAt(0).base(16)).leftpad(4,"0"),str);return str};pi.util.AddEvent(pi.env.ie?window:document,pi.env.ie?"load":"DOMContentLoaded",function(){for(var i=0;i>>"));el.left.console.input=new pi.element("INPUT").attribute.set("type","text").attribute.addClass("Input").event.addListener("keydown",listen.consoleTextbox).insert(new pi.element("DIV").attribute.addClass("InputContainer").insert(el.left.console.container));el.right.console={};el.right.console.container=new pi.element("DIV").attribute.addClass("Console Container").insert(el.right.container);el.right.console.mlButton=new pi.element("A").attribute.addClass("MLButton CloseML").event.addListener("click",d.console.toggleML).insert(el.right.console.container);el.right.console.input=new pi.element("TEXTAREA").attribute.addClass("Input").insert(el.right.console.container);el.right.console.run=new pi.element("A").attribute.addClass("Button").event.addListener("click",listen.runMultiline).update("Run").insert(el.right.console.container);el.right.console.clear=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.clean.curry(window,el.right.console.input)).update("Clear").insert(el.right.console.container);el.button.console={};el.button.console.container=new pi.element("DIV").attribute.addClass("ButtonSet").insert(el.button.container);el.button.console.clear=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.clean.curry(window,el.left.console.monitor)).update("Clear").insert(el.button.console.container);el.left.html={};el.left.html.container=new pi.element("DIV").attribute.addClass("HTML").insert(el.left.container);el.right.html={};el.right.html.container=new pi.element("DIV").attribute.addClass("HTML Container").insert(el.right.container);el.right.html.nav={};el.right.html.nav.container=new pi.element("DIV").attribute.addClass("Nav").insert(el.right.html.container);el.right.html.nav.computedStyle=new pi.element("A").attribute.addClass("Tab Selected").event.addListener("click",d.html.navigate.curry(firebug,"computedStyle")).update("Computed Style").insert(el.right.html.nav.container);if(!pi.env.ie6)el.right.html.nav.dom=new pi.element("A").attribute.addClass("Tab").event.addListener("click",d.html.navigate.curry(firebug,"dom")).update("DOM").insert(el.right.html.nav.container);el.right.html.content=new pi.element("DIV").attribute.addClass("Content").insert(el.right.html.container);el.button.html={};el.button.html.container=new pi.element("DIV").attribute.addClass("ButtonSet HTML").insert(el.button.container);el.left.css={};el.left.css.container=new pi.element("DIV").attribute.addClass("CSS").insert(el.left.container);el.right.css={};el.right.css.container=new pi.element("DIV").attribute.addClass("CSS Container").insert(el.right.container);el.right.css.nav={};el.right.css.nav.container=new pi.element("DIV").attribute.addClass("Nav").insert(el.right.css.container);el.right.css.nav.runCSS=new pi.element("A").attribute.addClass("Tab Selected").update("Run CSS").insert(el.right.css.nav.container);el.right.css.mlButton=new pi.element("A").attribute.addClass("MLButton CloseML").event.addListener("click",d.console.toggleML).insert(el.right.css.container);el.right.css.input=new pi.element("TEXTAREA").attribute.addClass("Input").insert(el.right.css.container);el.right.css.run=new pi.element("A").attribute.addClass("Button").event.addListener("click",listen.runCSS).update("Run").insert(el.right.css.container);el.right.css.clear=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.clean.curry(window,el.right.css.input)).update("Clear").insert(el.right.css.container);el.button.css={};el.button.css.container=new pi.element("DIV").attribute.addClass("ButtonSet CSS").insert(el.button.container);el.button.css.selectbox=new pi.element("SELECT").event.addListener("change",listen.cssSelectbox).insert(el.button.css.container);el.left.scripts={};el.left.scripts.container=new pi.element("DIV").attribute.addClass("Scripts").insert(el.left.container);el.right.scripts={};el.right.scripts.container=new pi.element("DIV").attribute.addClass("Scripts Container").insert(el.right.container);el.button.scripts={};el.button.scripts.container=new pi.element("DIV").attribute.addClass("ButtonSet Scripts").insert(el.button.container);el.button.scripts.selectbox=new pi.element("SELECT").event.addListener("change",listen.scriptsSelectbox).insert(el.button.scripts.container);el.button.scripts.lineNumbers=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.scripts.toggleLineNumbers).update("Show Line Numbers").insert(el.button.scripts.container);el.left.dom={};el.left.dom.container=new pi.element("DIV").attribute.addClass("DOM").insert(el.left.container);el.right.dom={};el.right.dom.container=new pi.element("DIV").attribute.addClass("DOM Container").insert(el.right.container);el.button.dom={};el.button.dom.container=new pi.element("DIV").attribute.addClass("ButtonSet DOM").insert(el.button.container);el.button.dom.label=new pi.element("LABEL").update("Object Path:").insert(el.button.dom.container);el.button.dom.textbox=new pi.element("INPUT").event.addListener("keydown",listen.domTextbox).update("window").insert(el.button.dom.container);el.left.str={};el.left.str.container=new pi.element("DIV").attribute.addClass("STR").insert(el.left.container);el.right.str={};el.right.str.container=new pi.element("DIV").attribute.addClass("STR").insert(el.left.container);el.button.str={};el.button.str.container=new pi.element("DIV").attribute.addClass("ButtonSet XHR").insert(el.button.container);el.button.str.watch=new pi.element("A").attribute.addClass("Button").event.addListener("click",d.navigate.curry(window,"xhr")).update("Back").insert(el.button.str.container);el.left.xhr={};el.left.xhr.container=new pi.element("DIV").attribute.addClass("XHR").insert(el.left.container);el.right.xhr={};el.right.xhr.container=new pi.element("DIV").attribute.addClass("XHR").insert(el.left.container);el.button.xhr={};el.button.xhr.container=new pi.element("DIV").attribute.addClass("ButtonSet XHR").insert(el.button.container);el.button.xhr.label=new pi.element("LABEL").update("XHR Path:").insert(el.button.xhr.container);el.button.xhr.textbox=new pi.element("INPUT").event.addListener("keydown",listen.xhrTextbox).insert(el.button.xhr.container);el.button.xhr.watch=new pi.element("A").attribute.addClass("Button").event.addListener("click",listen.addXhrObject).update("Watch").insert(el.button.xhr.container);if(pi.env.ie6){var buttons=[el.button.inspect,el.button.close,el.button.inspect,el.button.console.clear,el.right.console.run,el.right.console.clear,el.right.css.run,el.right.css.clear];for(var i=0;i>> console.dir("+_value+")");d.dom.open(_value,d.console.addLine())}},addLine:function(){with(firebug){return new pi.element("DIV").attribute.addClass("Row").insert(el.left.console.monitor)}},openObject:function(_index){with(firebug){d.dom.open(env.objCn[_index],el.left.dom.container,pi.env.ie);d.navigate("dom")}},historyIndex:0,history:[],log:function(_values){with(firebug){if(env.init==false){env.ctmp.push(arguments);return}var value="";for(var i=0;i0?" ":"")+d.highlight(arguments[i],false,false,true)}d.console.addLine().update(value);d.console.scroll()}},print:function(_cmd,_text){with(firebug){d.console.addLine().attribute.addClass("Arrow").update(">>> "+_cmd);d.console.addLine().update(d.highlight(_text,false,false,true));d.console.scroll();d.console.historyIndex=d.console.history.push(_cmd)}},run:function(cmd){with(firebug){if(cmd.length==0)return;el.left.console.input.environment.getElement().value="";try{var result=eval.call(window,cmd);d.console.print(cmd,result)}catch(e){d.console.addLine().attribute.addClass("Arrow").update(">>> "+cmd);if(!pi.env.ff){d.console.scroll();return d.console.addLine().attribute.addClass("Error").update("Error: "+(e.description||e),true)}if(e.fileName==null){d.console.addLine().attribute.addClass("Error").update("Error: "+e.message,true)}var fileName=e.fileName.split("\/").getLastItem();d.console.addLine().attribute.addClass("Error").update("Error: "+e.message+" ("+fileName+","+e.lineNumber+")",true);d.console.scroll()}d.console.scroll()}},scroll:function(){with(firebug){el.left.console.monitor.environment.getElement().parentNode.scrollTop=Math.abs(el.left.console.monitor.environment.getSize().offsetHeight-200)}},toggleML:function(){with(firebug){var open=!env.ml;env.ml=!env.ml;d.navigateRightColumn("console",open);el[open?"left":"right"].console.mlButton.environment.addStyle({display:"none"});el[!open?"left":"right"].console.mlButton.environment.addStyle({display:"block"});el.left.console.monitor.environment.addStyle({"height":(open?233:210)+"px"});el.left.console.mlButton.attribute[(open?"add":"remove")+"Class"]("CloseML")}}},css:{index:-1,open:function(_index){with(firebug){var item=document.styleSheets[_index];var uri=item.href;if(uri.indexOf("http:\/\/")>-1&&getDomain(uri)!=document.domain){el.left.css.container.update("Access to restricted URI denied");return}var rules=item[pi.env.ie?"rules":"cssRules"];var str="";for(var i=0;i";for(var i=0;i<_css.length;i++){var item=_css[i];str+="
"+item.replace(/(.+\:)(.+)/,"$1$2;")+"
"}str+="
}
";return str}},refresh:function(){with(firebug){el.button.css.selectbox.update("");var collection=document.styleSheets;for(var i=0;i-1){if(_value==null){return"null"}if(["boolean","number"].indexOf(typeof _value)>-1){return""+_value+""}if(typeof _value=="function"){return"function()"}return"\""+(!_inObject&&!_inArray?_value:_value.substring(0,35)).replace(/\n/g,"\\n").replace(/\s/g," ").replace(/>/g,">").replace(/"}else if(isElement){if(_value.nodeType==3)return d.highlight(_value.nodeValue);if(_inArray||_inObject){var result=""+_value.nodeName.toLowerCase();if(_value.getAttribute&&_value.getAttribute("id"))result+="#"+_value.getAttribute("id")+"";var elClass=_value.getAttribute?_value.getAttribute(pi.env.ie?"className":"class"):"";if(elClass)result+="."+elClass.split(" ")[0]+"";return result+""}var result="<"+_value.nodeName.toLowerCase()+"";if(_value.attributes)for(var i=0;i<_value.attributes.length;i++){var item=_value.attributes[i];if(pi.env.ie&&Boolean(item.nodeValue)==false)continue;result+=" "+item.nodeName+"=\""+item.nodeValue+"\""}result+=">";return result}else if(isArray||["object","array"].indexOf(typeof _value)>-1){var result="";if(isArray||_value instanceof Array){if(_inObject)return"["+_value.length+"]";result+="[ ";for(var i=0;i<_value.length;i++){if((_inObject||_inArray)&&pi.env.ie&&i>3)break;result+=(i>0?", ":"")+d.highlight(_value[i],false,true,true)}result+=" ]";return result}if(_inObject)return"Object";result+="Object";var i=0;for(var key in _value){var value=_value[key];if((_inObject||_inArray)&&pi.env.ie&&i>3)break;result+=" "+key+"="+d.highlight(value,true);i++};result+="";return result}else{if(_inObject)return""+_value+"";return _value}}},html:{nIndex:"computedStyle",current:null,highlight:function(_element,_clear,_event){with(firebug){if(_clear){el.bgInspector.environment.addStyle({"display":"none"});return}d.inspector.inspect(_element,true)}},inspect:function(_element){var el=_element,map=[],parent=_element;while(parent){map.push(parent);if(parent==document.body)break;parent=parent.parentNode}map=map.reverse();with(firebug){d.inspector.toggle();var parentLayer=el.left.html.container.child.get()[1].childNodes[1].pi;for(var t=0;map[t];){if(t==map.length-1){var link=parentLayer.environment.getElement().previousSibling.pi;link.attribute.addClass("Selected");if(d.html.current)d.html.current[1].attribute.removeClass("Selected");d.html.current=[_element,link];return;t}parentLayer=d.html.openHtmlTree(map[t],parentLayer,map[t+1]);t++}}},navigate:function(_index,_element){with(firebug){el.right.html.nav[d.html.nIndex].attribute.removeClass("Selected");el.right.html.nav[_index].attribute.addClass("Selected");d.html.nIndex=_index;d.html.openProperties()}},openHtmlTree:function(_element,_parent,_returnParentElementByElement,_event){with(firebug){var element=_element||document.documentElement,parent=_parent||el.left.html.container,returnParentEl=_returnParentElementByElement||null,returnParentVal=null;if(parent!=el.left.html.container){var nodeLink=parent.environment.getParent().pi.child.get()[0].pi;if(d.html.current)d.html.current[1].attribute.removeClass("Selected");nodeLink.attribute.addClass("Selected");d.html.current=[_element,nodeLink];d.html.openProperties()}if(element.childNodes&&(element.childNodes.length==0||(element.childNodes.length==1&&element.childNodes[0].nodeType==3)))return;parent.clean();if(parent.opened&&Boolean(_returnParentElementByElement)==false){parent.opened=false;parent.environment.getParent().pi.child.get()[0].pi.attribute.removeClass("Open");return}if(parent!=el.left.html.container){parent.environment.getParent().pi.child.get()[0].pi.attribute.addClass("Open");parent.opened=true}for(var i=0;i"));continue}else if(item.childNodes&&item.childNodes.length==0)continue;link.attribute.addClass("ParentLink")}return returnParentVal}},openProperties:function(){with(firebug){var index=d.html.nIndex;var node=d.html.current[0];d.clean(el.right.html.content);var str="";switch(index){case"computedStyle":var property=["opacity","filter","azimuth","background","backgroundAttachment","backgroundColor","backgroundImage","backgroundPosition","backgroundRepeat","border","borderCollapse","borderColor","borderSpacing","borderStyle","borderTop","borderRight","borderBottom","borderLeft","borderTopColor","borderRightColor","borderBottomColor","borderLeftColor","borderTopStyle","borderRightStyle","borderBottomStyle","borderLeftStyle","borderTopWidth","borderRightWidth","borderBottomWidth","borderLeftWidth","borderWidth","bottom","captionSide","clear","clip","color","content","counterIncrement","counterReset","cue","cueAfter","cueBefore","cursor","direction","display","elevation","emptyCells","cssFloat","font","fontFamily","fontSize","fontSizeAdjust","fontStretch","fontStyle","fontVariant","fontWeight","height","left","letterSpacing","lineHeight","listStyle","listStyleImage","listStylePosition","listStyleType","margin","marginTop","marginRight","marginBottom","marginLeft","markerOffset","marks","maxHeight","maxWidth","minHeight","minWidth","orphans","outline","outlineColor","outlineStyle","outlineWidth","overflow","padding","paddingTop","paddingRight","paddingBottom","paddingLeft","page","pageBreakAfter","pageBreakBefore","pageBreakInside","pause","pauseAfter","pauseBefore","pitch","pitchRange","playDuring","position","quotes","richness","right","size","speak","speakHeader","speakNumeral","speakPunctuation","speechRate","stress","tableLayout","textAlign","textDecoration","textIndent","textShadow","textTransform","top","unicodeBidi","verticalAlign","visibility","voiceFamily","volume","whiteSpace","widows","width","wordSpacing","zIndex"].sort();var view=document.defaultView?document.defaultView.getComputedStyle(node,null):node.currentStyle;for(var i=0;i
"+item+"
"+d.highlight(view[item])+"
"}el.right.html.content.update(str);break;case"dom":d.dom.open(node,el.right.html.content,pi.env.ie);break}}}},inspector:{enabled:false,el:null,inspect:function(_element,_bgInspector){var el=_element,top=el.offsetTop,left=el.offsetLeft,parent=_element.offsetParent;while(Boolean(parent)&&parent!=document.firstChild){top+=parent.offsetTop;left+=parent.offsetLeft;parent=parent.offsetParent;if(parent==document.body)break};with(firebug){el[_bgInspector?"bgInspector":"borderInspector"].environment.addStyle({"width":_element.offsetWidth+"px","height":_element.offsetHeight+"px","top":top-(_bgInspector?0:2)+"px","left":left-(_bgInspector?0:2)+"px","display":"block"});if(!_bgInspector){d.inspector.el=_element}}},toggle:function(){with(firebug){d.inspector.enabled=!d.inspector.enabled;el.button.inspect.attribute[(d.inspector.enabled?"add":"remove")+"Class"]("Enabled");if(d.inspector.enabled==false){el.borderInspector.environment.addStyle({"display":"none"});d.inspector.el=null}else if(pi.env.dIndex!="html"){d.navigate("html")}}}},scripts:{index:-1,lineNumbers:false,open:function(_index){with(firebug){d.scripts.index=_index;el.left.scripts.container.update("");var script=document.getElementsByTagName("script")[_index],uri=script.src||document.location.href,source;if(uri.indexOf("http:\/\/")>-1&&getDomain(uri)!=document.domain){el.left.scripts.container.update("Access to restricted URI denied");return}if(uri!=document.location.href){source=env.cache[uri]||pi.xhr.get(uri).responseText;env.cache[uri]=source}else source=script.innerHTML;source=source.replace(/\n|\t|<|>/g,function(_ch){return({"<":"<",">":">","\t":"        ","\n":"
"})[_ch]});if(!d.scripts.lineNumbers)el.left.scripts.container.child.add(new pi.element("DIV").attribute.addClass("CodeContainer").update(source));else{source=source.split("
");for(var i=0;i"))}}},xhr:{objects:[],addObject:function(){with(firebug){for(var i=0;id.console.historyIndex?d.console.history[d.console.historyIndex]:"")}},cssSelectbox:function(){with(firebug){d.css.open(el.button.css.selectbox.environment.getElement().selectedIndex)}},domTextbox:function(_event){with(firebug){if(_event.keyCode==13){d.dom.open(eval(el.button.dom.textbox.environment.getElement().value),el.left.dom.container)}}},inspector:function(){with(firebug){d.html.inspect(d.inspector.el)}},keyboard:function(_event){with(firebug){if(_event.keyCode==27&&d.inspector.enabled)d.inspector.toggle()}},mouse:function(_event){with(firebug){var target=_event[pi.env.ie?"srcElement":"target"];if(d.inspector.enabled&&target!=document.body&&target!=document.firstChild&&target!=document.childNodes[1]&&target!=el.borderInspector.environment.getElement()&&target!=el.main.environment.getElement()&&target.offsetParent!=el.main.environment.getElement())d.inspector.inspect(target)}},runMultiline:function(){with(firebug){d.console.run.call(window,el.right.console.input.environment.getElement().value)}},runCSS:function(){with(firebug){var source=el.right.css.input.environment.getElement().value.replace(/\n|\t/g,"").split("}");for(var i=0;i0?collection[0]:document.body.appendChild(document.createElement("style"));if(!item.match(/.+\{.+\}/))continue;if(pi.env.ie)style.styleSheet.addRule(rule[0],rule[1]);else style.sheet.insertRule(rule,style.sheet.cssRules.length)}}},scriptsSelectbox:function(){with(firebug){d.scripts.open(parseInt(el.button.scripts.selectbox.environment.getElement().value))}},xhrTextbox:function(_event){with(firebug){if(_event.keyCode==13){d.xhr.addObject.apply(window,el.button.xhr.textbox.environment.getElement().value.split(","))}}}}};window.console=firebug.d.console;pi.util.AddEvent(window,"resize",firebug.d.refreshSize);pi.util.AddEvent(document,"mousemove",firebug.listen.mouse);pi.util.AddEvent(document,"keydown",firebug.listen.keyboard);pi.util.DOMContentLoaded.push(firebug.init); \ No newline at end of file diff --git a/ui/lib/jquery.js b/ui/lib/jquery.js deleted file mode 100755 index 88e661e..0000000 --- a/ui/lib/jquery.js +++ /dev/null @@ -1,3549 +0,0 @@ -(function(){ -/* - * jQuery 1.2.6 - New Wave Javascript - * - * Copyright (c) 2008 John Resig (jquery.com) - * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. - * - * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $ - * $Rev: 5685 $ - */ - -// Map over jQuery in case of overwrite -var _jQuery = window.jQuery, -// Map over the $ in case of overwrite - _$ = window.$; - -var jQuery = window.jQuery = window.$ = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context ); -}; - -// A simple way to check for HTML strings or ID strings -// (both of which we optimize for) -var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/, - -// Is it a simple selector - isSimple = /^.[^:#\[\.]*$/, - -// Will speed up references to undefined, and allows munging its name. - undefined; - -jQuery.fn = jQuery.prototype = { - init: function( selector, context ) { - // Make sure that a selection was provided - selector = selector || document; - - // Handle $(DOMElement) - if ( selector.nodeType ) { - this[0] = selector; - this.length = 1; - return this; - } - // Handle HTML strings - if ( typeof selector == "string" ) { - // Are we dealing with HTML string or an ID? - var match = quickExpr.exec( selector ); - - // Verify a match, and that no context was specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) - selector = jQuery.clean( [ match[1] ], context ); - - // HANDLE: $("#id") - else { - var elem = document.getElementById( match[3] ); - - // Make sure an element was located - if ( elem ){ - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id != match[3] ) - return jQuery().find( selector ); - - // Otherwise, we inject the element directly into the jQuery object - return jQuery( elem ); - } - selector = []; - } - - // HANDLE: $(expr, [context]) - // (which is just equivalent to: $(content).find(expr) - } else - return jQuery( context ).find( selector ); - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) - return jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector ); - - return this.setArray(jQuery.makeArray(selector)); - }, - - // The current version of jQuery being used - jquery: "1.2.6", - - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - - // The number of elements contained in the matched element set - length: 0, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num == undefined ? - - // Return a 'clean' array - jQuery.makeArray( this ) : - - // Return just the object - this[ num ]; - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - // Build a new jQuery matched element set - var ret = jQuery( elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - // Return the newly-formed element set - return ret; - }, - - // Force the current matched set of elements to become - // the specified array of elements (destroying the stack in the process) - // You should use pushStack() in order to do this, but maintain the stack - setArray: function( elems ) { - // Resetting the length to 0, then using the native Array push - // is a super-fast way to populate an object with array-like properties - this.length = 0; - Array.prototype.push.apply( this, elems ); - - return this; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - var ret = -1; - - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem && elem.jquery ? elem[0] : elem - , this ); - }, - - attr: function( name, value, type ) { - var options = name; - - // Look for the case where we're accessing a style value - if ( name.constructor == String ) - if ( value === undefined ) - return this[0] && jQuery[ type || "attr" ]( this[0], name ); - - else { - options = {}; - options[ name ] = value; - } - - // Check to see if we're setting style values - return this.each(function(i){ - // Set all the styles - for ( name in options ) - jQuery.attr( - type ? - this.style : - this, - name, jQuery.prop( this, options[ name ], type, i, name ) - ); - }); - }, - - css: function( key, value ) { - // ignore negative width and height values - if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) - value = undefined; - return this.attr( key, value, "curCSS" ); - }, - - text: function( text ) { - if ( typeof text != "object" && text != null ) - return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); - - var ret = ""; - - jQuery.each( text || this, function(){ - jQuery.each( this.childNodes, function(){ - if ( this.nodeType != 8 ) - ret += this.nodeType != 1 ? - this.nodeValue : - jQuery.fn.text( [ this ] ); - }); - }); - - return ret; - }, - - wrapAll: function( html ) { - if ( this[0] ) - // The elements to wrap the target around - jQuery( html, this[0].ownerDocument ) - .clone() - .insertBefore( this[0] ) - .map(function(){ - var elem = this; - - while ( elem.firstChild ) - elem = elem.firstChild; - - return elem; - }) - .append(this); - - return this; - }, - - wrapInner: function( html ) { - return this.each(function(){ - jQuery( this ).contents().wrapAll( html ); - }); - }, - - wrap: function( html ) { - return this.each(function(){ - jQuery( this ).wrapAll( html ); - }); - }, - - append: function() { - return this.domManip(arguments, true, false, function(elem){ - if (this.nodeType == 1) - this.appendChild( elem ); - }); - }, - - prepend: function() { - return this.domManip(arguments, true, true, function(elem){ - if (this.nodeType == 1) - this.insertBefore( elem, this.firstChild ); - }); - }, - - before: function() { - return this.domManip(arguments, false, false, function(elem){ - this.parentNode.insertBefore( elem, this ); - }); - }, - - after: function() { - return this.domManip(arguments, false, true, function(elem){ - this.parentNode.insertBefore( elem, this.nextSibling ); - }); - }, - - end: function() { - return this.prevObject || jQuery( [] ); - }, - - find: function( selector ) { - var elems = jQuery.map(this, function(elem){ - return jQuery.find( selector, elem ); - }); - - return this.pushStack( /[^+>] [^+>]/.test( selector ) || selector.indexOf("..") > -1 ? - jQuery.unique( elems ) : - elems ); - }, - - clone: function( events ) { - // Do the clone - var ret = this.map(function(){ - if ( jQuery.browser.msie && !jQuery.isXMLDoc(this) ) { - // IE copies events bound via attachEvent when - // using cloneNode. Calling detachEvent on the - // clone will also remove the events from the orignal - // In order to get around this, we use innerHTML. - // Unfortunately, this means some modifications to - // attributes in IE that are actually only stored - // as properties will not be copied (such as the - // the name attribute on an input). - var clone = this.cloneNode(true), - container = document.createElement("div"); - container.appendChild(clone); - return jQuery.clean([container.innerHTML])[0]; - } else - return this.cloneNode(true); - }); - - // Need to set the expando to null on the cloned set if it exists - // removeData doesn't work here, IE removes it from the original as well - // this is primarily for IE but the data expando shouldn't be copied over in any browser - var clone = ret.find("*").andSelf().each(function(){ - if ( this[ expando ] != undefined ) - this[ expando ] = null; - }); - - // Copy the events from the original to the clone - if ( events === true ) - this.find("*").andSelf().each(function(i){ - if (this.nodeType == 3) - return; - var events = jQuery.data( this, "events" ); - - for ( var type in events ) - for ( var handler in events[ type ] ) - jQuery.event.add( clone[ i ], type, events[ type ][ handler ], events[ type ][ handler ].data ); - }); - - // Return the cloned set - return ret; - }, - - filter: function( selector ) { - return this.pushStack( - jQuery.isFunction( selector ) && - jQuery.grep(this, function(elem, i){ - return selector.call( elem, i ); - }) || - - jQuery.multiFilter( selector, this ) ); - }, - - not: function( selector ) { - if ( selector.constructor == String ) - // test special case where just one selector is passed in - if ( isSimple.test( selector ) ) - return this.pushStack( jQuery.multiFilter( selector, this, true ) ); - else - selector = jQuery.multiFilter( selector, this ); - - var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; - return this.filter(function() { - return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; - }); - }, - - add: function( selector ) { - return this.pushStack( jQuery.unique( jQuery.merge( - this.get(), - typeof selector == 'string' ? - jQuery( selector ) : - jQuery.makeArray( selector ) - ))); - }, - - is: function( selector ) { - return !!selector && jQuery.multiFilter( selector, this ).length > 0; - }, - - hasClass: function( selector ) { - return this.is( "." + selector ); - }, - - val: function( value ) { - if ( value == undefined ) { - - if ( this.length ) { - var elem = this[0]; - - // We need to handle select boxes special - if ( jQuery.nodeName( elem, "select" ) ) { - var index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type == "select-one"; - - // Nothing was selected - if ( index < 0 ) - return null; - - // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; - - if ( option.selected ) { - // Get the specifc value for the option - value = jQuery.browser.msie && !option.attributes.value.specified ? option.text : option.value; - - // We don't need an array for one selects - if ( one ) - return value; - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - - // Everything else, we just grab the value - } else - return (this[0].value || "").replace(/\r/g, ""); - - } - - return undefined; - } - - if( value.constructor == Number ) - value += ''; - - return this.each(function(){ - if ( this.nodeType != 1 ) - return; - - if ( value.constructor == Array && /radio|checkbox/.test( this.type ) ) - this.checked = (jQuery.inArray(this.value, value) >= 0 || - jQuery.inArray(this.name, value) >= 0); - - else if ( jQuery.nodeName( this, "select" ) ) { - var values = jQuery.makeArray(value); - - jQuery( "option", this ).each(function(){ - this.selected = (jQuery.inArray( this.value, values ) >= 0 || - jQuery.inArray( this.text, values ) >= 0); - }); - - if ( !values.length ) - this.selectedIndex = -1; - - } else - this.value = value; - }); - }, - - html: function( value ) { - return value == undefined ? - (this[0] ? - this[0].innerHTML : - null) : - this.empty().append( value ); - }, - - replaceWith: function( value ) { - return this.after( value ).remove(); - }, - - eq: function( i ) { - return this.slice( i, i + 1 ); - }, - - slice: function() { - return this.pushStack( Array.prototype.slice.apply( this, arguments ) ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function(elem, i){ - return callback.call( elem, i, elem ); - })); - }, - - andSelf: function() { - return this.add( this.prevObject ); - }, - - data: function( key, value ){ - var parts = key.split("."); - parts[1] = parts[1] ? "." + parts[1] : ""; - - if ( value === undefined ) { - var data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); - - if ( data === undefined && this.length ) - data = jQuery.data( this[0], key ); - - return data === undefined && parts[1] ? - this.data( parts[0] ) : - data; - } else - return this.trigger("setData" + parts[1] + "!", [parts[0], value]).each(function(){ - jQuery.data( this, key, value ); - }); - }, - - removeData: function( key ){ - return this.each(function(){ - jQuery.removeData( this, key ); - }); - }, - - domManip: function( args, table, reverse, callback ) { - var clone = this.length > 1, elems; - - return this.each(function(){ - if ( !elems ) { - elems = jQuery.clean( args, this.ownerDocument ); - - if ( reverse ) - elems.reverse(); - } - - var obj = this; - - if ( table && jQuery.nodeName( this, "table" ) && jQuery.nodeName( elems[0], "tr" ) ) - obj = this.getElementsByTagName("tbody")[0] || this.appendChild( this.ownerDocument.createElement("tbody") ); - - var scripts = jQuery( [] ); - - jQuery.each(elems, function(){ - var elem = clone ? - jQuery( this ).clone( true )[0] : - this; - - // execute all scripts after the elements have been injected - if ( jQuery.nodeName( elem, "script" ) ) - scripts = scripts.add( elem ); - else { - // Remove any inner scripts for later evaluation - if ( elem.nodeType == 1 ) - scripts = scripts.add( jQuery( "script", elem ).remove() ); - - // Inject the elements into the document - callback.call( obj, elem ); - } - }); - - scripts.each( evalScript ); - }); - } -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -function evalScript( i, elem ) { - if ( elem.src ) - jQuery.ajax({ - url: elem.src, - async: false, - dataType: "script" - }); - - else - jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); - - if ( elem.parentNode ) - elem.parentNode.removeChild( elem ); -} - -function now(){ - return +new Date; -} - -jQuery.extend = jQuery.fn.extend = function() { - // copy reference to target object - var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; - - // Handle a deep copy situation - if ( target.constructor == Boolean ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target != "object" && typeof target != "function" ) - target = {}; - - // extend jQuery itself if only one argument is passed - if ( length == i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) - // Extend the base object - for ( var name in options ) { - var src = target[ name ], copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) - continue; - - // Recurse if we're merging object values - if ( deep && copy && typeof copy == "object" && !copy.nodeType ) - target[ name ] = jQuery.extend( deep, - // Never move original objects, clone them - src || ( copy.length != null ? [ ] : { } ) - , copy ); - - // Don't bring in undefined values - else if ( copy !== undefined ) - target[ name ] = copy; - - } - - // Return the modified object - return target; -}; - -var expando = "jQuery" + now(), uuid = 0, windowData = {}, - // exclude the following css properties to add px - exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, - // cache defaultView - defaultView = document.defaultView || {}; - -jQuery.extend({ - noConflict: function( deep ) { - window.$ = _$; - - if ( deep ) - window.jQuery = _jQuery; - - return jQuery; - }, - - // See test/unit/core.js for details concerning this function. - isFunction: function( fn ) { - return !!fn && typeof fn != "string" && !fn.nodeName && - fn.constructor != Array && /^[\s[]?function/.test( fn + "" ); - }, - - // check if an element is in a (or is an) XML document - isXMLDoc: function( elem ) { - return elem.documentElement && !elem.body || - elem.tagName && elem.ownerDocument && !elem.ownerDocument.body; - }, - - // Evalulates a script in a global context - globalEval: function( data ) { - data = jQuery.trim( data ); - - if ( data ) { - // Inspired by code by Andrea Giammarchi - // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html - var head = document.getElementsByTagName("head")[0] || document.documentElement, - script = document.createElement("script"); - - script.type = "text/javascript"; - if ( jQuery.browser.msie ) - script.text = data; - else - script.appendChild( document.createTextNode( data ) ); - - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709). - head.insertBefore( script, head.firstChild ); - head.removeChild( script ); - } - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); - }, - - cache: {}, - - data: function( elem, name, data ) { - elem = elem == window ? - windowData : - elem; - - var id = elem[ expando ]; - - // Compute a unique ID for the element - if ( !id ) - id = elem[ expando ] = ++uuid; - - // Only generate the data cache if we're - // trying to access or manipulate it - if ( name && !jQuery.cache[ id ] ) - jQuery.cache[ id ] = {}; - - // Prevent overriding the named cache with undefined values - if ( data !== undefined ) - jQuery.cache[ id ][ name ] = data; - - // Return the named cache data, or the ID for the element - return name ? - jQuery.cache[ id ][ name ] : - id; - }, - - removeData: function( elem, name ) { - elem = elem == window ? - windowData : - elem; - - var id = elem[ expando ]; - - // If we want to remove a specific section of the element's data - if ( name ) { - if ( jQuery.cache[ id ] ) { - // Remove the section of cache data - delete jQuery.cache[ id ][ name ]; - - // If we've removed all the data, remove the element's cache - name = ""; - - for ( name in jQuery.cache[ id ] ) - break; - - if ( !name ) - jQuery.removeData( elem ); - } - - // Otherwise, we want to remove all of the element's data - } else { - // Clean up the element expando - try { - delete elem[ expando ]; - } catch(e){ - // IE has trouble directly removing the expando - // but it's ok with using removeAttribute - if ( elem.removeAttribute ) - elem.removeAttribute( expando ); - } - - // Completely remove the data cache - delete jQuery.cache[ id ]; - } - }, - - // args is for internal usage only - each: function( object, callback, args ) { - var name, i = 0, length = object.length; - - if ( args ) { - if ( length == undefined ) { - for ( name in object ) - if ( callback.apply( object[ name ], args ) === false ) - break; - } else - for ( ; i < length; ) - if ( callback.apply( object[ i++ ], args ) === false ) - break; - - // A special, fast, case for the most common use of each - } else { - if ( length == undefined ) { - for ( name in object ) - if ( callback.call( object[ name ], name, object[ name ] ) === false ) - break; - } else - for ( var value = object[0]; - i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} - } - - return object; - }, - - prop: function( elem, value, type, i, name ) { - // Handle executable functions - if ( jQuery.isFunction( value ) ) - value = value.call( elem, i ); - - // Handle passing in a number to a CSS property - return value && value.constructor == Number && type == "curCSS" && !exclude.test( name ) ? - value + "px" : - value; - }, - - className: { - // internal only, use addClass("class") - add: function( elem, classNames ) { - jQuery.each((classNames || "").split(/\s+/), function(i, className){ - if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) - elem.className += (elem.className ? " " : "") + className; - }); - }, - - // internal only, use removeClass("class") - remove: function( elem, classNames ) { - if (elem.nodeType == 1) - elem.className = classNames != undefined ? - jQuery.grep(elem.className.split(/\s+/), function(className){ - return !jQuery.className.has( classNames, className ); - }).join(" ") : - ""; - }, - - // internal only, use hasClass("class") - has: function( elem, className ) { - return jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; - } - }, - - // A method for quickly swapping in/out CSS properties to get correct calculations - swap: function( elem, options, callback ) { - var old = {}; - // Remember the old values, and insert the new ones - for ( var name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - callback.call( elem ); - - // Revert the old values - for ( var name in options ) - elem.style[ name ] = old[ name ]; - }, - - css: function( elem, name, force ) { - if ( name == "width" || name == "height" ) { - var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; - - function getWH() { - val = name == "width" ? elem.offsetWidth : elem.offsetHeight; - var padding = 0, border = 0; - jQuery.each( which, function() { - padding += parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; - border += parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; - }); - val -= Math.round(padding + border); - } - - if ( jQuery(elem).is(":visible") ) - getWH(); - else - jQuery.swap( elem, props, getWH ); - - return Math.max(0, val); - } - - return jQuery.curCSS( elem, name, force ); - }, - - curCSS: function( elem, name, force ) { - var ret, style = elem.style; - - // A helper method for determining if an element's values are broken - function color( elem ) { - if ( !jQuery.browser.safari ) - return false; - - // defaultView is cached - var ret = defaultView.getComputedStyle( elem, null ); - return !ret || ret.getPropertyValue("color") == ""; - } - - // We need to handle opacity special in IE - if ( name == "opacity" && jQuery.browser.msie ) { - ret = jQuery.attr( style, "opacity" ); - - return ret == "" ? - "1" : - ret; - } - // Opera sometimes will give the wrong display answer, this fixes it, see #2037 - if ( jQuery.browser.opera && name == "display" ) { - var save = style.outline; - style.outline = "0 solid black"; - style.outline = save; - } - - // Make sure we're using the right name for getting the float value - if ( name.match( /float/i ) ) - name = styleFloat; - - if ( !force && style && style[ name ] ) - ret = style[ name ]; - - else if ( defaultView.getComputedStyle ) { - - // Only "float" is needed here - if ( name.match( /float/i ) ) - name = "float"; - - name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); - - var computedStyle = defaultView.getComputedStyle( elem, null ); - - if ( computedStyle && !color( elem ) ) - ret = computedStyle.getPropertyValue( name ); - - // If the element isn't reporting its values properly in Safari - // then some display: none elements are involved - else { - var swap = [], stack = [], a = elem, i = 0; - - // Locate all of the parent display: none elements - for ( ; a && color(a); a = a.parentNode ) - stack.unshift(a); - - // Go through and make them visible, but in reverse - // (It would be better if we knew the exact display type that they had) - for ( ; i < stack.length; i++ ) - if ( color( stack[ i ] ) ) { - swap[ i ] = stack[ i ].style.display; - stack[ i ].style.display = "block"; - } - - // Since we flip the display style, we have to handle that - // one special, otherwise get the value - ret = name == "display" && swap[ stack.length - 1 ] != null ? - "none" : - ( computedStyle && computedStyle.getPropertyValue( name ) ) || ""; - - // Finally, revert the display styles back - for ( i = 0; i < swap.length; i++ ) - if ( swap[ i ] != null ) - stack[ i ].style.display = swap[ i ]; - } - - // We should always get a number back from opacity - if ( name == "opacity" && ret == "" ) - ret = "1"; - - } else if ( elem.currentStyle ) { - var camelCase = name.replace(/\-(\w)/g, function(all, letter){ - return letter.toUpperCase(); - }); - - ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; - - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 - - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { - // Remember the original values - var left = style.left, rsLeft = elem.runtimeStyle.left; - - // Put in the new values to get a computed value out - elem.runtimeStyle.left = elem.currentStyle.left; - style.left = ret || 0; - ret = style.pixelLeft + "px"; - - // Revert the changed values - style.left = left; - elem.runtimeStyle.left = rsLeft; - } - } - - return ret; - }, - - clean: function( elems, context ) { - var ret = []; - context = context || document; - // !context.createElement fails in IE with an error but returns typeof 'object' - if (typeof context.createElement == 'undefined') - context = context.ownerDocument || context[0] && context[0].ownerDocument || document; - - jQuery.each(elems, function(i, elem){ - if ( !elem ) - return; - - if ( elem.constructor == Number ) - elem += ''; - - // Convert html string into DOM nodes - if ( typeof elem == "string" ) { - // Fix "XHTML"-style tags in all browsers - elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ - return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? - all : - front + ">"; - }); - - // Trim whitespace, otherwise indexOf won't work as expected - var tags = jQuery.trim( elem ).toLowerCase(), div = context.createElement("div"); - - var wrap = - // option or optgroup - !tags.indexOf("", "" ] || - - !tags.indexOf("", "" ] || - - tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && - [ 1, "", "
" ] || - - !tags.indexOf("", "" ] || - - // matched above - (!tags.indexOf("", "" ] || - - !tags.indexOf("", "" ] || - - // IE can't serialize and