From 21aa5c761a53e255d85d7666ece26a804f0b081e Mon Sep 17 00:00:00 2001 From: yomguy <> Date: Tue, 4 Dec 2007 10:20:38 +0000 Subject: [PATCH] Add exporters and begin JACK api --- etc/pre-barreau_courses.xml | 1 - export/api.py | 87 ++++++++++++++++++ export/core.py | 179 ++++++++++++++++++++++++++++++++++++ export/mp3.py | 155 +++++++++++++++++++++++++++++++ export/ogg.py | 135 +++++++++++++++++++++++++++ export/tools.py | 96 +++++++++++++++++++ jack/jack.py | 66 +++++++++++++ teleoddcast.py | 58 +++++++++++- 8 files changed, 772 insertions(+), 5 deletions(-) create mode 100644 export/api.py create mode 100644 export/core.py create mode 100644 export/mp3.py create mode 100644 export/ogg.py create mode 100644 export/tools.py create mode 100644 jack/jack.py diff --git a/etc/pre-barreau_courses.xml b/etc/pre-barreau_courses.xml index f84ece1..98326e4 100644 --- a/etc/pre-barreau_courses.xml +++ b/etc/pre-barreau_courses.xml @@ -203,4 +203,3 @@ -:q! \ No newline at end of file diff --git a/export/api.py b/export/api.py new file mode 100644 index 0000000..16f53bc --- /dev/null +++ b/export/api.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Parisson SARL +# Copyright (c) 2007 Olivier Guilyardi +# Copyright (c) 2007 Guillaume Pellerin +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://svn.parisson.org/telemeta/TelemetaLicense. +# +# Author: Olivier Guilyardi +# Guillaume Pellerin + +#from telemeta.core import Interface, TelemetaError + + +class IExporter(Interface): + """Export driver interface""" + + # Remark: the method prototypes do not include any self or cls argument + # because an interface is meant to show what methods a class must expose + # from the caller's point of view. However, when implementing the class + # you'll obviously want to include this extra argument. + + def get_format(): + """Return the export/encoding format as a short string + Example: "MP3", "OGG", "AVI", ... + """ + + def get_description(): + """Return a string describing what this export format provides, is good + for, etc... The description is meant to help the end user decide what + format is good for him/her + """ + + def get_file_extension(): + """Return the filename extension corresponding to this export format""" + + def get_mime_type(): + """Return the mime type corresponding to this export format""" + + def set_cache_dir(path): + """Set the directory where cached files should be stored. Does nothing + if the exporter doesn't support caching. + + The driver shouldn't assume that this method will always get called. A + temporary directory should be used if that's not the case. + """ + + def process(item_id, source, metadata, options=None): + """Perform the exporting process and return the absolute path + to the resulting file. + + item_id is the media item id that uniquely identifies this audio/video + resource + + source is the audio/video source file absolute path. For audio that + should be a WAV file + + metadata is a dictionary + + The returned file path is not meant to be permanent in any way, it + should be considered temporary/volatile by the caller. + + It is highly recommended that export drivers implement some sort of + cache instead of re-encoding each time process() is called. + + It should be possible to make subsequent calls to process() with + different items, using the same driver instance. + """ + +class ExportProcessError: + + 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/export/core.py b/export/core.py new file mode 100644 index 0000000..6454079 --- /dev/null +++ b/export/core.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Parisson SARL +# Copyright (c) 2006-2007 Guillaume Pellerin +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# yo"u should have received as part of this distribution. The terms +# are also available at http://svn.parisson.org/telemeta/TelemetaLicense. +# +# Author: Guillaume Pellerin + +import os +import re +import string +import subprocess +import mutagen +import export +import xml.dom.minidom +import xml.dom.ext + +#from telemeta.core import * +from export import * +from tools import * + +class ExporterCore: + """Defines the main parts of the exporting tools : + paths, formats, metadata...""" + + def __init__(self): + self.source = '' + self.collection = '' + self.verbose = '' + self.dest = '' + self.metadata = [] + self.cache_dir = 'cache' + self.buffer_size = 0xFFFF + + def set_cache_dir(self,path): + self.cache_dir = path + + def normalize(self): + """ Normalize the source and return its path """ + args = '' + if self.verbose == '0': + args = '-q' + try: + os.system('normalize-audio '+args+' "'+self.source+'"') + return self.source + except IOError: + return 'Exporter error: Cannot normalize, path does not exist.' + + def check_md5_key(self): + """ Check if the md5 key is OK and return a boolean """ + try: + md5_log = os.popen4('md5sum -c "'+self.dest+ \ + '" "'+self.dest+'.md5"') + return 'OK' in md5_log.split(':') + except IOError: + return 'Exporter error: Cannot check the md5 key...' + + def get_file_info(self): + """ Return the list of informations of the dest """ + return self.export.get_file_info() + + def get_wav_length_sec(self) : + """ Return the length of the audio source file in seconds """ + try: + file1, file2 = os.popen4('wavinfo "'+self.source+ \ + '" | grep wavDataSize') + for line in file2.readlines(): + line_split = line.split(':') + value = int(int(line_split[1])/(4*44100)) + return value + except IOError: + return 'Exporter error: Cannot get the wav length...' + + def compare_md5_key(self): + """ Compare 2 files wih md5 method """ + in1, in2 = os.popen4('md5sum -b "'+self.source+'"') + out1, out2 = os.popen4('md5sum -b "'+self.dest+'"') + for line in in2.readlines(): + line1 = line.split('*')[0] + for line in out2.readlines(): + line2 = line.split('*')[0] + return line1 == line2 + + def write_metadata_xml(self,path): + doc = xml.dom.minidom.Document() + root = doc.createElement('telemeta') + doc.appendChild(root) + for tag in self.metadata.keys() : + value = self.metadata[tag] + node = doc.createElement(tag) + node.setAttribute('value', str(value)) + #node.setAttribute('type', get_type(value)) + root.appendChild(node) + xml_file = open(path, "w") + xml.dom.ext.PrettyPrint(doc, xml_file) + xml_file.close() + + def pre_process(self, item_id, source, metadata, ext, + cache_dir, options=None): + """ Pre processing : prepare the export path and return it""" + self.item_id = str(item_id) + self.source = source + file_name = get_file_name(self.source) + file_name_wo_ext, file_ext = split_file_name(file_name) + self.cache_dir = cache_dir + self.metadata = metadata + #self.collection = self.metadata['Collection'] + #self.artist = self.metadata['Artist'] + #self.title = self.metadata['Title'] + + # Normalize if demanded + if not options is None: + self.options = options + if 'normalize' in self.options and \ + self.options['normalize'] == True: + self.normalize() + + # Define the export directory + self.ext = self.get_file_extension() + export_dir = os.path.join(self.cache_dir,self.ext) + + if not os.path.exists(export_dir): + export_dir_split = export_dir.split(os.sep) + path = os.sep + export_dir_split[0] + for _dir in export_dir_split[1:]: + path = os.path.join(path,_dir) + if not os.path.exists(path): + os.mkdir(path) + else: + path = export_dir + + # Set the target file + target_file = self.item_id+'.'+self.ext + dest = os.path.join(path,target_file) + return dest + + def core_process(self, command, buffer_size, dest): + """Encode and stream audio data through a generator""" + + __chunk = 0 + file_out = open(dest,'w') + + try: + proc = subprocess.Popen(command, + shell = True, + bufsize = buffer_size, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + close_fds = True) + except: + raise ExportProcessError('Command failure:', command, proc) + + + # Core processing + while True: + __chunk = proc.stdout.read(buffer_size) + status = proc.poll() + if status != None and status != 0: + raise ExportProcessError('Command failure:', command, proc) + if len(__chunk) == 0: + break + yield __chunk + file_out.write(__chunk) + + file_out.close() + + def post_process(self, item_id, source, metadata, ext, + cache_dir, options=None): + """ Post processing : write tags, print infos, etc...""" + self.write_tags() + if not options is None: + if 'verbose' in self.options and self.options['verbose'] != '0': + print self.dest + print self.get_file_info() + diff --git a/export/mp3.py b/export/mp3.py new file mode 100644 index 0000000..c83344c --- /dev/null +++ b/export/mp3.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Parisson SARL +# Copyright (c) 2006-2007 Guillaume Pellerin +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://svn.parisson.org/telemeta/TelemetaLicense. +# +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from telemeta.export.core import * +from telemeta.export.api import IExporter +#from mutagen.id3 import * + + +class Mp3Exporter(ExporterCore): + """Defines methods to export to MP3""" + + implements(IExporter) + + def __init__(self): + self.item_id = '' + self.metadata = {} + self.description = '' + self.info = [] + self.source = '' + self.dest = '' + self.options = {} + self.bitrate_default = '192' + self.buffer_size = 0xFFFF + 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', #composer + 'relation': 'tl', #album + #'type': 'tg', #genre + 'publisher': 'tc', #comment + 'date': 'ty', #year + } + def get_format(self): + return 'MP3' + + def get_file_extension(self): + return 'mp3' + + def get_mime_type(self): + return 'audio/mpeg' + + def get_description(self): + return "FIXME" + + def set_cache_dir(self,path): + self.cache_dir = path + + 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 IOError: + return 'Exporter error [1]: file does not exist.' + + def decode(self): + try: + os.system('sox "'+self.source+'" -w -r 44100 -t wav "' \ + +self.cache_dir+os.sep+self.item_id+'"') + return self.cache_dir+os.sep+self.item_id+'.wav' + except IOError: + return 'ExporterError [2]: decoder not compatible.' + + 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) + id3.add(frame) + id3.save() + + 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 '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 -o') + + for tag in self.metadata.keys(): + if tag in self.dub2args_dict.keys(): + arg = self.dub2args_dict[tag] + value = clean_word(self.metadata[tag]) + args.append('--' + arg) + args.append('"' + value + '"') + + return args + + def process(self, item_id, source, metadata, options=None): + self.item_id = item_id + self.source = source + self.metadata = metadata + self.args = self.get_args(options) + self.ext = self.get_file_extension() + self.args = ' '.join(self.args) + self.command = 'sox "%s" -q -w -r 44100 -t wav -c2 - | lame %s -' \ + % (self.source,self.args) + + # Pre-proccessing + self.dest = self.pre_process(self.item_id, + self.source, + self.metadata, + self.ext, + self.cache_dir, + self.options) + + # Processing (streaming + cache writing) + stream = self.core_process(self.command,self.buffer_size,self.dest) + for chunk in stream: + yield chunk + + # Post-proccessing + self.post_process(self.item_id, + self.source, + self.metadata, + self.ext, + self.cache_dir, + self.options) + diff --git a/export/ogg.py b/export/ogg.py new file mode 100644 index 0000000..ea64182 --- /dev/null +++ b/export/ogg.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 Parisson SARL +# Copyright (c) 2006-2007 Guillaume Pellerin +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://svn.parisson.org/telemeta/TelemetaLicense. +# +# Author: Guillaume Pellerin + +import os +import string +import subprocess + +from telemeta.export.core import * +from telemeta.export.api import IExporter +from mutagen.oggvorbis import OggVorbis + +class OggExporter(ExporterCore): + """Defines methods to export to OGG Vorbis""" + + implements(IExporter) + + def __init__(self): + self.item_id = '' + self.metadata = {} + self.description = '' + self.info = [] + self.source = '' + self.dest = '' + self.options = {} + self.bitrate_default = '192' + self.buffer_size = 0xFFFF + self.dub2args_dict = {'creator': 'artist', + 'relation': 'album' + } + + def get_format(self): + return 'OGG' + + def get_file_extension(self): + return 'ogg' + + def get_mime_type(self): + return 'application/ogg' + + def get_description(self): + return 'FIXME' + + def get_file_info(self): + try: + file_out1, file_out2 = os.popen4('ogginfo "'+self.dest+'"') + info = [] + for line in file_out2.readlines(): + info.append(clean_word(line[:-1])) + self.info = info + return self.info + except IOError: + return 'Exporter error [1]: file does not exist.' + + def set_cache_dir(self,path): + self.cache_dir = path + + def decode(self): + try: + os.system('oggdec -o "'+self.cache_dir+os.sep+self.item_id+ + '.wav" "'+self.source+'"') + return self.cache_dir+os.sep+self.item_id+'.wav' + except IOError: + return 'ExporterError [2]: decoder not compatible.' + + def write_tags(self): + media = OggVorbis(self.dest) + for tag in self.metadata.keys(): + media[tag] = str(self.metadata[tag]) + media.save() + + 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('-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.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, item_id, source, metadata, options=None): + self.item_id = item_id + self.source = source + self.metadata = metadata + self.args = self.get_args(options) + self.ext = self.get_file_extension() + self.args = ' '.join(self.args) + self.command = 'sox "%s" -q -w -r 44100 -t wav -c2 - | oggenc %s -' \ + % (self.source,self.args) + + # Pre-proccessing + self.dest = self.pre_process(self.item_id, + self.source, + self.metadata, + self.ext, + self.cache_dir, + self.options) + + # Processing (streaming + cache writing) + stream = self.core_process(self.command,self.buffer_size,self.dest) + for chunk in stream: + yield chunk + + # Post-proccessing + self.post_process(self.item_id, + self.source, + self.metadata, + self.ext, + self.cache_dir, + self.options) + diff --git a/export/tools.py b/export/tools.py new file mode 100644 index 0000000..0a57c45 --- /dev/null +++ b/export/tools.py @@ -0,0 +1,96 @@ + +# External functions + +def get_type(value): + """ Return a String with the type of value """ + types = {bool : 'bool', int : 'int', str : 'str'} + # 'bool' type must be placed *before* 'int' type, otherwise booleans are + # detected as integers + for type in types.keys(): + if isinstance(value, type) : + return types[type] + raise TypeError, str(value) + ' has an unsupported type' + +def get_cast(value, type) : + """ Return value, casted into type """ + if type == 'bool' : + if value == 'True' : + return True + return False + elif type == 'int' : + return int(value) + elif type == 'str' : + return str(value) + raise TypeError, type + ' is an unsupported type' + +def get_file_mime_type(path): + """ Return the mime type of a file """ + try: + file_out1, file_out2 = os.popen4('file -i "'+path+'"') + for line in file_out2.readlines(): + line_split = line.split(': ') + mime = line_split[len(line_split)-1] + return mime[:len(mime)-1] + except IOError: + return 'Exporter error [1]: path does not exist.' + +def get_file_type_desc(path): + """ Return the type of a file given by the 'file' command """ + try: + file_out1, file_out2 = os.popen4('file "'+path+'"') + for line in file_out2.readlines(): + description = line.split(': ') + description = description[1].split(', ') + return description + except IOError: + return 'Exporter error [1]: path does not exist.' + +def iswav(path): + """ Tell if path is a WAV """ + try: + mime = get_file_mime_type(path) + return mime == 'audio/x-wav' + except IOError: + return 'Exporter error [1]: path does not exist.' + +def iswav16(path): + """ Tell if path is a 16 bit WAV """ + try: + file_type_desc = get_file_type_desc(path) + return iswav(path) and '16 bit' in file_type_desc + except IOError: + return 'Exporter error [1]: path does not exist.' + +def get_file_name(path): + """ Return the file name targeted in the path """ + return os.path.split(path)[1] + +def split_file_name(file): + """ Return main file name and its extension """ + try: + return os.path.splitext(file) + except IOError: + return 'Exporter error [1]: path does not exist.' + +def clean_word(word) : + """ Return the word without excessive blank spaces, underscores and + characters causing problem to exporters""" + word = re.sub("^[^\w]+","",word) #trim the beginning + word = re.sub("[^\w]+$","",word) #trim the end + word = re.sub("_+","_",word) #squeeze continuous _ to one _ + word = re.sub("^[^\w]+","",word) #trim the beginning _ + #word = string.replace(word,' ','_') + #word = string.capitalize(word) + dict = '&[];"*:,' + for letter in dict: + word = string.replace(word,letter,'_') + return word + +def recover_par_key(path): + """ Recover a file with par2 key """ + os.system('par2 r "'+path+'"') + +def verify_par_key(path): + """ Verify a par2 key """ + os.system('par2 v "'+path+'.par2"') + diff --git a/jack/jack.py b/jack/jack.py new file mode 100644 index 0000000..4c73769 --- /dev/null +++ b/jack/jack.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +# Capture 3 seconds of stereo audio from alsa_pcm:capture_1/2; then play it back. +# +# Copyright 2003, Andrew W. Schmeder +# This source code is released under the terms of the GNU Public License. +# See LICENSE for the full text of these terms. + +import Numeric +import jack +import time + +class JackInput: + "A JACK connexion input in TeleOddCast" + + def __init__(self, dict): + self.host = dict['host'] + self.name = dict['name'] + self.jack = jack() + self.buffer_size = self.jack.get_buffer_size() + self.sample_rate = float(self.jack.get_sample_rate()) + print "Buffer Size:", N, "Sample Rate:", Sr + self.power = True + self.capture = Numeric.zeros((2, self.buffer_size), 'f') + + + def attach(self): + jack.attach(self.name) + + def get_ports(self): + return self.jack.get_ports() + + def register_ports(self): + self.jack.register_port("in_1", self.jack.IsInput) + self.jack.register_port("in_2", self.jack.IsInput) + + def activate(self): + self.jack.activate() + + def stop(self): + self.power = False + + def connect(self): + self.jack.connect("alsa_pcm:capture_1", self.name+":in_1") + self.jack.connect("alsa_pcm:capture_2", self.name+":in_2") + #jack.connect(self.name+":out_1", "alsa_pcm:playback_1") + #jack.connect(self.name+":out_2", "alsa_pcm:playback_2") + + def process(self): + while True: + try: + if not self.power: + break + self.jack.process(__chunk, capture[:,self.buffer_size]) + yield __chunk + except self.jack.InputSyncError: + print "Input Sync" + pass + except self.jack.OutputSyncError: + print "Output Sync" + pass + + def desactivate(self): + self.jack.deactivate() + + def detach(self): + self.jack.detach() diff --git a/teleoddcast.py b/teleoddcast.py index cfaa905..c8eb1b8 100755 --- a/teleoddcast.py +++ b/teleoddcast.py @@ -20,7 +20,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. """ -version = '0.3' +version = '0.3.1' import os @@ -30,6 +30,7 @@ import datetime import time import codecs import string +import jack from tools import * from mutagen.oggvorbis import OggVorbis @@ -113,6 +114,55 @@ class Station(Course): self.set_lock() time.sleep(1) + def star_mp3cast(self): + + item_id = item_id + source = source + metadata = metadata + args = get_args(options) + ext = get_file_extension() + args = ' '.join(args) + command = 'sox "%s" -q -w -r 44100 -t wav -c2 - | lame %s -' \ + % (source, args) + + # Processing (streaming + cache writing) + e = ExporterCore() + stream = e.core_process(self.command,self.buffer_size,self.dest) + + for chunk in stream: + yield chunk + + + def core_process(self, command, buffer_size, dest): + """Encode and stream audio data through a generator""" + + __chunk = 0 + file_out = open(dest,'w') + + try: + proc = subprocess.Popen(command, + shell = True, + bufsize = buffer_size, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + close_fds = True) + except: + raise ExportProcessError('Command failure:', command, proc) + + + # Core processing + while True: + __chunk = proc.stdout.read(buffer_size) + status = proc.poll() + if status != None and status != 0: + raise ExportProcessError('Command failure:', command, proc) + if len(__chunk) == 0: + break + yield __chunk + file_out.write(__chunk) + + file_out.close() + def set_lock(self): lock = open(self.lock_file,'w') lock_text = clean_string('_*_'.join(self.description)) @@ -131,11 +181,11 @@ class Station(Course): os.system(command) def stop_oddcast(self): - if self.odd_pid[0]: + if len(self.odd_pid) != 0: os.system('kill -9 ' + self.odd_pid[0]) def stop_rip(self): - if self.rip_pid[0]: + if len(self.rip_pid) != 0: os.system('kill -9 ' + self.rip_pid[0]) time.sleep(1) date = datetime.datetime.now().strftime("%Y") @@ -240,7 +290,7 @@ class WebView: def start_form(self): self.header() print "
" - print "
Cliquez ici pour écouter le flux continu 24/24 en direct
" + print "
Cliquez ici pour écouter le flux continu 24/24 en direct
" print "\t" print "\t\t" print "\t\t" -- 2.39.5
Titre :"+self.title+"