From c6d23038c368178beea0432a454cf7570ca032ec Mon Sep 17 00:00:00 2001 From: Thomas Fillon Date: Fri, 13 Dec 2013 20:46:28 +0100 Subject: [PATCH] TimeSide Ccore ProcessPipe: Solve #8 by providing a more user-friendly implementation than previous solution - The frames can be stored in the `ProcessPipe` frames_stack attribute if the pipe is run with argument 'stack=True' (default stack=False) - The ProcessPipe is now linked to its processors through the processor self.process_pipe attribute (this avoid confusion with `decoder.pipe` or `decoder.pipeline` (-> reflect this change in all analyzers) - After the process, during the release every processors of the pipe are removed from the pipe (except the decoder) --- doc/source/tutorial/ArrayDecoder.rst | 44 ------------------- doc/source/tutorial/frames_stack.rst | 65 ++++++++++++++++++++++++++++ doc/source/tutorial/index.rst | 2 +- tests/test_AnalyzerResult.py | 2 +- timeside/analyzer/aubio_melenergy.py | 2 +- timeside/analyzer/aubio_mfcc.py | 2 +- timeside/analyzer/aubio_pitch.py | 4 +- timeside/analyzer/aubio_specdesc.py | 2 +- timeside/analyzer/aubio_temporal.py | 10 ++--- timeside/analyzer/core.py | 2 +- timeside/analyzer/dc.py | 2 +- timeside/analyzer/level.py | 4 +- timeside/analyzer/odf.py | 4 +- timeside/analyzer/spectrogram.py | 2 +- timeside/analyzer/vamp_plugin.py | 2 +- timeside/analyzer/waveform.py | 2 +- timeside/analyzer/yaafe.py | 2 +- timeside/core.py | 38 +++++++++++++--- timeside/decoder/core.py | 3 +- 19 files changed, 119 insertions(+), 75 deletions(-) delete mode 100644 doc/source/tutorial/ArrayDecoder.rst create mode 100644 doc/source/tutorial/frames_stack.rst diff --git a/doc/source/tutorial/ArrayDecoder.rst b/doc/source/tutorial/ArrayDecoder.rst deleted file mode 100644 index 6a37158..0000000 --- a/doc/source/tutorial/ArrayDecoder.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. This file is part of TimeSide - @author: Thomas Fillon - -=============================================== - Running a pipe with previously decoded frames -=============================================== - -Example of use of the :class:`ArrayDecoder ` and :class:`Waveform analyzer ` to run a pipe with previously frames from memory on a second pass - -First, setup a :class:`FileDecoder ` on an audio file: - ->>> import timeside ->>> import numpy as np ->>> ->>> audio_file = 'http://github.com/yomguy/timeside-samples/raw/master/samples/sweep.mp3' ->>> ->>> file_decoder = timeside.decoder.FileDecoder(audio_file) - -Then, setup an arbitrary analyzer to check that both decoding process are equivalent and a :class:`Waveform analyzer ` which result will store the decoded frames: - ->>> pitch_on_file = timeside.analyzer.AubioPitch() ->>> waveform = timeside.analyzer.Waveform() - -And run the pipe: - ->>> (file_decoder | pitch_on_file | waveform).run() - -To run the second pass, we need to get back the decoded samples and the original samplerate and pass them to :class:`ArrayDecoder `: - ->>> samples = waveform.results['waveform_analyzer'].data ->>> samplerate = waveform.results['waveform_analyzer'].frame_metadata.samplerate ->>> array_decoder = timeside.decoder.ArrayDecoder(samples=samples, samplerate=samplerate) - -Then we can run a second pipe with the previously decoded frames and pass the frames to the same analyzer: - ->>> pitch_on_array = timeside.analyzer.AubioPitch() ->>> (array_decoder | pitch_on_array).run() - -To assert that the frames passed to the two analyzers are the same, we check that the results of these analyzers are equivalent: - ->>> np.allclose(pitch_on_file.results['aubio_pitch.pitch'].data, -... pitch_on_array.results['aubio_pitch.pitch'].data) -True - diff --git a/doc/source/tutorial/frames_stack.rst b/doc/source/tutorial/frames_stack.rst new file mode 100644 index 0000000..9201b0c --- /dev/null +++ b/doc/source/tutorial/frames_stack.rst @@ -0,0 +1,65 @@ +.. This file is part of TimeSide + @author: Thomas Fillon + +=============================================== + Running a pipe with previously decoded frames +=============================================== + +Example of use of the `stack` option in :func:`timeside.core.ProcessPipe.run` to run a pipe with previously decoded frames stacked in memory on a second pass. + +>>> import timeside +>>> import numpy as np +>>> audio_file = 'http://github.com/yomguy/timeside-samples/raw/master/samples/sweep.mp3' +>>> decoder = timeside.decoder.FileDecoder(audio_file) + +Setup an arbitrary analyzer to check that decoding process from file and from stack are equivalent: + +>>> pitch_on_file = timeside.analyzer.AubioPitch() +>>> myPipe = (decoder | pitch_on_file) +>>> print myPipe.processors #doctest: +ELLIPSIS +[, ] + +If the pipe is run with the default argument `stack=False`, the other processes of the pipe are released from the pipe after the run and only the :class:`fileDecoder ` is kept in the pipe: + +>>> myPipe.run() +>>> print myPipe.processors #doctest: +ELLIPSIS +[] + + +If the pipe is run with the argument `stack=True`, the processed frames are stored in the pipe attribute `frames_stack`. +The other processes of the pipe are also released from the pipe after the run but the :class:`fileDecoder ` is replaced by an :class:`ArrayDecoder `: + +>>> myPipe = (decoder | pitch_on_file) +>>> myPipe.run(stack=True) +>>> print myPipe.processors #doctest: +ELLIPSIS +[] + +The stack + +>>> myPipe.frames_stack #doctest: +ELLIPSIS +array([[...]], dtype=float32) + + +Then we can run a second pipe with the previously decoded frames and pass the frames to the same analyzer. + +Define a second analyzer equivalent to the previous one: + +>>> pitch_on_stack = timeside.analyzer.AubioPitch() + +Add it to the pipe: + +>>> myPipe |= pitch_on_stack +>>> print myPipe.processors #doctest: +ELLIPSIS +[, ] + + +Run the pipe: + +>>> myPipe.run() + +Assert that the frames passed to the two analyzers are the same, we check that the results of these analyzers are equivalent: + +>>> np.allclose(pitch_on_file.results['aubio_pitch.pitch'].data, +... pitch_on_stack.results['aubio_pitch.pitch'].data) +True + diff --git a/doc/source/tutorial/index.rst b/doc/source/tutorial/index.rst index 333e314..5b6d893 100644 --- a/doc/source/tutorial/index.rst +++ b/doc/source/tutorial/index.rst @@ -13,6 +13,6 @@ Contents: Quick start Usage of AnalyzerResult - Running a pipe with previously decoded frames + Running a pipe with previously decoded frames diff --git a/tests/test_AnalyzerResult.py b/tests/test_AnalyzerResult.py index 45cc2c7..e0dd58d 100755 --- a/tests/test_AnalyzerResult.py +++ b/tests/test_AnalyzerResult.py @@ -236,4 +236,4 @@ class TestAnalyzerResultAsDict(TestAnalyzerResult): self.result.as_dict().keys()) if __name__ == '__main__': - unittest.main(testRunner=TestRunner()) \ No newline at end of file + unittest.main(testRunner=TestRunner()) diff --git a/timeside/analyzer/aubio_melenergy.py b/timeside/analyzer/aubio_melenergy.py index 451a0ae..23f277d 100644 --- a/timeside/analyzer/aubio_melenergy.py +++ b/timeside/analyzer/aubio_melenergy.py @@ -78,4 +78,4 @@ class AubioMelEnergy(Analyzer): melenergy.parameters = dict(n_filters=self.n_filters, n_coeffs=self.n_coeffs) melenergy.data_object.value = self.melenergy_results - self.pipe.results.add(melenergy) + self.process_pipe.results.add(melenergy) diff --git a/timeside/analyzer/aubio_mfcc.py b/timeside/analyzer/aubio_mfcc.py index 0b44368..1d43c3d 100644 --- a/timeside/analyzer/aubio_mfcc.py +++ b/timeside/analyzer/aubio_mfcc.py @@ -80,4 +80,4 @@ class AubioMfcc(Analyzer): mfcc.parameters = dict(n_filters=self.n_filters, n_coeffs=self.n_coeffs) mfcc.data_object.value = self.mfcc_results - self.pipe.results.add(mfcc) + self.process_pipe.results.add(mfcc) diff --git a/timeside/analyzer/aubio_pitch.py b/timeside/analyzer/aubio_pitch.py index 580b994..c280d06 100644 --- a/timeside/analyzer/aubio_pitch.py +++ b/timeside/analyzer/aubio_pitch.py @@ -85,11 +85,11 @@ class AubioPitch(Analyzer): pitch.id_metadata.name += ' ' + "pitch" pitch.id_metadata.unit = "Hz" pitch.data_object.value = self.pitches - self.pipe.results.add(pitch) + self.process_pipe.results.add(pitch) pitch_confidence = self.new_result(data_mode='value', time_mode='framewise') pitch_confidence.id_metadata.id += '.' + "pitch_confidence" pitch_confidence.id_metadata.name += ' ' + "pitch confidence" pitch_confidence.id_metadata.unit = None pitch_confidence.data_object.value = self.pitch_confidences - self.pipe.results.add(pitch_confidence) + self.process_pipe.results.add(pitch_confidence) diff --git a/timeside/analyzer/aubio_specdesc.py b/timeside/analyzer/aubio_specdesc.py index 931969d..2b9cf82 100644 --- a/timeside/analyzer/aubio_specdesc.py +++ b/timeside/analyzer/aubio_specdesc.py @@ -92,4 +92,4 @@ class AubioSpecdesc(Analyzer): res_specdesc.id_metadata.name = ' ' + method res_specdesc.data_object.value = self.specdesc_results[method] - self.pipe.results.add(res_specdesc) + self.process_pipe.results.add(res_specdesc) diff --git a/timeside/analyzer/aubio_temporal.py b/timeside/analyzer/aubio_temporal.py index 59e7c4a..b1e3746 100644 --- a/timeside/analyzer/aubio_temporal.py +++ b/timeside/analyzer/aubio_temporal.py @@ -96,7 +96,7 @@ class AubioTemporal(Analyzer): onsets.data_object.label = numpy.ones(len(self.onsets)) onsets.label_metadata.label = {1: 'Onset'} - self.pipe.results.add(onsets) + self.process_pipe.results.add(onsets) #--------------------------------- # Onset Rate: Segment (time, duration, value) @@ -115,7 +115,7 @@ class AubioTemporal(Analyzer): onsetrate.data_object.value = [] onsetrate.data_object.time = [] - self.pipe.results.add(onsetrate) + self.process_pipe.results.add(onsetrate) #--------------------------------- # Beats: Event (time, "Beat") @@ -128,7 +128,7 @@ class AubioTemporal(Analyzer): beats.data_object.label = numpy.ones(len(self.beats)) beats.label_metadata.label = {1: 'Beat'} - self.pipe.results.add(beats) + self.process_pipe.results.add(beats) #--------------------------------- # Beat confidences: Event (time, value) @@ -140,7 +140,7 @@ class AubioTemporal(Analyzer): beat_confidences.data_object.time = self.beats beat_confidences.data_object.value = self.beat_confidences - self.pipe.results.add(beat_confidences) + self.process_pipe.results.add(beat_confidences) #--------------------------------- # BPM: Segment (time, duration, value) @@ -158,4 +158,4 @@ class AubioTemporal(Analyzer): else: bpm.data_object.value = [] - self.pipe.results.add(bpm) + self.process_pipe.results.add(bpm) diff --git a/timeside/analyzer/core.py b/timeside/analyzer/core.py index c48e0ff..9da3049 100644 --- a/timeside/analyzer/core.py +++ b/timeside/analyzer/core.py @@ -984,7 +984,7 @@ class Analyzer(Processor): def results(self): return AnalyzerResultContainer( - [self.pipe.results[key] for key in self.pipe.results.keys() + [self.process_pipe.results[key] for key in self.process_pipe.results.keys() if key.split('.')[0] == self.id()]) @staticmethod diff --git a/timeside/analyzer/dc.py b/timeside/analyzer/dc.py index bee0a21..446d23e 100644 --- a/timeside/analyzer/dc.py +++ b/timeside/analyzer/dc.py @@ -61,4 +61,4 @@ class MeanDCShift(Analyzer): dc_result = self.new_result(data_mode='value', time_mode='global') dc_result.data_object.value = numpy.round( numpy.mean(100 * self.values), 3) - self.pipe.results.add(dc_result) + self.process_pipe.results.add(dc_result) diff --git a/timeside/analyzer/level.py b/timeside/analyzer/level.py index 007d526..7151ab9 100644 --- a/timeside/analyzer/level.py +++ b/timeside/analyzer/level.py @@ -72,7 +72,7 @@ class Level(Analyzer): max_level.id_metadata.name += ' ' + "Max" max_level.data_object.value = np.round(20*np.log10(self.max_value), 3) - self.pipe.results.add(max_level) + self.process_pipe.results.add(max_level) # RMS level rms_level = self.new_result(data_mode='value', time_mode='global') @@ -81,5 +81,5 @@ class Level(Analyzer): rms_level.data_object.value = np.round(20*np.log10( np.sqrt(np.mean(self.mean_values))), 3) - self.pipe.results.add(rms_level) + self.process_pipe.results.add(rms_level) diff --git a/timeside/analyzer/odf.py b/timeside/analyzer/odf.py index 42eb0bc..91cd1ae 100644 --- a/timeside/analyzer/odf.py +++ b/timeside/analyzer/odf.py @@ -70,7 +70,7 @@ class OnsetDetectionFunction(Analyzer): def post_process(self): #spectrogram = self.parents()[0]['spectrogram_analyzer'].data - spectrogram = self.pipe.results['spectrogram_analyzer'].data + spectrogram = self.process_pipe.results['spectrogram_analyzer'].data #spectrogram = self.pipe._results[self.parents()[0].id] # Low-pass filtering of the spectrogram amplitude along the time axis @@ -108,4 +108,4 @@ class OnsetDetectionFunction(Analyzer): odf = self.new_result(data_mode='value', time_mode='framewise') #odf.parameters = {'FFT_SIZE': self.FFT_SIZE} odf.data_object.value = odf_diff - self.pipe.results.add(odf) + self.process_pipe.results.add(odf) diff --git a/timeside/analyzer/spectrogram.py b/timeside/analyzer/spectrogram.py index 9728c6a..587d7f4 100644 --- a/timeside/analyzer/spectrogram.py +++ b/timeside/analyzer/spectrogram.py @@ -72,4 +72,4 @@ class Spectrogram(Analyzer): spectrogram = self.new_result(data_mode='value', time_mode='framewise') spectrogram.parameters = {'FFT_SIZE': self.FFT_SIZE} spectrogram.data_object.value = self.values - self.pipe.results.add(spectrogram) + self.process_pipe.results.add(spectrogram) diff --git a/timeside/analyzer/vamp_plugin.py b/timeside/analyzer/vamp_plugin.py index 68d1eb9..7f5f65b 100644 --- a/timeside/analyzer/vamp_plugin.py +++ b/timeside/analyzer/vamp_plugin.py @@ -110,7 +110,7 @@ class VampSimpleHost(Analyzer): plugin_res.id_metadata.name += ' ' + \ ' '.join(plugin_line[1:]) - self.pipe.results.add(plugin_res) + self.process_pipe.results.add(plugin_res) @staticmethod def vamp_plugin(plugin, wavfile): diff --git a/timeside/analyzer/waveform.py b/timeside/analyzer/waveform.py index e1289f0..0a14c76 100644 --- a/timeside/analyzer/waveform.py +++ b/timeside/analyzer/waveform.py @@ -67,4 +67,4 @@ class Waveform(Analyzer): def post_process(self): waveform = self.new_result(data_mode='value', time_mode='framewise') waveform.data_object.value = np.vstack(self.values) - self.pipe.results.add(waveform) + self.process_pipe.results.add(waveform) diff --git a/timeside/analyzer/yaafe.py b/timeside/analyzer/yaafe.py index 45af517..5e00a08 100644 --- a/timeside/analyzer/yaafe.py +++ b/timeside/analyzer/yaafe.py @@ -114,4 +114,4 @@ class Yaafe(Analyzer): result.data_object.value = self.yaafe_engine.readOutput(featName) # Store results in Container if len(result.data_object.value): - self.pipe.results.add(result) + self.process_pipe.results.add(result) diff --git a/timeside/core.py b/timeside/core.py index b937de6..4f5a54b 100644 --- a/timeside/core.py +++ b/timeside/core.py @@ -21,6 +21,8 @@ from timeside.component import * from timeside.api import IProcessor from timeside.exceptions import Error, ApiError + + import re import time import numpy @@ -244,18 +246,15 @@ class ProcessPipe(object): from timeside.analyzer.core import AnalyzerResultContainer self.results = AnalyzerResultContainer() - for proc in self.processors: - proc.pipe = self - def __or__(self, other): return ProcessPipe(self, other) def __ior__(self, other): if isinstance(other, Processor): - parents = other.parents - for parent in parents: + for parent in other.parents: self |= parent self.processors.append(other) + other.process_pipe = self elif isinstance(other, ProcessPipe): self.processors.extend(other.processors) else: @@ -277,13 +276,22 @@ class ProcessPipe(object): pipe += ' | ' return pipe - def run(self, channels = None, samplerate = None, blocksize = None): + def run(self, channels=None, samplerate=None, blocksize=None, stack=None): """Setup/reset all processors in cascade and stream audio data along the pipe. Also returns the pipe itself.""" source = self.processors[0] items = self.processors[1:] - source.setup(channels = channels, samplerate = samplerate, blocksize = blocksize) + source.setup(channels=channels, samplerate=samplerate, + blocksize=blocksize) + + if stack is None: + self.stack = False + else: + self.stack = stack + + if self.stack: + self.frames_stack = [] last = source @@ -300,11 +308,27 @@ class ProcessPipe(object): eod = False while not eod: frames, eod = source.process() + if self.stack: + self.frames_stack.append(frames) for item in items: frames, eod = item.process(frames, eod) + # Post-processing for item in items: item.post_process() + # Release processors + if self.stack: + if not isinstance(self.frames_stack, numpy.ndarray): + self.frames_stack = numpy.vstack(self.frames_stack) + from timeside.decoder.core import ArrayDecoder + new_source = ArrayDecoder(samples=self.frames_stack, + samplerate=source.samplerate()) + new_source.setup(channels=source.channels(), + samplerate=source.samplerate(), + blocksize=source.blocksize()) + self.processors[0] = new_source + for item in items: item.release() + self.processors.remove(item) diff --git a/timeside/decoder/core.py b/timeside/decoder/core.py index 9fa1f7e..dbee2a0 100644 --- a/timeside/decoder/core.py +++ b/timeside/decoder/core.py @@ -62,7 +62,7 @@ class FileDecoder(Processor): def id(): return "gst_dec" - def __init__(self, uri, start = 0, duration = None): + def __init__(self, uri, start=0, duration=None): """ Construct a new FileDecoder @@ -520,4 +520,3 @@ if __name__ == "__main__": from tests import test_decoding, test_array_decoding run_test_module([test_decoding, test_array_decoding]) - -- 2.39.5