Merge remote-tracking branch 'joar/audio+sniffing'
Conflicts: mediagoblin/media_types/image/processing.py mediagoblin/media_types/video/__init__.py mediagoblin/media_types/video/processing.py mediagoblin/tests/test_submission.py
This commit is contained in:
commit
deea3f6661
616
extlib/freesound/audioprocessing.py
Normal file
616
extlib/freesound/audioprocessing.py
Normal file
@ -0,0 +1,616 @@
|
||||
#!/usr/bin/env python
|
||||
# processing.py -- various audio processing functions
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# Authors:
|
||||
# Bram de Jong <bram.dejong at domain.com where domain in gmail>
|
||||
# 2012, Joar Wandborg <first name at last name dot se>
|
||||
|
||||
from PIL import Image, ImageDraw, ImageColor #@UnresolvedImport
|
||||
from functools import partial
|
||||
import math
|
||||
import numpy
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
|
||||
|
||||
def get_sound_type(input_filename):
|
||||
sound_type = os.path.splitext(input_filename.lower())[1].strip(".")
|
||||
|
||||
if sound_type == "fla":
|
||||
sound_type = "flac"
|
||||
elif sound_type == "aif":
|
||||
sound_type = "aiff"
|
||||
|
||||
return sound_type
|
||||
|
||||
|
||||
try:
|
||||
import scikits.audiolab as audiolab
|
||||
except ImportError:
|
||||
print "WARNING: audiolab is not installed so wav2png will not work"
|
||||
import subprocess
|
||||
|
||||
class AudioProcessingException(Exception):
|
||||
pass
|
||||
|
||||
class TestAudioFile(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.nframes = num_frames
|
||||
self.samplerate = 44100
|
||||
self.channels = 1
|
||||
self.has_broken_header = has_broken_header
|
||||
|
||||
def seek(self, seekpoint):
|
||||
self.seekpoint = seekpoint
|
||||
|
||||
def read_frames(self, frames_to_read):
|
||||
if self.has_broken_header and self.seekpoint + frames_to_read > self.num_frames / 2:
|
||||
raise RuntimeError()
|
||||
|
||||
num_frames_left = self.num_frames - self.seekpoint
|
||||
will_read = num_frames_left if num_frames_left < frames_to_read else frames_to_read
|
||||
self.seekpoint += will_read
|
||||
return numpy.random.random(will_read)*2 - 1
|
||||
|
||||
|
||||
def get_max_level(filename):
|
||||
max_value = 0
|
||||
buffer_size = 4096
|
||||
audio_file = audiolab.Sndfile(filename, 'r')
|
||||
n_samples_left = audio_file.nframes
|
||||
|
||||
while n_samples_left:
|
||||
to_read = min(buffer_size, n_samples_left)
|
||||
|
||||
try:
|
||||
samples = audio_file.read_frames(to_read)
|
||||
except RuntimeError:
|
||||
# this can happen with a broken header
|
||||
break
|
||||
|
||||
# convert to mono by selecting left channel only
|
||||
if audio_file.channels > 1:
|
||||
samples = samples[:,0]
|
||||
|
||||
max_value = max(max_value, numpy.abs(samples).max())
|
||||
|
||||
n_samples_left -= to_read
|
||||
|
||||
audio_file.close()
|
||||
|
||||
return max_value
|
||||
|
||||
class AudioProcessor(object):
|
||||
"""
|
||||
The audio processor processes chunks of audio an calculates the spectrac centroid and the peak
|
||||
samples in that chunk of audio.
|
||||
"""
|
||||
def __init__(self, input_filename, fft_size, window_function=numpy.hanning):
|
||||
max_level = get_max_level(input_filename)
|
||||
|
||||
self.audio_file = audiolab.Sndfile(input_filename, 'r')
|
||||
self.fft_size = fft_size
|
||||
self.window = window_function(self.fft_size)
|
||||
self.spectrum_range = None
|
||||
self.lower = 100
|
||||
self.higher = 22050
|
||||
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))
|
||||
|
||||
# figure out what the maximum value is for an FFT doing the FFT of a DC signal
|
||||
fft = numpy.fft.rfft(numpy.ones(fft_size) * self.window)
|
||||
max_fft = (numpy.abs(fft)).max()
|
||||
# set the scale to normalized audio and normalized FFT
|
||||
self.scale = 1.0/max_level/max_fft if max_level > 0 else 1
|
||||
|
||||
def read(self, start, size, resize_if_less=False):
|
||||
""" read size samples starting at start, if resize_if_less is True and less than size
|
||||
samples are read, resize the array to size and fill with zeros """
|
||||
|
||||
# number of zeros to add to start and end of the buffer
|
||||
add_to_start = 0
|
||||
add_to_end = 0
|
||||
|
||||
if start < 0:
|
||||
# the first FFT window starts centered around zero
|
||||
if size + start <= 0:
|
||||
return numpy.zeros(size) if resize_if_less else numpy.array([])
|
||||
else:
|
||||
self.audio_file.seek(0)
|
||||
|
||||
add_to_start = -start # remember: start is negative!
|
||||
to_read = size + start
|
||||
|
||||
if to_read > self.audio_file.nframes:
|
||||
add_to_end = to_read - self.audio_file.nframes
|
||||
to_read = self.audio_file.nframes
|
||||
else:
|
||||
self.audio_file.seek(start)
|
||||
|
||||
to_read = size
|
||||
if start + to_read >= self.audio_file.nframes:
|
||||
to_read = self.audio_file.nframes - start
|
||||
add_to_end = size - to_read
|
||||
|
||||
try:
|
||||
samples = self.audio_file.read_frames(to_read)
|
||||
except RuntimeError:
|
||||
# this can happen for wave files with broken headers...
|
||||
return numpy.zeros(size) if resize_if_less else numpy.zeros(2)
|
||||
|
||||
# convert to mono by selecting left channel only
|
||||
if self.audio_file.channels > 1:
|
||||
samples = samples[:,0]
|
||||
|
||||
if resize_if_less and (add_to_start > 0 or add_to_end > 0):
|
||||
if add_to_start > 0:
|
||||
samples = numpy.concatenate((numpy.zeros(add_to_start), samples), axis=1)
|
||||
|
||||
if add_to_end > 0:
|
||||
samples = numpy.resize(samples, size)
|
||||
samples[size - add_to_end:] = 0
|
||||
|
||||
return samples
|
||||
|
||||
|
||||
def spectral_centroid(self, seek_point, spec_range=110.0):
|
||||
""" starting at seek_point read fft_size samples, and calculate the spectral centroid """
|
||||
|
||||
samples = self.read(seek_point - self.fft_size/2, self.fft_size, True)
|
||||
|
||||
samples *= self.window
|
||||
fft = numpy.fft.rfft(samples)
|
||||
spectrum = self.scale * numpy.abs(fft) # 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-60))).clip(-spec_range, 0.0) + spec_range)/spec_range
|
||||
|
||||
energy = spectrum.sum()
|
||||
spectral_centroid = 0
|
||||
|
||||
if energy > 1e-60:
|
||||
# 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.audio_file.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 peaks(self, start_seek, end_seek):
|
||||
""" read all samples between start_seek and end_seek, then find the minimum and maximum peak
|
||||
in that range. Returns that pair in the order they were found. So if min was found first,
|
||||
it returns (min, max) else the other way around. """
|
||||
|
||||
# larger blocksizes are faster but take more mem...
|
||||
# Aha, Watson, a clue, a tradeof!
|
||||
block_size = 4096
|
||||
|
||||
max_index = -1
|
||||
max_value = -1
|
||||
min_index = -1
|
||||
min_value = 1
|
||||
|
||||
if start_seek < 0:
|
||||
start_seek = 0
|
||||
|
||||
if end_seek > self.audio_file.nframes:
|
||||
end_seek = self.audio_file.nframes
|
||||
|
||||
if end_seek <= start_seek:
|
||||
samples = self.read(start_seek, 1)
|
||||
return (samples[0], samples[0])
|
||||
|
||||
if block_size > end_seek - start_seek:
|
||||
block_size = end_seek - start_seek
|
||||
|
||||
for i in range(start_seek, end_seek, block_size):
|
||||
samples = self.read(i, block_size)
|
||||
|
||||
local_max_index = numpy.argmax(samples)
|
||||
local_max_value = samples[local_max_index]
|
||||
|
||||
if local_max_value > max_value:
|
||||
max_value = local_max_value
|
||||
max_index = local_max_index
|
||||
|
||||
local_min_index = numpy.argmin(samples)
|
||||
local_min_value = samples[local_min_index]
|
||||
|
||||
if local_min_value < min_value:
|
||||
min_value = local_min_value
|
||||
min_index = local_min_index
|
||||
|
||||
return (min_value, max_value) if min_index < max_index else (max_value, min_value)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def desaturate(rgb, amount):
|
||||
"""
|
||||
desaturate colors by amount
|
||||
amount == 0, no change
|
||||
amount == 1, grey
|
||||
"""
|
||||
luminosity = sum(rgb) / 3.0
|
||||
desat = lambda color: color - amount * (color - luminosity)
|
||||
|
||||
return tuple(map(int, map(desat, rgb)))
|
||||
|
||||
|
||||
class WaveformImage(object):
|
||||
"""
|
||||
Given peaks and spectral centroids from the AudioProcessor, this class will construct
|
||||
a wavefile image which can be saved as PNG.
|
||||
"""
|
||||
def __init__(self, image_width, image_height, palette=1):
|
||||
if image_height % 2 == 0:
|
||||
raise AudioProcessingException, "Height should be uneven: images look much better at uneven height"
|
||||
|
||||
if palette == 1:
|
||||
background_color = (0,0,0)
|
||||
colors = [
|
||||
(50,0,200),
|
||||
(0,220,80),
|
||||
(255,224,0),
|
||||
(255,70,0),
|
||||
]
|
||||
elif palette == 2:
|
||||
background_color = (0,0,0)
|
||||
colors = [self.color_from_value(value/29.0) for value in range(0,30)]
|
||||
elif palette == 3:
|
||||
background_color = (213, 217, 221)
|
||||
colors = map( partial(desaturate, amount=0.7), [
|
||||
(50,0,200),
|
||||
(0,220,80),
|
||||
(255,224,0),
|
||||
])
|
||||
elif palette == 4:
|
||||
background_color = (213, 217, 221)
|
||||
colors = map( partial(desaturate, amount=0.8), [self.color_from_value(value/29.0) for value in range(0,30)])
|
||||
|
||||
self.image = Image.new("RGB", (image_width, image_height), background_color)
|
||||
|
||||
self.image_width = image_width
|
||||
self.image_height = image_height
|
||||
|
||||
self.draw = ImageDraw.Draw(self.image)
|
||||
self.previous_x, self.previous_y = None, None
|
||||
|
||||
self.color_lookup = interpolate_colors(colors)
|
||||
self.pix = self.image.load()
|
||||
|
||||
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.pix[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.pix[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.pix[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.pix[x, y_min_int - 1] = (r,g,b)
|
||||
|
||||
def save(self, filename):
|
||||
# draw a zero "zero" line
|
||||
a = 25
|
||||
for x in range(self.image_width):
|
||||
self.pix[x, self.image_height/2] = tuple(map(lambda p: p+a, self.pix[x, self.image_height/2]))
|
||||
|
||||
self.image.save(filename)
|
||||
|
||||
|
||||
class SpectrogramImage(object):
|
||||
"""
|
||||
Given spectra from the AudioProcessor, this class will construct a wavefile image which
|
||||
can be saved as PNG.
|
||||
"""
|
||||
def __init__(self, image_width, image_height, fft_size):
|
||||
self.image_width = image_width
|
||||
self.image_height = image_height
|
||||
self.fft_size = fft_size
|
||||
|
||||
self.image = Image.new("RGBA", (image_height, image_width))
|
||||
|
||||
colors = [
|
||||
(0, 0, 0, 0),
|
||||
(58/4, 68/4, 65/4, 255),
|
||||
(80/2, 100/2, 153/2, 255),
|
||||
(90, 180, 100, 255),
|
||||
(224, 224, 44, 255),
|
||||
(255, 60, 30, 255),
|
||||
(255, 255, 255, 255)
|
||||
]
|
||||
self.palette = interpolate_colors(colors)
|
||||
|
||||
# generate the lookup which translates y-coordinate to fft-bin
|
||||
self.y_to_bin = []
|
||||
f_min = 100.0
|
||||
f_max = 22050.0
|
||||
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 = []
|
||||
|
||||
def draw_spectrum(self, x, spectrum):
|
||||
# for all frequencies, draw the pixels
|
||||
for (index, alpha) in self.y_to_bin:
|
||||
self.pixels.append( self.palette[int((255.0-alpha) * spectrum[index] + alpha * spectrum[index + 1])] )
|
||||
|
||||
# if the FFT is too small to fill up the image, fill with black to the top
|
||||
for y in range(len(self.y_to_bin), self.image_height): #@UnusedVariable
|
||||
self.pixels.append(self.palette[0])
|
||||
|
||||
def save(self, filename, quality=80):
|
||||
assert filename.lower().endswith(".jpg")
|
||||
self.image.putdata(self.pixels)
|
||||
self.image.transpose(Image.ROTATE_90).save(filename, quality=quality)
|
||||
|
||||
|
||||
def create_wave_images(input_filename, output_filename_w, output_filename_s, image_width, image_height, fft_size, progress_callback=None):
|
||||
"""
|
||||
Utility function for creating both wavefile and spectrum images from an audio input file.
|
||||
"""
|
||||
processor = AudioProcessor(input_filename, fft_size, numpy.hanning)
|
||||
samples_per_pixel = processor.audio_file.nframes / float(image_width)
|
||||
|
||||
waveform = WaveformImage(image_width, image_height)
|
||||
spectrogram = SpectrogramImage(image_width, image_height, fft_size)
|
||||
|
||||
for x in range(image_width):
|
||||
|
||||
if progress_callback and x % (image_width/10) == 0:
|
||||
progress_callback((x*100)/image_width)
|
||||
|
||||
seek_point = int(x * samples_per_pixel)
|
||||
next_seek_point = int((x + 1) * samples_per_pixel)
|
||||
|
||||
(spectral_centroid, db_spectrum) = processor.spectral_centroid(seek_point)
|
||||
peaks = processor.peaks(seek_point, next_seek_point)
|
||||
|
||||
waveform.draw_peaks(x, peaks, spectral_centroid)
|
||||
spectrogram.draw_spectrum(x, db_spectrum)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(100)
|
||||
|
||||
waveform.save(output_filename_w)
|
||||
spectrogram.save(output_filename_s)
|
||||
|
||||
|
||||
class NoSpaceLeftException(Exception):
|
||||
pass
|
||||
|
||||
def convert_to_pcm(input_filename, output_filename):
|
||||
"""
|
||||
converts any audio file type to pcm audio
|
||||
"""
|
||||
|
||||
if not os.path.exists(input_filename):
|
||||
raise AudioProcessingException, "file %s does not exist" % input_filename
|
||||
|
||||
sound_type = get_sound_type(input_filename)
|
||||
|
||||
if sound_type == "mp3":
|
||||
cmd = ["lame", "--decode", input_filename, output_filename]
|
||||
elif sound_type == "ogg":
|
||||
cmd = ["oggdec", input_filename, "-o", output_filename]
|
||||
elif sound_type == "flac":
|
||||
cmd = ["flac", "-f", "-d", "-s", "-o", output_filename, input_filename]
|
||||
else:
|
||||
return False
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(stdout, stderr) = process.communicate()
|
||||
|
||||
if process.returncode != 0 or not os.path.exists(output_filename):
|
||||
if "No space left on device" in stderr + " " + stdout:
|
||||
raise NoSpaceLeftException
|
||||
raise AudioProcessingException, "failed converting to pcm data:\n" + " ".join(cmd) + "\n" + stderr + "\n" + stdout
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def stereofy_and_find_info(stereofy_executble_path, input_filename, output_filename):
|
||||
"""
|
||||
converts a pcm wave file to two channel, 16 bit integer
|
||||
"""
|
||||
|
||||
if not os.path.exists(input_filename):
|
||||
raise AudioProcessingException, "file %s does not exist" % input_filename
|
||||
|
||||
cmd = [stereofy_executble_path, "--input", input_filename, "--output", output_filename]
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(stdout, stderr) = process.communicate()
|
||||
|
||||
if process.returncode != 0 or not os.path.exists(output_filename):
|
||||
if "No space left on device" in stderr + " " + stdout:
|
||||
raise NoSpaceLeftException
|
||||
raise AudioProcessingException, "failed calling stereofy data:\n" + " ".join(cmd) + "\n" + stderr + "\n" + stdout
|
||||
|
||||
stdout = (stdout + " " + stderr).replace("\n", " ")
|
||||
|
||||
duration = 0
|
||||
m = re.match(r".*#duration (?P<duration>[\d\.]+).*", stdout)
|
||||
if m != None:
|
||||
duration = float(m.group("duration"))
|
||||
|
||||
channels = 0
|
||||
m = re.match(r".*#channels (?P<channels>\d+).*", stdout)
|
||||
if m != None:
|
||||
channels = float(m.group("channels"))
|
||||
|
||||
samplerate = 0
|
||||
m = re.match(r".*#samplerate (?P<samplerate>\d+).*", stdout)
|
||||
if m != None:
|
||||
samplerate = float(m.group("samplerate"))
|
||||
|
||||
bitdepth = None
|
||||
m = re.match(r".*#bitdepth (?P<bitdepth>\d+).*", stdout)
|
||||
if m != None:
|
||||
bitdepth = float(m.group("bitdepth"))
|
||||
|
||||
bitrate = (os.path.getsize(input_filename) * 8.0) / 1024.0 / duration if duration > 0 else 0
|
||||
|
||||
return dict(duration=duration, channels=channels, samplerate=samplerate, bitrate=bitrate, bitdepth=bitdepth)
|
||||
|
||||
|
||||
def convert_to_mp3(input_filename, output_filename, quality=70):
|
||||
"""
|
||||
converts the incoming wave file to a mp3 file
|
||||
"""
|
||||
|
||||
if not os.path.exists(input_filename):
|
||||
raise AudioProcessingException, "file %s does not exist" % input_filename
|
||||
|
||||
command = ["lame", "--silent", "--abr", str(quality), input_filename, output_filename]
|
||||
|
||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(stdout, stderr) = process.communicate()
|
||||
|
||||
if process.returncode != 0 or not os.path.exists(output_filename):
|
||||
raise AudioProcessingException, stdout
|
||||
|
||||
def convert_to_ogg(input_filename, output_filename, quality=1):
|
||||
"""
|
||||
converts the incoming wave file to n ogg file
|
||||
"""
|
||||
|
||||
if not os.path.exists(input_filename):
|
||||
raise AudioProcessingException, "file %s does not exist" % input_filename
|
||||
|
||||
command = ["oggenc", "-q", str(quality), input_filename, "-o", output_filename]
|
||||
|
||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(stdout, stderr) = process.communicate()
|
||||
|
||||
if process.returncode != 0 or not os.path.exists(output_filename):
|
||||
raise AudioProcessingException, stdout
|
||||
|
||||
def convert_using_ffmpeg(input_filename, output_filename):
|
||||
"""
|
||||
converts the incoming wave file to stereo pcm using fffmpeg
|
||||
"""
|
||||
TIMEOUT = 3 * 60
|
||||
def alarm_handler(signum, frame):
|
||||
raise AudioProcessingException, "timeout while waiting for ffmpeg"
|
||||
|
||||
if not os.path.exists(input_filename):
|
||||
raise AudioProcessingException, "file %s does not exist" % input_filename
|
||||
|
||||
command = ["ffmpeg", "-y", "-i", input_filename, "-ac","1","-acodec", "pcm_s16le", "-ar", "44100", output_filename]
|
||||
|
||||
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
signal.signal(signal.SIGALRM,alarm_handler)
|
||||
signal.alarm(TIMEOUT)
|
||||
(stdout, stderr) = process.communicate()
|
||||
signal.alarm(0)
|
||||
if process.returncode != 0 or not os.path.exists(output_filename):
|
||||
raise AudioProcessingException, stdout
|
@ -69,11 +69,28 @@ base_url = string(default="/mgoblin_media/")
|
||||
storage_class = string(default="mediagoblin.storage.filestorage:BasicFileStorage")
|
||||
base_dir = string(default="%(here)s/user_dev/media/queue")
|
||||
|
||||
[media:medium]
|
||||
# Dimensions used when creating media display images.
|
||||
max_width = integer(default=640)
|
||||
max_height = integer(default=640)
|
||||
|
||||
[media:thumb]
|
||||
# Dimensions used when creating media thumbnails
|
||||
# This is unfortunately not implemented in the media
|
||||
# types yet. You can help!
|
||||
# TODO: Make plugins follow the media size settings
|
||||
max_width = integer(default=180)
|
||||
max_height = integer(default=180)
|
||||
|
||||
# Should we keep the original file?
|
||||
[media_type:mediagoblin.media_types.video]
|
||||
# Should we keep the original file?
|
||||
keep_original = boolean(default=False)
|
||||
|
||||
[media_type:mediagoblin.media_types.audio]
|
||||
# vorbisenc qualiy
|
||||
quality = float(default=0.3)
|
||||
create_spectrogram = boolean(default=True)
|
||||
|
||||
|
||||
[beaker.cache]
|
||||
type = string(default="file")
|
||||
|
@ -16,10 +16,13 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
class FileTypeNotSupported(Exception):
|
||||
pass
|
||||
@ -28,6 +31,35 @@ class InvalidFileType(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def sniff_media(media):
|
||||
'''
|
||||
Iterate through the enabled media types and find those suited
|
||||
for a certain file.
|
||||
'''
|
||||
|
||||
try:
|
||||
return get_media_type_and_manager(media.filename)
|
||||
except FileTypeNotSupported:
|
||||
_log.info('No media handler found by file extension. Doing it the expensive way...')
|
||||
# Create a temporary file for sniffers suchs as GStreamer-based
|
||||
# Audio video
|
||||
media_file = tempfile.NamedTemporaryFile()
|
||||
media_file.write(media.file.read())
|
||||
media.file.seek(0)
|
||||
|
||||
for media_type, manager in get_media_managers():
|
||||
_log.info('Sniffing {0}'.format(media_type))
|
||||
if manager['sniff_handler'](media_file, media=media):
|
||||
_log.info('{0} accepts the file'.format(media_type))
|
||||
return media_type, manager
|
||||
else:
|
||||
_log.debug('{0} did not accept the file'.format(media_type))
|
||||
|
||||
raise FileTypeNotSupported(
|
||||
# TODO: Provide information on which file types are supported
|
||||
_(u'Sorry, I don\'t support that file type :('))
|
||||
|
||||
|
||||
def get_media_types():
|
||||
"""
|
||||
Generator, yields the available media types
|
||||
@ -42,7 +74,7 @@ def get_media_managers():
|
||||
'''
|
||||
for media_type in get_media_types():
|
||||
__import__(media_type)
|
||||
|
||||
|
||||
yield media_type, sys.modules[media_type].MEDIA_MANAGER
|
||||
|
||||
|
||||
@ -67,22 +99,22 @@ def get_media_manager(_media_type):
|
||||
|
||||
def get_media_type_and_manager(filename):
|
||||
'''
|
||||
Get the media type and manager based on a filename
|
||||
Try to find the media type based on the file name, extension
|
||||
specifically. This is used as a speedup, the sniffing functionality
|
||||
then falls back on more in-depth bitsniffing of the source file.
|
||||
'''
|
||||
if filename.find('.') > 0:
|
||||
# Get the file extension
|
||||
ext = os.path.splitext(filename)[1].lower()
|
||||
else:
|
||||
raise InvalidFileType(
|
||||
_(u'Could not extract any file extension from "{filename}"').format(
|
||||
filename=filename))
|
||||
|
||||
for media_type, manager in get_media_managers():
|
||||
# Omit the dot from the extension and match it against
|
||||
# the media manager
|
||||
if ext[1:] in manager['accepted_extensions']:
|
||||
return media_type, manager
|
||||
for media_type, manager in get_media_managers():
|
||||
# Omit the dot from the extension and match it against
|
||||
# the media manager
|
||||
if ext[1:] in manager['accepted_extensions']:
|
||||
return media_type, manager
|
||||
else:
|
||||
raise FileTypeNotSupported(
|
||||
# TODO: Provide information on which file types are supported
|
||||
_(u'Sorry, I don\'t support that file type :('))
|
||||
_log.info('File {0} has no file extension, let\'s hope the sniffers get it.'.format(
|
||||
filename))
|
||||
|
||||
raise FileTypeNotSupported(
|
||||
_(u'Sorry, I don\'t support that file type :('))
|
||||
|
@ -14,13 +14,15 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.media_types.ascii.processing import process_ascii
|
||||
from mediagoblin.media_types.ascii.processing import process_ascii, \
|
||||
sniff_handler
|
||||
|
||||
|
||||
MEDIA_MANAGER = {
|
||||
"human_readable": "ASCII",
|
||||
"processor": process_ascii, # alternately a string,
|
||||
# 'mediagoblin.media_types.image.processing'?
|
||||
"sniff_handler": sniff_handler,
|
||||
"display_template": "mediagoblin/media_displays/ascii.html",
|
||||
"default_thumb": "images/media_thumbs/ascii.jpg",
|
||||
"accepted_extensions": [
|
||||
|
@ -23,6 +23,7 @@ import os
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsciiToImage(object):
|
||||
'''
|
||||
Converter of ASCII art into image files, preserving whitespace
|
||||
|
@ -19,11 +19,25 @@ import Image
|
||||
import logging
|
||||
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin.processing import create_pub_filepath, THUMB_SIZE
|
||||
from mediagoblin.processing import create_pub_filepath
|
||||
from mediagoblin.media_types.ascii import asciitoimage
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
SUPPORTED_EXTENSIONS = ['txt', 'asc', 'nfo']
|
||||
|
||||
|
||||
def sniff_handler(media_file, **kw):
|
||||
if kw.get('media') is not None:
|
||||
name, ext = os.path.splitext(kw['media'].filename)
|
||||
clean_ext = ext[1:].lower()
|
||||
|
||||
if clean_ext in SUPPORTED_EXTENSIONS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_ascii(entry):
|
||||
'''
|
||||
Code to process a txt file
|
||||
@ -69,7 +83,10 @@ def process_ascii(entry):
|
||||
queued_file.read())
|
||||
|
||||
with file(tmp_thumb_filename, 'w') as thumb_file:
|
||||
thumb.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
|
||||
thumb.thumbnail(
|
||||
(mgg.global_config['media:thumb']['max_width'],
|
||||
mgg.global_config['media:thumb']['max_height']),
|
||||
Image.ANTIALIAS)
|
||||
thumb.save(thumb_file)
|
||||
|
||||
_log.debug('Copying local file to public storage')
|
||||
@ -84,7 +101,6 @@ def process_ascii(entry):
|
||||
as original_file:
|
||||
original_file.write(queued_file.read())
|
||||
|
||||
|
||||
queued_file.seek(0) # Rewind *again*
|
||||
|
||||
unicode_filepath = create_pub_filepath(entry, 'ascii-portable.txt')
|
||||
|
25
mediagoblin/media_types/audio/__init__.py
Normal file
25
mediagoblin/media_types/audio/__init__.py
Normal file
@ -0,0 +1,25 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.media_types.audio.processing import process_audio, \
|
||||
sniff_handler
|
||||
|
||||
MEDIA_MANAGER = {
|
||||
'human_readable': 'Audio',
|
||||
'processor': process_audio,
|
||||
'sniff_handler': sniff_handler,
|
||||
'display_template': 'mediagoblin/media_displays/audio.html',
|
||||
'accepted_extensions': ['mp3', 'flac', 'ogg', 'wav', 'm4a']}
|
1
mediagoblin/media_types/audio/audioprocessing.py
Symbolic link
1
mediagoblin/media_types/audio/audioprocessing.py
Symbolic link
@ -0,0 +1 @@
|
||||
../../../extlib/freesound/audioprocessing.py
|
131
mediagoblin/media_types/audio/processing.py
Normal file
131
mediagoblin/media_types/audio/processing.py
Normal file
@ -0,0 +1,131 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin.processing import create_pub_filepath, BadMediaFail
|
||||
|
||||
from mediagoblin.media_types.audio.transcoders import AudioTranscoder, \
|
||||
AudioThumbnailer
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
def sniff_handler(media_file, **kw):
|
||||
try:
|
||||
transcoder = AudioTranscoder()
|
||||
data = transcoder.discover(media_file.name)
|
||||
except BadMediaFail:
|
||||
_log.debug('Audio discovery raised BadMediaFail')
|
||||
return False
|
||||
|
||||
if data.is_audio == True and data.is_video == False:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def process_audio(entry):
|
||||
audio_config = mgg.global_config['media_type:mediagoblin.media_types.audio']
|
||||
|
||||
workbench = mgg.workbench_manager.create_workbench()
|
||||
|
||||
queued_filepath = entry.queued_media_file
|
||||
queued_filename = workbench.localized_file(
|
||||
mgg.queue_store, queued_filepath,
|
||||
'source')
|
||||
|
||||
ogg_filepath = create_pub_filepath(
|
||||
entry,
|
||||
'{original}.webm'.format(
|
||||
original=os.path.splitext(
|
||||
queued_filepath[-1])[0]))
|
||||
|
||||
transcoder = AudioTranscoder()
|
||||
|
||||
with tempfile.NamedTemporaryFile() as ogg_tmp:
|
||||
|
||||
transcoder.transcode(
|
||||
queued_filename,
|
||||
ogg_tmp.name,
|
||||
quality=audio_config['quality'])
|
||||
|
||||
data = transcoder.discover(ogg_tmp.name)
|
||||
|
||||
_log.debug('Saving medium...')
|
||||
mgg.public_store.get_file(ogg_filepath, 'wb').write(
|
||||
ogg_tmp.read())
|
||||
|
||||
entry.media_files['ogg'] = ogg_filepath
|
||||
|
||||
entry.media_data['audio'] = {
|
||||
u'length': int(data.audiolength)}
|
||||
|
||||
if audio_config['create_spectrogram']:
|
||||
spectrogram_filepath = create_pub_filepath(
|
||||
entry,
|
||||
'{original}-spectrogram.jpg'.format(
|
||||
original=os.path.splitext(
|
||||
queued_filepath[-1])[0]))
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.wav') as wav_tmp:
|
||||
_log.info('Creating WAV source for spectrogram')
|
||||
transcoder.transcode(
|
||||
queued_filename,
|
||||
wav_tmp.name,
|
||||
mux_string='wavenc')
|
||||
|
||||
thumbnailer = AudioThumbnailer()
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpg') as spectrogram_tmp:
|
||||
thumbnailer.spectrogram(
|
||||
wav_tmp.name,
|
||||
spectrogram_tmp.name,
|
||||
width=mgg.global_config['media:medium']['max_width'])
|
||||
|
||||
_log.debug('Saving spectrogram...')
|
||||
mgg.public_store.get_file(spectrogram_filepath, 'wb').write(
|
||||
spectrogram_tmp.read())
|
||||
|
||||
entry.media_files['spectrogram'] = spectrogram_filepath
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.jpg') as thumb_tmp:
|
||||
thumbnailer.thumbnail_spectrogram(
|
||||
spectrogram_tmp.name,
|
||||
thumb_tmp.name,
|
||||
(mgg.global_config['media:thumb']['max_width'],
|
||||
mgg.global_config['media:thumb']['max_height']))
|
||||
|
||||
thumb_filepath = create_pub_filepath(
|
||||
entry,
|
||||
'{original}-thumbnail.jpg'.format(
|
||||
original=os.path.splitext(
|
||||
queued_filepath[-1])[0]))
|
||||
|
||||
mgg.public_store.get_file(thumb_filepath, 'wb').write(
|
||||
thumb_tmp.read())
|
||||
|
||||
entry.media_files['thumb'] = thumb_filepath
|
||||
else:
|
||||
entry.media_files['thumb'] = ['fake', 'thumb', 'path.jpg']
|
||||
|
||||
mgg.queue_store.delete_file(queued_filepath)
|
||||
|
||||
entry.save()
|
||||
|
||||
# clean up workbench
|
||||
workbench.destroy_self()
|
237
mediagoblin/media_types/audio/transcoders.py
Normal file
237
mediagoblin/media_types/audio/transcoders.py
Normal file
@ -0,0 +1,237 @@
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pdb
|
||||
import logging
|
||||
from PIL import Image
|
||||
|
||||
from mediagoblin.processing import BadMediaFail
|
||||
from mediagoblin.media_types.audio import audioprocessing
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
CPU_COUNT = 2 # Just assuming for now
|
||||
|
||||
# IMPORT MULTIPROCESSING
|
||||
try:
|
||||
import multiprocessing
|
||||
try:
|
||||
CPU_COUNT = multiprocessing.cpu_count()
|
||||
except NotImplementedError:
|
||||
_log.warning('multiprocessing.cpu_count not implemented!\n'
|
||||
'Assuming 2 CPU cores')
|
||||
except ImportError:
|
||||
_log.warning('Could not import multiprocessing, assuming 2 CPU cores')
|
||||
|
||||
# IMPORT GOBJECT
|
||||
try:
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
except ImportError:
|
||||
raise Exception('gobject could not be found')
|
||||
|
||||
# IMPORT PYGST
|
||||
try:
|
||||
import pygst
|
||||
|
||||
# We won't settle for less. For now, this is an arbitrary limit
|
||||
# as we have not tested with > 0.10
|
||||
pygst.require('0.10')
|
||||
|
||||
import gst
|
||||
|
||||
import gst.extend.discoverer
|
||||
except ImportError:
|
||||
raise Exception('gst/pygst >= 0.10 could not be imported')
|
||||
|
||||
import numpy
|
||||
|
||||
|
||||
class AudioThumbnailer(object):
|
||||
def __init__(self):
|
||||
_log.info('Initializing {0}'.format(self.__class__.__name__))
|
||||
|
||||
def spectrogram(self, src, dst, **kw):
|
||||
width = kw['width']
|
||||
height = int(kw.get('height', float(width) * 0.3))
|
||||
fft_size = kw.get('fft_size', 2048)
|
||||
callback = kw.get('progress_callback')
|
||||
|
||||
processor = audioprocessing.AudioProcessor(
|
||||
src,
|
||||
fft_size,
|
||||
numpy.hanning)
|
||||
|
||||
samples_per_pixel = processor.audio_file.nframes / float(width)
|
||||
|
||||
spectrogram = audioprocessing.SpectrogramImage(width, height, fft_size)
|
||||
|
||||
for x in range(width):
|
||||
if callback and x % (width / 10) == 0:
|
||||
callback((x * 100) / width)
|
||||
|
||||
seek_point = int(x * samples_per_pixel)
|
||||
|
||||
(spectral_centroid, db_spectrum) = processor.spectral_centroid(
|
||||
seek_point)
|
||||
|
||||
spectrogram.draw_spectrum(x, db_spectrum)
|
||||
|
||||
if callback:
|
||||
callback(100)
|
||||
|
||||
spectrogram.save(dst)
|
||||
|
||||
def thumbnail_spectrogram(self, src, dst, thumb_size):
|
||||
'''
|
||||
Takes a spectrogram and creates a thumbnail from it
|
||||
'''
|
||||
if not (type(thumb_size) == tuple and len(thumb_size) == 2):
|
||||
raise Exception('thumb_size argument should be a tuple(width, height)')
|
||||
|
||||
im = Image.open(src)
|
||||
|
||||
im_w, im_h = [float(i) for i in im.size]
|
||||
th_w, th_h = [float(i) for i in thumb_size]
|
||||
|
||||
wadsworth_position = im_w * 0.3
|
||||
|
||||
start_x = max((
|
||||
wadsworth_position - ((im_h * (th_w / th_h)) / 2.0),
|
||||
0.0))
|
||||
|
||||
stop_x = start_x + (im_h * (th_w / th_h))
|
||||
|
||||
th = im.crop((
|
||||
int(start_x), 0,
|
||||
int(stop_x), int(im_h)))
|
||||
|
||||
if th.size[0] > th_w or th.size[1] > th_h:
|
||||
th.thumbnail(thumb_size, Image.ANTIALIAS)
|
||||
|
||||
th.save(dst)
|
||||
|
||||
|
||||
class AudioTranscoder(object):
|
||||
def __init__(self):
|
||||
_log.info('Initializing {0}'.format(self.__class__.__name__))
|
||||
|
||||
# Instantiate MainLoop
|
||||
self._loop = gobject.MainLoop()
|
||||
self._failed = None
|
||||
|
||||
def discover(self, src):
|
||||
self._src_path = src
|
||||
_log.info('Discovering {0}'.format(src))
|
||||
self._discovery_path = src
|
||||
|
||||
self._discoverer = gst.extend.discoverer.Discoverer(
|
||||
self._discovery_path)
|
||||
self._discoverer.connect('discovered', self.__on_discovered)
|
||||
self._discoverer.discover()
|
||||
|
||||
self._loop.run() # Run MainLoop
|
||||
|
||||
if self._failed:
|
||||
raise self._failed
|
||||
|
||||
# Once MainLoop has returned, return discovery data
|
||||
return getattr(self, '_discovery_data', False)
|
||||
|
||||
def __on_discovered(self, data, is_media):
|
||||
if not is_media:
|
||||
self._failed = BadMediaFail()
|
||||
_log.error('Could not discover {0}'.format(self._src_path))
|
||||
self.halt()
|
||||
|
||||
_log.debug('Discovered: {0}'.format(data.__dict__))
|
||||
|
||||
self._discovery_data = data
|
||||
|
||||
# Gracefully shut down MainLoop
|
||||
self.halt()
|
||||
|
||||
def transcode(self, src, dst, **kw):
|
||||
_log.info('Transcoding {0} into {1}'.format(src, dst))
|
||||
self._discovery_data = kw.get('data', self.discover(src))
|
||||
|
||||
self.__on_progress = kw.get('progress_callback')
|
||||
|
||||
quality = kw.get('quality', 0.3)
|
||||
|
||||
mux_string = kw.get(
|
||||
'mux_string',
|
||||
'vorbisenc quality={0} ! webmmux'.format(quality))
|
||||
|
||||
# Set up pipeline
|
||||
self.pipeline = gst.parse_launch(
|
||||
'filesrc location="{src}" ! '
|
||||
'decodebin2 ! queue ! audiorate tolerance={tolerance} ! '
|
||||
'audioconvert ! audio/x-raw-float,channels=2 ! '
|
||||
'{mux_string} ! '
|
||||
'progressreport silent=true ! '
|
||||
'filesink location="{dst}"'.format(
|
||||
src=src,
|
||||
tolerance=80000000,
|
||||
mux_string=mux_string,
|
||||
dst=dst))
|
||||
|
||||
self.bus = self.pipeline.get_bus()
|
||||
self.bus.add_signal_watch()
|
||||
self.bus.connect('message', self.__on_bus_message)
|
||||
|
||||
self.pipeline.set_state(gst.STATE_PLAYING)
|
||||
|
||||
self._loop.run()
|
||||
|
||||
def __on_bus_message(self, bus, message):
|
||||
_log.debug(message)
|
||||
|
||||
if (message.type == gst.MESSAGE_ELEMENT
|
||||
and message.structure.get_name() == 'progress'):
|
||||
data = dict(message.structure)
|
||||
|
||||
if self.__on_progress:
|
||||
self.__on_progress(data)
|
||||
|
||||
_log.info('{0}% done...'.format(
|
||||
data.get('percent')))
|
||||
elif message.type == gst.MESSAGE_EOS:
|
||||
_log.info('Done')
|
||||
self.halt()
|
||||
|
||||
def halt(self):
|
||||
if getattr(self, 'pipeline', False):
|
||||
self.pipeline.set_state(gst.STATE_NULL)
|
||||
del self.pipeline
|
||||
_log.info('Quitting MainLoop gracefully...')
|
||||
gobject.idle_add(self._loop.quit)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
logging.basicConfig()
|
||||
_log.setLevel(logging.INFO)
|
||||
|
||||
#transcoder = AudioTranscoder()
|
||||
#data = transcoder.discover(sys.argv[1])
|
||||
#res = transcoder.transcode(*sys.argv[1:3])
|
||||
|
||||
thumbnailer = AudioThumbnailer()
|
||||
|
||||
thumbnailer.spectrogram(*sys.argv[1:], width=640)
|
||||
|
||||
pdb.set_trace()
|
@ -14,13 +14,15 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.media_types.image.processing import process_image
|
||||
from mediagoblin.media_types.image.processing import process_image, \
|
||||
sniff_handler
|
||||
|
||||
|
||||
MEDIA_MANAGER = {
|
||||
"human_readable": "Image",
|
||||
"processor": process_image, # alternately a string,
|
||||
# 'mediagoblin.media_types.image.processing'?
|
||||
"sniff_handler": sniff_handler,
|
||||
"display_template": "mediagoblin/media_displays/image.html",
|
||||
"default_thumb": "images/media_thumbs/image.jpg",
|
||||
"accepted_extensions": ["jpg", "jpeg", "png", "gif", "tiff"]}
|
||||
|
@ -16,14 +16,18 @@
|
||||
|
||||
import Image
|
||||
import os
|
||||
import logging
|
||||
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin.processing import BadMediaFail, \
|
||||
create_pub_filepath, THUMB_SIZE, MEDIUM_SIZE, FilenameBuilder
|
||||
create_pub_filepath, FilenameBuilder
|
||||
from mediagoblin.tools.exif import exif_fix_image_orientation, \
|
||||
extract_exif, clean_exif, get_gps_data, get_useful, \
|
||||
exif_image_needs_rotation
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
|
||||
size_limits=(0, 0)):
|
||||
"""Store a resized version of an image and return its pathname.
|
||||
@ -35,18 +39,13 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
|
||||
exif_tags -- EXIF data for the original image
|
||||
workdir -- directory path for storing converted image files
|
||||
new_size -- 2-tuple size for the resized image
|
||||
size_limits (optional) -- image is only resized if it exceeds this size
|
||||
|
||||
"""
|
||||
try:
|
||||
resized = Image.open(filename)
|
||||
except IOError:
|
||||
raise BadMediaFail()
|
||||
resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation
|
||||
|
||||
if ((resized.size[0] > size_limits[0]) or
|
||||
(resized.size[1] > size_limits[1])):
|
||||
resized.thumbnail(new_size, Image.ANTIALIAS)
|
||||
resized.thumbnail(new_size, Image.ANTIALIAS)
|
||||
|
||||
# Copy the new file to the conversion subdir, then remotely.
|
||||
tmp_resized_filename = os.path.join(workdir, new_path[-1])
|
||||
@ -54,6 +53,33 @@ def resize_image(entry, filename, new_path, exif_tags, workdir, new_size,
|
||||
resized.save(resized_file)
|
||||
mgg.public_store.copy_local_to_storage(tmp_resized_filename, new_path)
|
||||
|
||||
|
||||
SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg']
|
||||
|
||||
|
||||
def sniff_handler(media_file, **kw):
|
||||
if kw.get('media') is not None: # That's a double negative!
|
||||
name, ext = os.path.splitext(kw['media'].filename)
|
||||
clean_ext = ext[1:].lower() # Strip the . from ext and make lowercase
|
||||
|
||||
_log.debug('name: {0}\next: {1}\nlower_ext: {2}'.format(
|
||||
name,
|
||||
ext,
|
||||
clean_ext))
|
||||
|
||||
if clean_ext in SUPPORTED_FILETYPES:
|
||||
_log.info('Found file extension in supported filetypes')
|
||||
return True
|
||||
else:
|
||||
_log.debug('Media present, extension not found in {0}'.format(
|
||||
SUPPORTED_FILETYPES))
|
||||
else:
|
||||
_log.warning('Need additional information (keyword argument \'media\')'
|
||||
' to be able to handle sniffing')
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_image(entry):
|
||||
"""
|
||||
Code to process an image
|
||||
@ -77,19 +103,24 @@ def process_image(entry):
|
||||
thumb_filepath = create_pub_filepath(
|
||||
entry, name_builder.fill('{basename}.thumbnail{ext}'))
|
||||
resize_image(entry, queued_filename, thumb_filepath,
|
||||
exif_tags, conversions_subdir, THUMB_SIZE)
|
||||
exif_tags, conversions_subdir,
|
||||
(mgg.global_config['media:thumb']['max_width'],
|
||||
mgg.global_config['media:thumb']['max_height']))
|
||||
|
||||
# If the size of the original file exceeds the specified size of a `medium`
|
||||
# file, a `.medium.jpg` files is created and later associated with the media
|
||||
# entry.
|
||||
medium = Image.open(queued_filename)
|
||||
if medium.size[0] > MEDIUM_SIZE[0] or medium.size[1] > MEDIUM_SIZE[1] \
|
||||
or exif_image_needs_rotation(exif_tags):
|
||||
if medium.size[0] > mgg.global_config['media:medium']['max_width'] \
|
||||
or medium.size[1] > mgg.global_config['media:medium']['max_height'] \
|
||||
or exif_image_needs_rotation(exif_tags):
|
||||
medium_filepath = create_pub_filepath(
|
||||
entry, name_builder.fill('{basename}.medium{ext}'))
|
||||
resize_image(
|
||||
entry, queued_filename, medium_filepath,
|
||||
exif_tags, conversions_subdir, MEDIUM_SIZE, MEDIUM_SIZE)
|
||||
exif_tags, conversions_subdir,
|
||||
(mgg.global_config['media:medium']['max_width'],
|
||||
mgg.global_config['media:medium']['max_height']))
|
||||
else:
|
||||
medium_filepath = None
|
||||
|
||||
@ -99,7 +130,7 @@ def process_image(entry):
|
||||
|
||||
with queued_file:
|
||||
original_filepath = create_pub_filepath(
|
||||
entry, name_builder.fill('{basename}{ext}') )
|
||||
entry, name_builder.fill('{basename}{ext}'))
|
||||
|
||||
with mgg.public_store.get_file(original_filepath, 'wb') \
|
||||
as original_file:
|
||||
|
@ -14,16 +14,16 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.media_types.video.processing import process_video
|
||||
from mediagoblin.media_types.video.processing import process_video, \
|
||||
sniff_handler
|
||||
|
||||
|
||||
MEDIA_MANAGER = {
|
||||
"human_readable": "Video",
|
||||
"processor": process_video, # alternately a string,
|
||||
# 'mediagoblin.media_types.image.processing'?
|
||||
"processor": process_video, # alternately a string,
|
||||
# 'mediagoblin.media_types.image.processing'?
|
||||
"sniff_handler": sniff_handler,
|
||||
"display_template": "mediagoblin/media_displays/video.html",
|
||||
"default_thumb": "images/media_thumbs/video.jpg",
|
||||
# TODO: This list should be autogenerated based on gst plugins
|
||||
"accepted_extensions": [
|
||||
"mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "ogg",
|
||||
"m4v"]}
|
||||
"mp4", "mov", "webm", "avi", "3gp", "3gpp", "mkv", "ogv", "m4v"]}
|
||||
|
@ -20,7 +20,7 @@ import os
|
||||
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin.processing import mark_entry_failed, \
|
||||
THUMB_SIZE, MEDIUM_SIZE, create_pub_filepath, FilenameBuilder
|
||||
create_pub_filepath, FilenameBuilder
|
||||
from . import transcoders
|
||||
|
||||
logging.basicConfig()
|
||||
@ -29,17 +29,27 @@ _log = logging.getLogger(__name__)
|
||||
_log.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def sniff_handler(media_file, **kw):
|
||||
transcoder = transcoders.VideoTranscoder()
|
||||
data = transcoder.discover(media_file.name)
|
||||
|
||||
_log.debug('Discovered: {0}'.format(data))
|
||||
|
||||
if not data:
|
||||
_log.error('Could not discover {0}'.format(
|
||||
kw.get('media')))
|
||||
return False
|
||||
|
||||
if data['is_video'] == True:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_video(entry):
|
||||
"""
|
||||
Code to process a video
|
||||
|
||||
Much of this code is derived from the arista-transcoder script in
|
||||
the arista PyPI package and changed to match the needs of
|
||||
MediaGoblin
|
||||
|
||||
This function sets up the arista video encoder in some kind of new thread
|
||||
and attaches callbacks to that child process, hopefully, the
|
||||
entry-complete callback will be called when the video is done.
|
||||
Process a video entry, transcode the queued media files (originals) and
|
||||
create a thumbnail for the entry.
|
||||
"""
|
||||
video_config = mgg.global_config['media_type:mediagoblin.media_types.video']
|
||||
|
||||
@ -62,7 +72,8 @@ def process_video(entry):
|
||||
|
||||
with tmp_dst:
|
||||
# Transcode queued file to a VP8/vorbis file that fits in a 640x640 square
|
||||
transcoder = transcoders.VideoTranscoder(queued_filename, tmp_dst.name)
|
||||
transcoder = transcoders.VideoTranscoder()
|
||||
transcoder.transcode(queued_filename, tmp_dst.name)
|
||||
|
||||
# Push transcoded video to public storage
|
||||
_log.debug('Saving medium...')
|
||||
|
@ -21,12 +21,9 @@ os.putenv('GST_DEBUG_DUMP_DOT_DIR', '/tmp')
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import pdb
|
||||
import urllib
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
_log.setLevel(logging.DEBUG)
|
||||
|
||||
CPU_COUNT = 2
|
||||
try:
|
||||
@ -38,17 +35,16 @@ try:
|
||||
pass
|
||||
except ImportError:
|
||||
_log.warning('Could not import multiprocessing, defaulting to 2 CPU cores')
|
||||
pass
|
||||
|
||||
try:
|
||||
import gtk
|
||||
except:
|
||||
except ImportError:
|
||||
raise Exception('Could not find pygtk')
|
||||
|
||||
try:
|
||||
import gobject
|
||||
gobject.threads_init()
|
||||
except:
|
||||
except ImportError:
|
||||
raise Exception('gobject could not be found')
|
||||
|
||||
try:
|
||||
@ -56,7 +52,7 @@ try:
|
||||
pygst.require('0.10')
|
||||
import gst
|
||||
from gst.extend import discoverer
|
||||
except:
|
||||
except ImportError:
|
||||
raise Exception('gst/pygst 0.10 could not be found')
|
||||
|
||||
|
||||
@ -270,7 +266,7 @@ class VideoThumbnailer:
|
||||
return 0
|
||||
|
||||
try:
|
||||
return pipeline.query_duration(gst.FORMAT_TIME)[0]
|
||||
return pipeline.query_duration(gst.FORMAT_TIME)[0]
|
||||
except gst.QueryError:
|
||||
return self._get_duration(pipeline, retries + 1)
|
||||
|
||||
@ -320,12 +316,11 @@ class VideoThumbnailer:
|
||||
self.bus.disconnect(self.watch_id)
|
||||
self.bus = None
|
||||
|
||||
|
||||
def __halt_final(self):
|
||||
_log.info('Done')
|
||||
if self.errors:
|
||||
_log.error(','.join(self.errors))
|
||||
|
||||
|
||||
self.loop.quit()
|
||||
|
||||
|
||||
@ -341,10 +336,15 @@ class VideoTranscoder:
|
||||
that it was refined afterwards and therefore is done more
|
||||
correctly.
|
||||
'''
|
||||
def __init__(self, src, dst, **kwargs):
|
||||
def __init__(self):
|
||||
_log.info('Initializing VideoTranscoder...')
|
||||
|
||||
self.loop = gobject.MainLoop()
|
||||
|
||||
def transcode(self, src, dst, **kwargs):
|
||||
'''
|
||||
Transcode a video file into a 'medium'-sized version.
|
||||
'''
|
||||
self.source_path = src
|
||||
self.destination_path = dst
|
||||
|
||||
@ -358,6 +358,34 @@ class VideoTranscoder:
|
||||
self._setup()
|
||||
self._run()
|
||||
|
||||
def discover(self, src):
|
||||
'''
|
||||
Discover properties about a media file
|
||||
'''
|
||||
_log.info('Discovering {0}'.format(src))
|
||||
|
||||
self.source_path = src
|
||||
self._setup_discover(discovered_callback=self.__on_discovered)
|
||||
|
||||
self.discoverer.discover()
|
||||
|
||||
self.loop.run()
|
||||
|
||||
if hasattr(self, '_discovered_data'):
|
||||
return self._discovered_data.__dict__
|
||||
else:
|
||||
return None
|
||||
|
||||
def __on_discovered(self, data, is_media):
|
||||
_log.debug('Discovered: {0}'.format(data))
|
||||
if not is_media:
|
||||
self.__stop()
|
||||
raise Exception('Could not discover {0}'.format(self.source_path))
|
||||
|
||||
self._discovered_data = data
|
||||
|
||||
self.__stop_mainloop()
|
||||
|
||||
def _setup(self):
|
||||
self._setup_discover()
|
||||
self._setup_pipeline()
|
||||
@ -370,12 +398,14 @@ class VideoTranscoder:
|
||||
_log.debug('Initializing MainLoop()')
|
||||
self.loop.run()
|
||||
|
||||
def _setup_discover(self):
|
||||
def _setup_discover(self, **kw):
|
||||
_log.debug('Setting up discoverer')
|
||||
self.discoverer = discoverer.Discoverer(self.source_path)
|
||||
|
||||
# Connect self.__discovered to the 'discovered' event
|
||||
self.discoverer.connect('discovered', self.__discovered)
|
||||
self.discoverer.connect(
|
||||
'discovered',
|
||||
kw.get('discovered_callback', self.__discovered))
|
||||
|
||||
def __discovered(self, data, is_media):
|
||||
'''
|
||||
@ -422,7 +452,7 @@ class VideoTranscoder:
|
||||
self.ffmpegcolorspace = gst.element_factory_make(
|
||||
'ffmpegcolorspace', 'ffmpegcolorspace')
|
||||
self.pipeline.add(self.ffmpegcolorspace)
|
||||
|
||||
|
||||
self.videoscale = gst.element_factory_make('ffvideoscale', 'videoscale')
|
||||
#self.videoscale.set_property('method', 2) # I'm not sure this works
|
||||
#self.videoscale.set_property('add-borders', 0)
|
||||
@ -516,7 +546,6 @@ class VideoTranscoder:
|
||||
# Setup the message bus and connect _on_message to the pipeline
|
||||
self._setup_bus()
|
||||
|
||||
|
||||
def _on_dynamic_pad(self, dbin, pad, islast):
|
||||
'''
|
||||
Callback called when ``decodebin2`` has a pad that we can connect to
|
||||
@ -561,11 +590,11 @@ class VideoTranscoder:
|
||||
|
||||
t = message.type
|
||||
|
||||
if t == gst.MESSAGE_EOS:
|
||||
if message.type == gst.MESSAGE_EOS:
|
||||
self._discover_dst_and_stop()
|
||||
_log.info('Done')
|
||||
|
||||
elif t == gst.MESSAGE_ELEMENT:
|
||||
elif message.type == gst.MESSAGE_ELEMENT:
|
||||
if message.structure.get_name() == 'progress':
|
||||
data = dict(message.structure)
|
||||
|
||||
@ -587,7 +616,6 @@ class VideoTranscoder:
|
||||
|
||||
self.dst_discoverer.discover()
|
||||
|
||||
|
||||
def __dst_discovered(self, data, is_media):
|
||||
self.dst_data = data
|
||||
|
||||
@ -596,8 +624,9 @@ class VideoTranscoder:
|
||||
def __stop(self):
|
||||
_log.debug(self.loop)
|
||||
|
||||
# Stop executing the pipeline
|
||||
self.pipeline.set_state(gst.STATE_NULL)
|
||||
if hasattr(self, 'pipeline'):
|
||||
# Stop executing the pipeline
|
||||
self.pipeline.set_state(gst.STATE_NULL)
|
||||
|
||||
# This kills the loop, mercifully
|
||||
gobject.idle_add(self.__stop_mainloop)
|
||||
@ -615,14 +644,15 @@ class VideoTranscoder:
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.nice(19)
|
||||
logging.basicConfig()
|
||||
from optparse import OptionParser
|
||||
|
||||
parser = OptionParser(
|
||||
usage='%prog [-v] -a [ video | thumbnail ] SRC DEST')
|
||||
usage='%prog [-v] -a [ video | thumbnail | discover ] SRC [ DEST ]')
|
||||
|
||||
parser.add_option('-a', '--action',
|
||||
dest='action',
|
||||
help='One of "video" or "thumbnail"')
|
||||
help='One of "video", "discover" or "thumbnail"')
|
||||
|
||||
parser.add_option('-v',
|
||||
dest='verbose',
|
||||
@ -646,13 +676,17 @@ if __name__ == '__main__':
|
||||
|
||||
_log.debug(args)
|
||||
|
||||
if not len(args) == 2:
|
||||
if not len(args) == 2 and not options.action == 'discover':
|
||||
parser.print_help()
|
||||
sys.exit()
|
||||
|
||||
transcoder = VideoTranscoder()
|
||||
|
||||
if options.action == 'thumbnail':
|
||||
VideoThumbnailer(*args)
|
||||
elif options.action == 'video':
|
||||
def cb(data):
|
||||
print('I\'m a callback!')
|
||||
transcoder = VideoTranscoder(*args, progress_callback=cb)
|
||||
transcoder.transcode(*args, progress_callback=cb)
|
||||
elif options.action == 'discover':
|
||||
print transcoder.discover(*args).__dict__
|
||||
|
@ -24,9 +24,6 @@ from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
THUMB_SIZE = 180, 180
|
||||
MEDIUM_SIZE = 640, 640
|
||||
|
||||
|
||||
def create_pub_filepath(entry, filename):
|
||||
return mgg.public_store.get_unique_filepath(
|
||||
|
53
mediagoblin/static/css/audio.css
Normal file
53
mediagoblin/static/css/audio.css
Normal file
@ -0,0 +1,53 @@
|
||||
.audio-spectrogram {
|
||||
position: relative;
|
||||
}
|
||||
.playhead {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: rgba(134, 212, 177, 0.3);
|
||||
border-right: thin solid #ffaa00;
|
||||
height: 100%;
|
||||
-webkit-transition: width .1s ease-out;
|
||||
-moz-transition: width .1s ease-out;
|
||||
}
|
||||
.audio-control-play-pause {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 5px;
|
||||
cursor: pointer;
|
||||
/* background: rgba(0, 0, 0, 0.7); */
|
||||
font-size: 40px;
|
||||
width: 50px;
|
||||
text-shadow: 0 0 10px black;
|
||||
}
|
||||
.audio-control-play-pause.playing {
|
||||
color: #b71500;
|
||||
}
|
||||
.audio-control-play-pause.paused {
|
||||
color: rgb(134, 212, 177);
|
||||
}
|
||||
.buffered {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
width: 0;
|
||||
-webkit-transition: width 1s ease-out;
|
||||
-moz-transition: width 1s ease-out;
|
||||
background: rgba(134, 177, 212, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
.seekbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.audio-currentTime {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
146
mediagoblin/static/js/audio.js
Normal file
146
mediagoblin/static/js/audio.js
Normal file
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* GNU MediaGoblin -- federated, autonomous media hosting
|
||||
* Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var audioPlayer = new Object();
|
||||
|
||||
(function (audioPlayer) {
|
||||
audioPlayer.init = function (audioElement) {
|
||||
audioPlayer.audioElement = audioElement;
|
||||
console.log(audioElement);
|
||||
attachEvents();
|
||||
$(audioElement).hide();
|
||||
};
|
||||
|
||||
function attachEvents () {
|
||||
audioPlayer.audioElement.addEventListener('durationchange', audioPlayer.durationChange, true);
|
||||
audioPlayer.audioElement.addEventListener('timeupdate', audioPlayer.timeUpdate, true);
|
||||
audioPlayer.audioElement.addEventListener('progress', audioPlayer.onProgress, true);
|
||||
$(document).ready( function () {
|
||||
$('.audio-spectrogram').delegate('.seekbar', 'click', audioPlayer.onSeek);
|
||||
$('.audio-spectrogram').delegate('.audio-control-play-pause', 'click', audioPlayer.playPause);
|
||||
});
|
||||
}
|
||||
|
||||
audioPlayer.onProgress = function(a, b, c) {
|
||||
console.log(a, b, c);
|
||||
buffered = audioPlayer.audioElement.buffered;
|
||||
|
||||
ranges = new Array();
|
||||
|
||||
for (i = 0; i < buffered.length; i++) {
|
||||
ranges[i] = new Array();
|
||||
ranges[i][0] = buffered.start(i);
|
||||
ranges[i][1] = buffered.end(i);
|
||||
}
|
||||
console.log('ranges', ranges);
|
||||
$('.audio-spectrogram .buffered').width(
|
||||
(ranges[0][1] / audioPlayer.audioElement.duration) * audioPlayer.imageElement.width());
|
||||
};
|
||||
|
||||
audioPlayer.onSeek = function (e) {
|
||||
console.log('onSeek', e);
|
||||
im = audioPlayer.imageElement;
|
||||
pos = e.offsetX / im.width();
|
||||
audioPlayer.audioElement.currentTime = pos * audioPlayer.audioElement.duration;
|
||||
audioPlayer.audioElement.play();
|
||||
audioPlayer.setState(audioPlayer.PLAYING);
|
||||
};
|
||||
|
||||
audioPlayer.playPause = function (e) {
|
||||
console.log('playPause', e);
|
||||
if (audioPlayer.audioElement.paused) {
|
||||
audioPlayer.audioElement.play();
|
||||
audioPlayer.setState(audioPlayer.PLAYING);
|
||||
} else {
|
||||
audioPlayer.audioElement.pause();
|
||||
audioPlayer.setState(audioPlayer.PAUSED);
|
||||
}
|
||||
};
|
||||
|
||||
audioPlayer.NULL = null;
|
||||
audioPlayer.PLAYING = 2;
|
||||
audioPlayer.PAUSED = 4;
|
||||
|
||||
audioPlayer.state = audioPlayer.NULL;
|
||||
|
||||
audioPlayer.setState = function (state) {
|
||||
if (state == audioPlayer.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case audioPlayer.PLAYING:
|
||||
$('.audio-spectrogram .audio-control-play-pause')
|
||||
.removeClass('paused').addClass('playing')
|
||||
.text('■');
|
||||
break;
|
||||
case audioPlayer.PAUSED:
|
||||
$('.audio-spectrogram .audio-control-play-pause')
|
||||
.removeClass('playing').addClass('paused')
|
||||
.text('▶');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
audioPlayer.durationChange = function () {
|
||||
duration = audioPlayer.audioElement.duration;
|
||||
};
|
||||
|
||||
audioPlayer.timeUpdate = function () {
|
||||
currentTime = audioPlayer.audioElement.currentTime;
|
||||
playhead = audioPlayer.imageElement.parent().find('.playhead');
|
||||
playhead.css('width', (currentTime / audioPlayer.audioElement.duration) * audioPlayer.imageElement.width());
|
||||
time = formatTime(currentTime);
|
||||
duration = formatTime(audioPlayer.audioElement.duration);
|
||||
audioPlayer.imageElement.parent().find('.audio-currentTime').text(time + '/' + duration);
|
||||
};
|
||||
|
||||
function formatTime(seconds) {
|
||||
var h = Math.floor(seconds / (60 * 60));
|
||||
var m = Math.floor((seconds - h * 60 * 60) / 60);
|
||||
var s = Math.round(seconds - h * 60 * 60 - m * 60);
|
||||
return '' + (h ? (h < 10 ? '0' + h : h) + ':' : '') + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
|
||||
}
|
||||
|
||||
audioPlayer.attachToImage = function (imageElement) {
|
||||
/**
|
||||
* Attach the player to an image element
|
||||
*/
|
||||
console.log(imageElement);
|
||||
im = $(imageElement);
|
||||
audioPlayer.imageElement = im;
|
||||
$('<div class="playhead"></div>').appendTo(im.parent());
|
||||
$('<div class="buffered"></div>').appendTo(im.parent());
|
||||
$('<div class="seekbar"></div>').appendTo(im.parent());
|
||||
$('<div class="audio-control-play-pause paused">▶</div>').appendTo(im.parent());
|
||||
$('<div class="audio-currentTime">00:00</div>').appendTo(im.parent());
|
||||
};
|
||||
})(audioPlayer);
|
||||
|
||||
$(document).ready(function () {
|
||||
if (!$('.audio-media').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Initializing audio player');
|
||||
|
||||
audioElements = $('.audio-media .audio-player');
|
||||
audioPlayer.init(audioElements[0]);
|
||||
audioPlayer.attachToImage($('.audio-spectrogram img')[0]);
|
||||
});
|
||||
|
@ -17,6 +17,11 @@
|
||||
*/
|
||||
|
||||
$(document).ready(function () {
|
||||
if (!$('#tile-map').length) {
|
||||
return;
|
||||
}
|
||||
console.log('Initializing map');
|
||||
|
||||
var longitude = Number(
|
||||
$('#tile-map #gps-longitude').val());
|
||||
var latitude = Number(
|
||||
|
@ -20,7 +20,8 @@ from os.path import splitext
|
||||
from cgi import FieldStorage
|
||||
|
||||
from celery import registry
|
||||
import urllib,urllib2
|
||||
import urllib
|
||||
import urllib2
|
||||
import logging
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
@ -36,7 +37,7 @@ from mediagoblin.submit import forms as submit_forms
|
||||
from mediagoblin.processing import mark_entry_failed
|
||||
from mediagoblin.processing.task import ProcessMedia
|
||||
from mediagoblin.messages import add_message, SUCCESS
|
||||
from mediagoblin.media_types import get_media_type_and_manager, \
|
||||
from mediagoblin.media_types import sniff_media, \
|
||||
InvalidFileType, FileTypeNotSupported
|
||||
|
||||
|
||||
@ -56,7 +57,11 @@ def submit_start(request):
|
||||
else:
|
||||
try:
|
||||
filename = request.POST['file'].filename
|
||||
media_type, media_manager = get_media_type_and_manager(filename)
|
||||
|
||||
# Sniff the submitted media to determine which
|
||||
# media plugin should handle processing
|
||||
media_type, media_manager = sniff_media(
|
||||
request.POST['file'])
|
||||
|
||||
# create entry and save in database
|
||||
entry = request.db.MediaEntry()
|
||||
@ -131,9 +136,10 @@ def submit_start(request):
|
||||
raise
|
||||
|
||||
if mg_globals.app_config["push_urls"]:
|
||||
feed_url=request.urlgen(
|
||||
feed_url = request.urlgen(
|
||||
'mediagoblin.user_pages.atom_feed',
|
||||
qualified=True,user=request.user.username)
|
||||
qualified=True,
|
||||
user=request.user.username)
|
||||
hubparameters = {
|
||||
'hub.mode': 'publish',
|
||||
'hub.url': feed_url}
|
||||
@ -160,10 +166,9 @@ def submit_start(request):
|
||||
user=request.user.username)
|
||||
except Exception as e:
|
||||
'''
|
||||
This section is intended to catch exceptions raised in
|
||||
This section is intended to catch exceptions raised in
|
||||
mediagobling.media_types
|
||||
'''
|
||||
|
||||
if isinstance(e, InvalidFileType) or \
|
||||
isinstance(e, FileTypeNotSupported):
|
||||
submit_form.file.errors.append(
|
||||
|
61
mediagoblin/templates/mediagoblin/media_displays/audio.html
Normal file
61
mediagoblin/templates/mediagoblin/media_displays/audio.html
Normal file
@ -0,0 +1,61 @@
|
||||
{#
|
||||
# GNU MediaGoblin -- federated, autonomous media hosting
|
||||
# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
|
||||
{% extends 'mediagoblin/user_pages/media.html' %}
|
||||
|
||||
{% block mediagoblin_head %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" type="text/css" href="{{ request.staticdirect('/css/audio.css') }}" />
|
||||
<script type="text/javascript" src="{{ request.staticdirect(
|
||||
'/js/audio.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block mediagoblin_media %}
|
||||
<div class="audio-media">
|
||||
{% if 'spectrogram' in media.media_files %}
|
||||
<div class="audio-spectrogram">
|
||||
<img src="{{ request.app.public_store.file_url(
|
||||
media.media_files.spectrogram) }}"
|
||||
alt="Spectrogram" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<audio class="audio-player" controls="controls"
|
||||
preload="metadata">
|
||||
<source src="{{ request.app.public_store.file_url(
|
||||
media.media_files.ogg) }}" type="video/webm; encoding="vorbis"" />
|
||||
<div class="no_html5">
|
||||
{%- trans -%}Sorry, this audio will not work because
|
||||
your web browser does not support HTML5
|
||||
audio.{%- endtrans -%}<br/>
|
||||
{%- trans -%}You can get a modern web browser that
|
||||
can play the audio at <a href="http://getfirefox.com">
|
||||
http://getfirefox.com</a>!{%- endtrans -%}
|
||||
</div>
|
||||
</audio>
|
||||
</div>
|
||||
{% if 'original' in media.media_files %}
|
||||
<p>
|
||||
<a href="{{ request.app.public_store.file_url(
|
||||
media.media_files['original']) }}">
|
||||
{%- trans -%}
|
||||
Original
|
||||
{%- endtrans -%}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -17,20 +17,20 @@
|
||||
import urlparse
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
from nose.tools import assert_equal, assert_true, assert_false
|
||||
from pkg_resources import resource_filename
|
||||
|
||||
from mediagoblin.tests.tools import setup_fresh_app, get_test_app, \
|
||||
from mediagoblin.tests.tools import get_test_app, \
|
||||
fixture_add_user
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.processing import create_pub_filepath
|
||||
from mediagoblin.tools import template, common
|
||||
from mediagoblin.tools import template
|
||||
|
||||
|
||||
def resource(filename):
|
||||
return resource_filename('mediagoblin.tests', 'test_submission/' + filename)
|
||||
|
||||
|
||||
GOOD_JPG = resource('good.jpg')
|
||||
GOOD_PNG = resource('good.png')
|
||||
EVIL_FILE = resource('evil')
|
||||
@ -44,6 +44,7 @@ BAD_TAG_STRING = 'rage,' + 'f' * 26 + 'u' * 26
|
||||
FORM_CONTEXT = ['mediagoblin/submit/start.html', 'submit_form']
|
||||
REQUEST_CONTEXT = ['mediagoblin/user_pages/user.html', 'request']
|
||||
|
||||
|
||||
class TestSubmission:
|
||||
def setUp(self):
|
||||
self.test_app = get_test_app()
|
||||
@ -76,7 +77,7 @@ class TestSubmission:
|
||||
for key in context_keys:
|
||||
context_data = context_data[key]
|
||||
return response, context_data
|
||||
|
||||
|
||||
def upload_data(self, filename):
|
||||
return {'upload_files': [('file', filename)]}
|
||||
|
||||
@ -102,7 +103,7 @@ class TestSubmission:
|
||||
response, context = self.do_post({'title': title}, do_follow=True,
|
||||
**self.upload_data(filename))
|
||||
self.check_url(response, '/u/{0}/'.format(self.test_user.username))
|
||||
assert_true(context.has_key('mediagoblin/user_pages/user.html'))
|
||||
assert_true('mediagoblin/user_pages/user.html' in context)
|
||||
# Make sure the media view is at least reachable, logged in...
|
||||
url = '/u/{0}/m/{1}/'.format(self.test_user.username,
|
||||
title.lower().replace(' ', '-'))
|
||||
@ -190,8 +191,30 @@ class TestSubmission:
|
||||
r'^Could not extract any file extension from ".*?"$',
|
||||
str(form.file.errors[0])))
|
||||
|
||||
def test_sniffing(self):
|
||||
'''
|
||||
Test sniffing mechanism to assert that regular uploads work as intended
|
||||
'''
|
||||
template.clear_test_template_context()
|
||||
response = self.test_app.post(
|
||||
'/submit/', {
|
||||
'title': 'UNIQUE_TITLE_PLS_DONT_CREATE_OTHER_MEDIA_WITH_THIS_TITLE'
|
||||
}, upload_files=[(
|
||||
'file', GOOD_JPG)])
|
||||
|
||||
response.follow()
|
||||
|
||||
context = template.TEMPLATE_TEST_CONTEXT['mediagoblin/user_pages/user.html']
|
||||
|
||||
request = context['request']
|
||||
|
||||
media = request.db.MediaEntry.find_one({
|
||||
u'title': u'UNIQUE_TITLE_PLS_DONT_CREATE_OTHER_MEDIA_WITH_THIS_TITLE'})
|
||||
|
||||
assert media.media_type == 'mediagoblin.media_types.image'
|
||||
|
||||
def check_false_image(self, title, filename):
|
||||
# NOTE: These images should ultimately fail, but they
|
||||
# NOTE: The following 2 tests will ultimately fail, but they
|
||||
# *will* pass the initial form submission step. Instead,
|
||||
# they'll be caught as failures during the processing step.
|
||||
response, context = self.do_post({'title': title}, do_follow=True,
|
||||
|
Loading…
x
Reference in New Issue
Block a user