Power Sequencer: fix package name and import error

Fixes an error with not passing the python tests.
This commit is contained in:
Nathan Lovato 2020-05-14 15:11:08 -06:00
parent c9f09d722a
commit 55b820186e
12 changed files with 10 additions and 629 deletions

View File

@ -22,14 +22,18 @@ def get_operator_classes():
"""Returns the list of operators in the add-on"""
this_file = os.path.dirname(__file__)
module_files = [
f for f in os.listdir(this_file) if f.endswith(".py") and not f.startswith("__init__")
f
for f in os.listdir(this_file)
if f.endswith(".py") and not f.startswith("__init__")
]
module_paths = ["." + os.path.splitext(f)[0] for f in module_files]
classes = []
print(__name__)
for path in module_paths:
module = importlib.import_module(path, package="blender_power_sequencer.operators")
operator_names = [entry for entry in dir(module) if entry.startswith("POWER_SEQUENCER_OT")]
module = importlib.import_module(path, package="power_sequencer.operators")
operator_names = [
entry for entry in dir(module) if entry.startswith("POWER_SEQUENCER_OT")
]
classes.extend([getattr(module, name) for name in operator_names])
return classes
@ -38,7 +42,9 @@ doc = {
"sequencer.refresh_all": {
"name": "Refresh All",
"description": "",
"shortcuts": [({"type": "R", "value": "PRESS", "shift": True}, {}, "Refresh All")],
"shortcuts": [
({"type": "R", "value": "PRESS", "shift": True}, {}, "Refresh All")
],
"demo": "",
"keymap": "Sequencer",
}

View File

@ -1,110 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
import bpy
import subprocess
from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
class POWER_SEQUENCER_OT_align_audios(bpy.types.Operator):
"""*brief* Align two audio strips
Tries to synchronize the selected audio strip to the active audio strip by comparing the sound.
Useful to synchronize audio of the same event recorded with different microphones.
To use this feature, you must have [ffmpeg](https://www.ffmpeg.org/download.html) and
[scipy](https://www.scipy.org/install.html) installed on your computer and available on the PATH (command line) to work.
The longer the audio files, the longer the tool can take to run, as it has to convert, analyze,
and compare the audio sources to work.
"""
doc = {
"name": doc_name(__qualname__),
"demo": "https://i.imgur.com/xkBUzDj.gif",
"description": doc_description(__doc__),
"shortcuts": [],
"keymap": "Sequencer",
}
bl_idname = doc_idname(__qualname__)
bl_label = doc["name"]
bl_description = doc_brief(doc["description"])
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if not context.scene:
return False
active = context.scene.sequence_editor.active_strip
selected = context.selected_sequences
ok = (
len(selected) == 2
and active in selected
and all(map(lambda s: s.type == "SOUND", selected))
)
return ok
def execute(self, context):
try:
import scipy
except ImportError:
self.report({"ERROR"}, "Scipy must be installed to align audios")
return {"FINISHED"}
if not is_ffmpeg_available():
self.report({"ERROR"}, "ffmpeg must be installed to align audios")
return {"FINISHED"}
# This import is here because otherwise, it slows down blender startup
from .audiosync import find_offset
scene = context.scene
active = scene.sequence_editor.active_strip
active_filepath = bpy.path.abspath(active.sound.filepath)
selected = context.selected_sequences
selected.pop(selected.index(active))
align_strip = selected[0]
align_strip_filepath = bpy.path.abspath(align_strip.sound.filepath)
offset, score = find_offset(align_strip_filepath, active_filepath)
initial_offset = active.frame_start - align_strip.frame_start
fps = scene.render.fps / scene.render.fps_base
frames = int(offset * fps)
align_strip.frame_start -= frames - initial_offset
self.report({"INFO"}, "Alignment score: " + str(round(score, 1)))
return {"FINISHED"}
def is_ffmpeg_available():
"""
Returns true if ffmpeg is installed and available from the PATH
"""
try:
subprocess.call(["ffmpeg", "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return True
except OSError:
return False

View File

@ -1,17 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
from .find_offset import find_offset

View File

@ -1,60 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
import subprocess
import tempfile
def convert_and_trim(audio_filepath, freq, dur):
"""
Uses ffmpeg to convert an audio file to a temporary wav file for use
in finding offset.
Args
:audio: path to the audiofile to convert (string)
:freq: Samples / second in the output wav (int)
:dur: Max duration of the output wav in seconds (float)
Returns
:outpath: path to the output wav file
"""
tmp = tempfile.NamedTemporaryFile(mode="r+b", prefix="offset_", suffix=".wav")
outpath = tmp.name
tmp.close()
channel_count = "1"
subprocess.call(
[
"ffmpeg",
"-loglevel",
"panic",
"-i",
audio_filepath,
"-ac",
channel_count,
"-ar",
str(freq),
"-t",
str(dur),
"-acodec",
"pcm_s16le",
outpath,
]
)
return outpath

View File

@ -1,33 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
import numpy as np
def cross_correlation(mfcc1, mfcc2, nframes):
n1, mdim1 = mfcc1.shape
# n2, mdim2 = mfcc2.shape
n = n1 - nframes + 1
if n < 0:
return None
c = np.zeros(n)
for k in range(n):
cc = np.sum(np.multiply(mfcc1[k : k + nframes], mfcc2[:nframes]), axis=0)
c[k] = np.linalg.norm(cc)
return c

View File

@ -1,28 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
import numpy as np
def ensure_non_zero(signal):
"""
Adds a little bit of static to avoid
'divide by zero encountered in log' during MFCC computation.
"""
signal += np.random.random(len(signal)) * 10 ** -10
return signal

View File

@ -1,87 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
"""
This code is an adaptation of 'audio-offset-finder' by BBC
"""
import os
import numpy as np
from .mfcc import mfcc
from .convert_and_trim import convert_and_trim
from .std_mfcc import std_mfcc
from .cross_correlation import cross_correlation
from .ensure_non_zero import ensure_non_zero
def find_offset(file1, file2, freq=8000, trim=60 * 15, correl_nframes=1000):
"""
Determine the offset (in seconds) between 2 audio files
Uses cross-correlation of standardised Mel-Frequency Cepstral
Coefficients
"""
from scipy.io import wavfile
file1 = os.path.abspath(file1)
file2 = os.path.abspath(file2)
wav1_path = convert_and_trim(file1, freq, trim)
wav2_path = convert_and_trim(file2, freq, trim)
rate1, data1 = wavfile.read(wav1_path, mmap=True)
data1 = data1 / (2.0 ** 15)
rate2, data2 = wavfile.read(wav2_path, mmap=True)
data2 = data2 / (2.0 ** 15)
data1 = ensure_non_zero(data1)
data2 = ensure_non_zero(data2)
mfcc1 = mfcc(data1, nwin=256, nfft=512, fs=freq, nceps=13)[0]
mfcc2 = mfcc(data2, nwin=256, nfft=512, fs=freq, nceps=13)[0]
mfcc1 = std_mfcc(mfcc1)
mfcc2 = std_mfcc(mfcc2)
frames1 = mfcc1.shape[0]
frames2 = mfcc2.shape[0]
if frames1 > frames2:
flip = 1
else:
flip = -1
mfcc1, mfcc2 = mfcc2, mfcc1
c = cross_correlation(mfcc1, mfcc2, nframes=correl_nframes)
try:
c.any()
except AttributeError:
os.remove(wav1_path)
os.remove(wav2_path)
return 0, 0
max_k_index = np.argmax(c)
offset = max_k_index * 160.0 / float(freq)
score = (c[max_k_index] - np.mean(c)) / np.std(c)
os.remove(wav1_path)
os.remove(wav2_path)
return offset * flip, score

View File

@ -1,17 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
from .mfcc import mfcc

View File

@ -1,91 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
import numpy as np
from scipy.signal import hamming, lfilter
from scipy.fftpack import fft
from scipy.fftpack.realtransforms import dct
from .trfbank import trfbank
from .segment_axis import segment_axis
def mfcc(input, nwin=256, nfft=512, fs=16000, nceps=13):
"""Compute Mel Frequency Cepstral Coefficients.
Parameters
----------
input: ndarray
input from which the coefficients are computed
Returns
-------
ceps: ndarray
Mel-cepstrum coefficients
mspec: ndarray
Log-spectrum in the mel-domain.
Notes
-----
MFCC are computed as follows:
* Pre-processing in time-domain (pre-emphasizing)
* Compute the spectrum amplitude by windowing with a Hamming window
* Filter the signal in the spectral domain with a triangular
filter-bank, whose filters are approximatively linearly spaced on the
mel scale, and have equal bandwidth in the mel scale
* Compute the DCT of the log-spectrum
References
----------
.. [1] S.B. Davis and P. Mermelstein, "Comparison of parametric
representations for monosyllabic word recognition in continuously
spoken sentences", IEEE Trans. Acoustics. Speech, Signal Proc.
ASSP-28 (4): 357-366, August 1980."""
# MFCC parameters: taken from auditory toolbox
over = nwin - 160
# Pre-emphasis factor (to take into account the -6dB/octave rolloff of the
# radiation at the lips level)
prefac = 0.97
# lowfreq = 400 / 3.
lowfreq = 133.33
# highfreq = 6855.4976
linsc = 200 / 3.0
logsc = 1.0711703
nlinfil = 13
nlogfil = 27
w = hamming(nwin, sym=0)
fbank = trfbank(fs, nfft, lowfreq, linsc, logsc, nlinfil, nlogfil)[0]
# ------------------
# Compute the MFCC
# ------------------
extract = lfilter([1.0, -prefac], 1, input)
framed = segment_axis(extract, nwin, over) * w
# Compute the spectrum magnitude
spec = np.abs(fft(framed, nfft, axis=-1))
# Filter the spectrum through the triangle filterbank
mspec = np.log10(np.dot(spec, fbank.T))
# Use the DCT to 'compress' the coefficients (spectrum -> cepstrum domain)
ceps = dct(mspec, type=2, norm="ortho", axis=-1)[:, :nceps]
return ceps, mspec, spec

View File

@ -1,110 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
import numpy as np
import warnings
def segment_axis(a, length, overlap=0, axis=None, end="cut", endvalue=0):
"""Generate a new array that chops the given array along the given axis
into overlapping frames.
example:
>>> segment_axis(arange(10), 4, 2)
array([[0, 1, 2, 3],
[2, 3, 4, 5],
[4, 5, 6, 7],
[6, 7, 8, 9]])
arguments:
a The array to segment
length The length of each frame
overlap The number of array elements by which the frames should overlap
axis The axis to operate on; if None, act on the flattened array
end What to do with the last frame, if the array is not evenly
divisible into pieces. Options are:
'cut' Simply discard the extra values
'wrap' Copy values from the beginning of the array
'pad' Pad with a constant value
endvalue The value to use for end='pad'
The array is not copied unless necessary (either because it is unevenly
strided and being flattened or because end is set to 'pad' or 'wrap').
"""
if axis is None:
a = np.ravel(a) # may copy
axis = 0
l = a.shape[axis]
if overlap >= length:
raise ValueError("frames cannot overlap by more than 100%")
if overlap < 0 or length <= 0:
raise ValueError("overlap must be nonnegative and length must " "be positive")
if l < length or (l - length) % (length - overlap):
if l > length:
roundup = length + (1 + (l - length) // (length - overlap)) * (length - overlap)
rounddown = length + ((l - length) // (length - overlap)) * (length - overlap)
else:
roundup = length
rounddown = 0
assert rounddown < l < roundup
assert roundup == rounddown + (length - overlap) or (roundup == length and rounddown == 0)
a = a.swapaxes(-1, axis)
if end == "cut":
a = a[..., :rounddown]
elif end in ["pad", "wrap"]: # copying will be necessary
s = list(a.shape)
s[-1] = roundup
b = np.empty(s, dtype=a.dtype)
b[..., :l] = a
if end == "pad":
b[..., l:] = endvalue
elif end == "wrap":
b[..., l:] = a[..., : roundup - l]
a = b
a = a.swapaxes(-1, axis)
l = a.shape[axis]
if l == 0:
raise ValueError(
"Not enough data points to segment array in 'cut' mode; " "try 'pad' or 'wrap'"
)
assert l >= length
assert (l - length) % (length - overlap) == 0
n = 1 + (l - length) // (length - overlap)
s = a.strides[axis]
newshape = a.shape[:axis] + (n, length) + a.shape[axis + 1 :]
newstrides = a.strides[:axis] + ((length - overlap) * s, s) + a.strides[axis + 1 :]
try:
return np.ndarray.__new__(
np.ndarray, strides=newstrides, shape=newshape, buffer=a, dtype=a.dtype
)
except TypeError:
warnings.warn("Problem with ndarray creation forces copy.")
a = a.copy()
# Shape doesn't change but strides does
newstrides = a.strides[:axis] + ((length - overlap) * s, s) + a.strides[axis + 1 :]
return np.ndarray.__new__(
np.ndarray, strides=newstrides, shape=newshape, buffer=a, dtype=a.dtype
)

View File

@ -1,51 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
import numpy as np
def trfbank(fs, nfft, lowfreq, linsc, logsc, nlinfilt, nlogfilt):
"""Compute triangular filterbank for MFCC computation."""
# Total number of filters
nfilt = nlinfilt + nlogfilt
# ------------------------
# Compute the filter bank
# ------------------------
# Compute start/middle/end points of the triangular filters in spectral
# domain
freqs = np.zeros(nfilt + 2)
freqs[:nlinfilt] = lowfreq + np.arange(nlinfilt) * linsc
freqs[nlinfilt:] = freqs[nlinfilt - 1] * logsc ** np.arange(1, nlogfilt + 3)
heights = 2.0 / (freqs[2:] - freqs[0:-2])
# Compute filterbank coeff (in fft domain, in bins)
fbank = np.zeros((nfilt, nfft))
# FFT bins (in Hz)
nfreqs = np.arange(nfft) / (1.0 * nfft) * fs
for i in range(nfilt):
low = freqs[i]
cen = freqs[i + 1]
hi = freqs[i + 2]
lid = np.arange(np.floor(low * nfft / fs) + 1, np.floor(cen * nfft / fs) + 1, dtype=np.int)
lslope = heights[i] / (cen - low)
rid = np.arange(np.floor(cen * nfft / fs) + 1, np.floor(hi * nfft / fs) + 1, dtype=np.int)
rslope = heights[i] / (hi - cen)
fbank[i][lid] = lslope * (nfreqs[lid] - low)
fbank[i][rid] = rslope * (hi - nfreqs[rid])
return fbank, freqs

View File

@ -1,21 +0,0 @@
#
# Copyright (C) 2016-2020 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
import numpy as np
def std_mfcc(mfcc):
return (mfcc - np.mean(mfcc, axis=0)) / np.std(mfcc, axis=0)