From 471f1e20183fbf73b5a27eaca524f178a2a74be3 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Tue, 23 Dec 2014 00:05:06 +0100 Subject: [PATCH 001/481] almost working --- src/effects/native/nativebackend.cpp | 2 + src/effects/native/paneffect.cpp | 166 +++++++++++++++++++++++++++ src/effects/native/paneffect.h | 65 +++++++++++ 3 files changed, 233 insertions(+) create mode 100644 src/effects/native/paneffect.cpp create mode 100644 src/effects/native/paneffect.h diff --git a/src/effects/native/nativebackend.cpp b/src/effects/native/nativebackend.cpp index 2dcc3482f09..7f437614f93 100644 --- a/src/effects/native/nativebackend.cpp +++ b/src/effects/native/nativebackend.cpp @@ -13,6 +13,7 @@ #include "effects/native/reverbeffect.h" #endif #include "effects/native/echoeffect.h" +#include "effects/native/paneffect.h" NativeBackend::NativeBackend(QObject* pParent) : EffectsBackend(pParent, tr("Native")) { @@ -30,6 +31,7 @@ NativeBackend::NativeBackend(QObject* pParent) // Fancy effects registerEffect(); registerEffect(); + registerEffect(); #ifndef __MACAPPSTORE__ registerEffect(); #endif diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp new file mode 100644 index 00000000000..e152dab4e4d --- /dev/null +++ b/src/effects/native/paneffect.cpp @@ -0,0 +1,166 @@ +#include + +#include "effects/native/paneffect.h" + +#include "sampleutil.h" + +#define INCREMENT_RING(index, increment, length) index = (index + increment) % length +#define RAMP_LENGTH 500 + +// static +QString PanEffect::getId() { + return "org.mixxx.effects.pan"; +} + +// static +EffectManifest PanEffect::getManifest() { + EffectManifest manifest; + manifest.setId(getId()); + manifest.setName(QObject::tr("Pan")); + manifest.setAuthor("The Mixxx Team"); + manifest.setVersion("1.0"); + manifest.setDescription(QObject::tr("Simple Pan")); + + EffectManifestParameter* depth = manifest.addParameter(); + depth->setId("depth"); + depth->setName(QObject::tr("Depth")); + depth->setDescription("Controls the intensity of the effect."); + depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + depth->setMinimum(0.0); + depth->setMaximum(1.0); + depth->setDefault(0.5); + + EffectManifestParameter* strength = manifest.addParameter(); + strength->setId("strength"); + strength->setName(QObject::tr("Strength")); + strength->setDescription( + QObject::tr("How fast the signal goes from a channel to an other")); + strength->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + strength->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + strength->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + strength->setMinimum(0.1); + strength->setMaximum(0.9); + strength->setDefault(0.5); + + EffectManifestParameter* period = manifest.addParameter(); + period->setId("period"); + period->setName(QObject::tr("Period")); + period->setDescription("Controls the speed of the effect."); + period->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + period->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + period->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + period->setDefault(250.0); + period->setMinimum(1.0); + period->setMaximum(500.0); + + return manifest; +} + +PanEffect::PanEffect(EngineEffect* pEffect, const EffectManifest& manifest) + : + m_pDepthParameter(pEffect->getParameterById("depth")), + m_pStrengthParameter(pEffect->getParameterById("strength")), + m_pPeriodParameter(pEffect->getParameterById("period")) + { + Q_UNUSED(manifest); +} + +PanEffect::~PanEffect() { + //qDebug() << debugString() << "destroyed"; +} + +void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, + const CSAMPLE* pInput, + CSAMPLE* pOutput, const unsigned int numSamples, + const unsigned int sampleRate, + const EffectProcessor::EnableState enableState, + const GroupFeatureState& groupFeatures) { + Q_UNUSED(group); + Q_UNUSED(enableState); + Q_UNUSED(groupFeatures); + Q_UNUSED(sampleRate); + + PanGroupState& gs = *pGroupState; + + int read_position = gs.write_position; + + CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()); + CSAMPLE strength = m_pStrengthParameter->value(); + + gs.time++; + // gs.time += strength * ; + // gs.time += abs(lfoPeriod / 2.0f - gs.time) / lfoPeriod; + if (gs.time > lfoPeriod) { + gs.time = 0; + } + + CSAMPLE periodFraction = CSAMPLE(gs.time) / lfoPeriod; + + // size of a step (stuck on 0.25 or 0.75) + float stepSize = strength * lfoPeriod / 2.0; + + // coef of the slope + float a = 1 / 4 / (lfoPeriod - stepSize * 2); + + // merging of tests for + float inInterval = CSAMPLE(gs.time); + // float inInterval = CSAMPLE(gs.time) % (lfoPeriod / 2.0); + + // size of a segment of slope + float u = ( lfoPeriod / 2.0 - stepSize ) / 2.0; + + // bool firstHalf = (periodFraction > 0.5); + CSAMPLE position; + if (inInterval > u && inInterval < ( u + stepSize)) { + position = 0.25; + } else if( inInterval > (3.0 * u + stepSize) + && inInterval < (3.0 * u + 2.0 * stepSize) ){ + // position should be stuck on 0.25 or 0.75 + // Is the position in the first or second half of the period? + // position = periodFraction > 0.5 ? 0.25 : 0.75; + position = 0.75; + } else { + // position should be in the slope + position = periodFraction * a; + } + + + // 0.8 > strength > 0.2 + + /* + CSAMPLE clampedPeriod = roundf(strength * lfoPeriod); + CSAMPLE position = (periodFraction * clampedPeriod + (lfoPeriod - clampedPeriod) / 2) / lfoPeriod; + */ + + + // get a sinusoid + // CSAMPLE frac1 = sin(M_PI * 2.0f * periodFraction); + // CSAMPLE frac1 = sin(M_PI * 2.0f * position); + // add strength to the signal (keep an impair as power to avoid loss of sign) + // CSAMPLE frac2 = frac1 * strength; + // CSAMPLE frac2 = frac1; + // CSAMPLE frac3 = CSAMPLE_clamp(frac2); + // set the curve between 0 and 1 + CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; + // CSAMPLE frac = (frac1 + 1.0f) / 2.0f; + qDebug() << "strength" << strength << "| position :" << (roundf(position * 100.0) / 100.0) + << "| time :" << gs.time << "| period :" << lfoPeriod + // << "| clamped period :" << clampedPeriod + << "| frac :" << frac; + + // qDebug() << "frac" << frac << "| 1 :" << frac1 << "| 2: " << frac2 << "| 3 :" << frac3; + + // Pingpong the output. If the pingpong value is zero, all of the + // math below should result in a simple copy of delay buf to pOutput. + for (unsigned int i = 0; i + 1 < numSamples; i += 2) { + + pOutput[i] = + (pInput[i] + pInput[i + 1]) * frac; + + pOutput[i + 1] = + (pInput[i] + pInput[i + 1]) * (1 - frac); + + } +} diff --git a/src/effects/native/paneffect.h b/src/effects/native/paneffect.h new file mode 100644 index 00000000000..cf532618227 --- /dev/null +++ b/src/effects/native/paneffect.h @@ -0,0 +1,65 @@ +#ifndef PANEFFECT_H +#define PANEFFECT_H + +#include + +#include "util.h" +#include "util/defs.h" +#include "util/types.h" +#include "engine/effects/engineeffect.h" +#include "engine/effects/engineeffectparameter.h" +#include "effects/effectprocessor.h" +#include "sampleutil.h" + +struct PanGroupState { + PanGroupState() { + delay_buf = SampleUtil::alloc(MAX_BUFFER_LEN); + SampleUtil::applyGain(delay_buf, 0, MAX_BUFFER_LEN); + prev_delay_time = 0.0; + prev_delay_samples = 0; + write_position = 0; + + time = 0; + } + ~PanGroupState() { + SampleUtil::free(delay_buf); + } + CSAMPLE* delay_buf; + double prev_delay_time; + int prev_delay_samples; + int write_position; + unsigned int time; +}; + +class PanEffect : public GroupEffectProcessor { + public: + PanEffect(EngineEffect* pEffect, const EffectManifest& manifest); + virtual ~PanEffect(); + + static QString getId(); + static EffectManifest getManifest(); + + // See effectprocessor.h + void processGroup(const QString& group, + PanGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples, + const unsigned int sampleRate, + const EffectProcessor::EnableState enableState, + const GroupFeatureState& groupFeatures); + + private: + // int getDelaySamples(double delay_time, const unsigned int sampleRate) const; + + QString debugString() const { + return getId(); + } + + EngineEffectParameter* m_pDepthParameter; + EngineEffectParameter* m_pStrengthParameter; + EngineEffectParameter* m_pPeriodParameter; + + DISALLOW_COPY_AND_ASSIGN(PanEffect); +}; + +#endif /* PANEFFECT_H */ From f5210f5958e104cb0087fb1d26bbad939fad9ecd Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Tue, 23 Dec 2014 18:23:29 +0100 Subject: [PATCH 002/481] steps working --- src/effects/native/paneffect.cpp | 82 ++++++++++++++------------------ 1 file changed, 35 insertions(+), 47 deletions(-) diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp index e152dab4e4d..f2ab327a7bc 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/paneffect.cpp @@ -40,9 +40,9 @@ EffectManifest PanEffect::getManifest() { strength->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); strength->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); strength->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - strength->setMinimum(0.1); - strength->setMaximum(0.9); - strength->setDefault(0.5); + strength->setMinimum(0.0); + strength->setMaximum(0.5); + strength->setDefault(0.25); EffectManifestParameter* period = manifest.addParameter(); period->setId("period"); @@ -87,68 +87,54 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, int read_position = gs.write_position; CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()); - CSAMPLE strength = m_pStrengthParameter->value(); + CSAMPLE stepFrac = m_pStrengthParameter->value(); gs.time++; - // gs.time += strength * ; - // gs.time += abs(lfoPeriod / 2.0f - gs.time) / lfoPeriod; if (gs.time > lfoPeriod) { gs.time = 0; } CSAMPLE periodFraction = CSAMPLE(gs.time) / lfoPeriod; - // size of a step (stuck on 0.25 or 0.75) - float stepSize = strength * lfoPeriod / 2.0; + // size of a step (while stuck on 0.25 or 0.75) + // todo rename as stepFrac + // float stepSize = stepFrac * lfoPeriod / 2.0; // coef of the slope - float a = 1 / 4 / (lfoPeriod - stepSize * 2); + // a = (y2 - y1) / (x2 - x1) + // 1 / ( 1 - 2 * stepfrac) + float a = 1.0 / (1.0 - stepFrac * 2.0); // merging of tests for - float inInterval = CSAMPLE(gs.time); - // float inInterval = CSAMPLE(gs.time) % (lfoPeriod / 2.0); + // float inInterval = fmod( CSAMPLE(gs.time), (lfoPeriod / 2.0) ); + float inInterval = fmod( periodFraction, 0.5 ); // size of a segment of slope - float u = ( lfoPeriod / 2.0 - stepSize ) / 2.0; + float u = ( 0.5 - stepFrac ) / 2.0; + + float quarter = floorf(periodFraction * 4.0); - // bool firstHalf = (periodFraction > 0.5); CSAMPLE position; - if (inInterval > u && inInterval < ( u + stepSize)) { - position = 0.25; - } else if( inInterval > (3.0 * u + stepSize) - && inInterval < (3.0 * u + 2.0 * stepSize) ){ + if (inInterval > u && inInterval < ( u + stepFrac)) { // position should be stuck on 0.25 or 0.75 // Is the position in the first or second half of the period? - // position = periodFraction > 0.5 ? 0.25 : 0.75; - position = 0.75; - } else { + position = quarter < 2.0 ? 0.25 : 0.75; + } else { + // qDebug() << "calcul | a : " << a << "| step : " << stepFrac; // position should be in the slope - position = periodFraction * a; + position = (periodFraction - (floorf((quarter+1.0)/2.0) * stepFrac )) * a; } - - // 0.8 > strength > 0.2 - - /* - CSAMPLE clampedPeriod = roundf(strength * lfoPeriod); - CSAMPLE position = (periodFraction * clampedPeriod + (lfoPeriod - clampedPeriod) / 2) / lfoPeriod; - */ - - - // get a sinusoid - // CSAMPLE frac1 = sin(M_PI * 2.0f * periodFraction); - // CSAMPLE frac1 = sin(M_PI * 2.0f * position); - // add strength to the signal (keep an impair as power to avoid loss of sign) - // CSAMPLE frac2 = frac1 * strength; - // CSAMPLE frac2 = frac1; - // CSAMPLE frac3 = CSAMPLE_clamp(frac2); // set the curve between 0 and 1 CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; - // CSAMPLE frac = (frac1 + 1.0f) / 2.0f; - qDebug() << "strength" << strength << "| position :" << (roundf(position * 100.0) / 100.0) - << "| time :" << gs.time << "| period :" << lfoPeriod - // << "| clamped period :" << clampedPeriod - << "| frac :" << frac; + // frac = CSAMPLE_clamp(frac); + // frac = (roundf(frac * 100.0) / 100.0); + // qDebug() << "stepFrac" << stepFrac << "| position :" << (roundf(position * 100.0) / 100.0) + // << "| time :" << gs.time << "| period :" << lfoPeriod + // << "| a :" << a + // << "| q :" << quarter + // << "| q+ :" << floorf((quarter+1.0)/2.0) + // << "| frac :" << frac; // qDebug() << "frac" << frac << "| 1 :" << frac1 << "| 2: " << frac2 << "| 3 :" << frac3; @@ -156,11 +142,13 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // math below should result in a simple copy of delay buf to pOutput. for (unsigned int i = 0; i + 1 < numSamples; i += 2) { - pOutput[i] = - (pInput[i] + pInput[i + 1]) * frac; - - pOutput[i + 1] = - (pInput[i] + pInput[i + 1]) * (1 - frac); + pOutput[i] = (pInput[i] + pInput[i + 1]) * frac; + pOutput[i + 1] = (pInput[i] + pInput[i + 1]) * (1 - frac); } } + + +float fmod(float a, float b) { + return (a/b) - ( (int)(a/b) * b); +} From a576e21ad8bedc9266512dd880445180f97ba23a Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Fri, 26 Dec 2014 01:53:29 +0100 Subject: [PATCH 003/481] forgotten files --- build/depends.py | 1 + .../background1024x600.png | Bin 0 -> 2852 bytes .../btn_beatgrid1.png | Bin 0 -> 128 bytes .../btn_beatgrid1_over.png | Bin 0 -> 136 bytes .../btn_beatgrid2.png | Bin 0 -> 128 bytes .../btn_beatgrid2_over.png | Bin 0 -> 136 bytes .../btn_beatloop1_0125.png | Bin 0 -> 132 bytes .../btn_beatloop1_0125_over.png | Bin 0 -> 132 bytes .../btn_beatloop1_0250.png | Bin 0 -> 134 bytes .../btn_beatloop1_0250_over.png | Bin 0 -> 134 bytes .../btn_beatloop1_0500.png | Bin 0 -> 138 bytes .../btn_beatloop1_0500_over.png | Bin 0 -> 138 bytes .../btn_beatloop1_1.png | Bin 0 -> 115 bytes .../btn_beatloop1_16.png | Bin 0 -> 122 bytes .../btn_beatloop1_16_over.png | Bin 0 -> 122 bytes .../btn_beatloop1_1_over.png | Bin 0 -> 115 bytes .../btn_beatloop1_2.png | Bin 0 -> 121 bytes .../btn_beatloop1_2_over.png | Bin 0 -> 121 bytes .../btn_beatloop1_4.png | Bin 0 -> 120 bytes .../btn_beatloop1_4_over.png | Bin 0 -> 120 bytes .../btn_beatloop1_8.png | Bin 0 -> 117 bytes .../btn_beatloop1_8_over.png | Bin 0 -> 117 bytes .../btn_beatloop1_double.png | Bin 0 -> 138 bytes .../btn_beatloop1_double_over.png | Bin 0 -> 138 bytes .../btn_beatloop1_halve.png | Bin 0 -> 130 bytes .../btn_beatloop1_halve_over.png | Bin 0 -> 130 bytes .../btn_beatloop2_0125.png | Bin 0 -> 132 bytes .../btn_beatloop2_0125_over.png | Bin 0 -> 132 bytes .../btn_beatloop2_0250.png | Bin 0 -> 134 bytes .../btn_beatloop2_0250_over.png | Bin 0 -> 134 bytes .../btn_beatloop2_0500.png | Bin 0 -> 138 bytes .../btn_beatloop2_0500_over.png | Bin 0 -> 138 bytes .../btn_beatloop2_1.png | Bin 0 -> 115 bytes .../btn_beatloop2_16.png | Bin 0 -> 122 bytes .../btn_beatloop2_16_over.png | Bin 0 -> 122 bytes .../btn_beatloop2_1_over.png | Bin 0 -> 115 bytes .../btn_beatloop2_2.png | Bin 0 -> 121 bytes .../btn_beatloop2_2_over.png | Bin 0 -> 121 bytes .../btn_beatloop2_4.png | Bin 0 -> 120 bytes .../btn_beatloop2_4_over.png | Bin 0 -> 120 bytes .../btn_beatloop2_8.png | Bin 0 -> 117 bytes .../btn_beatloop2_8_over.png | Bin 0 -> 117 bytes .../btn_beatloop2_double.png | Bin 0 -> 138 bytes .../btn_beatloop2_double_over.png | Bin 0 -> 138 bytes .../btn_beatloop2_halve.png | Bin 0 -> 130 bytes .../btn_beatloop2_halve_over.png | Bin 0 -> 130 bytes .../btn_clipping1.png | Bin 0 -> 89 bytes .../btn_clipping1_over.png | Bin 0 -> 85 bytes .../btn_clipping2.png | Bin 0 -> 89 bytes .../btn_clipping2_over.png | Bin 0 -> 85 bytes .../btn_clipping_master.png | Bin 0 -> 101 bytes .../btn_clipping_master_over.png | Bin 0 -> 100 bytes .../btn_clipping_microphone.png | Bin 0 -> 87 bytes .../btn_clipping_microphone_over.png | Bin 0 -> 84 bytes .../btn_clipping_sampler.png | Bin 0 -> 89 bytes .../btn_clipping_sampler_over.png | Bin 0 -> 85 bytes .../ShadeDark1024x600-Netbook/btn_cue1.png | Bin 0 -> 134 bytes .../btn_cue1_over.png | Bin 0 -> 134 bytes .../ShadeDark1024x600-Netbook/btn_cue2.png | Bin 0 -> 134 bytes .../btn_cue2_over.png | Bin 0 -> 134 bytes .../ShadeDark1024x600-Netbook/btn_eject1.png | Bin 0 -> 188 bytes .../btn_eject1_over.png | Bin 0 -> 188 bytes .../ShadeDark1024x600-Netbook/btn_eject2.png | Bin 0 -> 188 bytes .../btn_eject2_over.png | Bin 0 -> 188 bytes .../btn_eject_sampler.png | Bin 0 -> 190 bytes .../btn_eject_sampler_over.png | Bin 0 -> 190 bytes .../btn_forward1.png | Bin 0 -> 164 bytes .../btn_forward1_over.png | Bin 0 -> 164 bytes .../btn_forward2.png | Bin 0 -> 164 bytes .../btn_forward2_over.png | Bin 0 -> 164 bytes .../ShadeDark1024x600-Netbook/btn_fx1.png | Bin 0 -> 128 bytes .../btn_fx1_over.png | Bin 0 -> 128 bytes .../ShadeDark1024x600-Netbook/btn_fx2.png | Bin 0 -> 128 bytes .../btn_fx2_over.png | Bin 0 -> 128 bytes .../btn_hotcue1_1.png | Bin 0 -> 115 bytes .../btn_hotcue1_1_over.png | Bin 0 -> 115 bytes .../btn_hotcue1_2.png | Bin 0 -> 121 bytes .../btn_hotcue1_2_over.png | Bin 0 -> 121 bytes .../btn_hotcue1_3.png | Bin 0 -> 118 bytes .../btn_hotcue1_3_over.png | Bin 0 -> 118 bytes .../btn_hotcue1_4.png | Bin 0 -> 120 bytes .../btn_hotcue1_4_over.png | Bin 0 -> 120 bytes .../btn_hotcue2_1.png | Bin 0 -> 115 bytes .../btn_hotcue2_1_over.png | Bin 0 -> 115 bytes .../btn_hotcue2_2.png | Bin 0 -> 121 bytes .../btn_hotcue2_2_over.png | Bin 0 -> 121 bytes .../btn_hotcue2_3.png | Bin 0 -> 118 bytes .../btn_hotcue2_3_over.png | Bin 0 -> 118 bytes .../btn_hotcue2_4.png | Bin 0 -> 120 bytes .../btn_hotcue2_4_over.png | Bin 0 -> 120 bytes .../btn_keylock1.png | Bin 0 -> 146 bytes .../btn_keylock1_over.png | Bin 0 -> 146 bytes .../btn_keylock2.png | Bin 0 -> 146 bytes .../btn_keylock2_over.png | Bin 0 -> 146 bytes .../btn_keylock_sampler.png | Bin 0 -> 156 bytes .../btn_keylock_sampler_over.png | Bin 0 -> 156 bytes .../ShadeDark1024x600-Netbook/btn_kill.png | Bin 0 -> 102 bytes .../btn_kill_over.png | Bin 0 -> 102 bytes .../btn_loop_in1.png | Bin 0 -> 116 bytes .../btn_loop_in1_over.png | Bin 0 -> 116 bytes .../btn_loop_in2.png | Bin 0 -> 116 bytes .../btn_loop_in2_over.png | Bin 0 -> 116 bytes .../btn_loop_out1.png | Bin 0 -> 115 bytes .../btn_loop_out1_over.png | Bin 0 -> 115 bytes .../btn_loop_out2.png | Bin 0 -> 115 bytes .../btn_loop_out2_over.png | Bin 0 -> 115 bytes .../btn_microphone_talkover.png | Bin 0 -> 136 bytes .../btn_microphone_talkover_over.png | Bin 0 -> 136 bytes .../btn_nudge_down1.png | Bin 0 -> 165 bytes .../btn_nudge_down1_over.png | Bin 0 -> 165 bytes .../btn_nudge_down2.png | Bin 0 -> 165 bytes .../btn_nudge_down2_over.png | Bin 0 -> 165 bytes .../btn_nudge_up1.png | Bin 0 -> 167 bytes .../btn_nudge_up1_over.png | Bin 0 -> 167 bytes .../btn_nudge_up2.png | Bin 0 -> 167 bytes .../btn_nudge_up2_over.png | Bin 0 -> 167 bytes .../btn_orientation_microphone_left_over.png | Bin 0 -> 144 bytes .../btn_orientation_microphone_master.png | Bin 0 -> 184 bytes .../btn_orientation_microphone_right_over.png | Bin 0 -> 185 bytes .../btn_orientation_sampler_left_over.png | Bin 0 -> 144 bytes .../btn_orientation_sampler_master.png | Bin 0 -> 192 bytes .../btn_orientation_sampler_right_over.png | Bin 0 -> 185 bytes .../ShadeDark1024x600-Netbook/btn_pfl1.png | Bin 0 -> 235 bytes .../btn_pfl1_over.png | Bin 0 -> 235 bytes .../ShadeDark1024x600-Netbook/btn_pfl2.png | Bin 0 -> 235 bytes .../btn_pfl2_over.png | Bin 0 -> 235 bytes .../btn_pfl_sampler.png | Bin 0 -> 235 bytes .../btn_pfl_sampler_over.png | Bin 0 -> 235 bytes .../btn_pitch_down1.png | Bin 0 -> 126 bytes .../btn_pitch_down1_over.png | Bin 0 -> 126 bytes .../btn_pitch_down2.png | Bin 0 -> 126 bytes .../btn_pitch_down2_over.png | Bin 0 -> 126 bytes .../btn_pitch_up1.png | Bin 0 -> 135 bytes .../btn_pitch_up1_over.png | Bin 0 -> 135 bytes .../btn_pitch_up2.png | Bin 0 -> 135 bytes .../btn_pitch_up2_over.png | Bin 0 -> 135 bytes .../ShadeDark1024x600-Netbook/btn_play1.png | Bin 0 -> 183 bytes .../btn_play1_over.png | Bin 0 -> 183 bytes .../ShadeDark1024x600-Netbook/btn_play2.png | Bin 0 -> 183 bytes .../btn_play2_over.png | Bin 0 -> 183 bytes .../btn_play_sampler.png | Bin 0 -> 157 bytes .../btn_play_sampler_over.png | Bin 0 -> 157 bytes .../btn_quantize1.png | Bin 0 -> 174 bytes .../btn_quantize1_over.png | Bin 0 -> 174 bytes .../btn_quantize2.png | Bin 0 -> 174 bytes .../btn_quantize2_over.png | Bin 0 -> 174 bytes .../ShadeDark1024x600-Netbook/btn_reloop1.png | Bin 0 -> 129 bytes .../btn_reloop1_over.png | Bin 0 -> 129 bytes .../ShadeDark1024x600-Netbook/btn_reloop2.png | Bin 0 -> 129 bytes .../btn_reloop2_over.png | Bin 0 -> 129 bytes .../ShadeDark1024x600-Netbook/btn_repeat1.png | Bin 0 -> 214 bytes .../btn_repeat1_over.png | Bin 0 -> 214 bytes .../ShadeDark1024x600-Netbook/btn_repeat2.png | Bin 0 -> 214 bytes .../btn_repeat2_over.png | Bin 0 -> 214 bytes .../btn_repeat_sampler.png | Bin 0 -> 212 bytes .../btn_repeat_sampler_over.png | Bin 0 -> 212 bytes .../btn_reverse1.png | Bin 0 -> 138 bytes .../btn_reverse1_over.png | Bin 0 -> 138 bytes .../btn_reverse2.png | Bin 0 -> 138 bytes .../btn_reverse2_over.png | Bin 0 -> 138 bytes .../ShadeDark1024x600-Netbook/btn_rewind1.png | Bin 0 -> 182 bytes .../btn_rewind1_over.png | Bin 0 -> 182 bytes .../ShadeDark1024x600-Netbook/btn_rewind2.png | Bin 0 -> 182 bytes .../btn_rewind2_over.png | Bin 0 -> 182 bytes .../ShadeDark1024x600-Netbook/btn_spinny1.png | Bin 0 -> 218 bytes .../btn_spinny1_over.png | Bin 0 -> 288 bytes .../ShadeDark1024x600-Netbook/btn_spinny2.png | Bin 0 -> 218 bytes .../btn_spinny2_over.png | Bin 0 -> 288 bytes .../ShadeDark1024x600-Netbook/btn_sync1.png | Bin 0 -> 133 bytes .../btn_sync1_over.png | Bin 0 -> 133 bytes .../ShadeDark1024x600-Netbook/btn_sync2.png | Bin 0 -> 133 bytes .../btn_sync2_over.png | Bin 0 -> 133 bytes .../ShadeDark1024x600-Netbook/btn_tap1.png | Bin 0 -> 109 bytes .../btn_tap1_over.png | Bin 0 -> 187 bytes .../ShadeDark1024x600-Netbook/btn_tap2.png | Bin 0 -> 109 bytes .../btn_tap2_over.png | Bin 0 -> 187 bytes .../btn_tap_sampler.png | Bin 0 -> 108 bytes .../btn_tap_sampler_over.png | Bin 0 -> 130 bytes .../btn_vinylcontrol_cueing_hot1.png | Bin 0 -> 198 bytes .../btn_vinylcontrol_cueing_hot2.png | Bin 0 -> 198 bytes .../btn_vinylcontrol_cueing_off1.png | Bin 0 -> 204 bytes .../btn_vinylcontrol_cueing_off2.png | Bin 0 -> 204 bytes .../btn_vinylcontrol_cueing_one1.png | Bin 0 -> 204 bytes .../btn_vinylcontrol_cueing_one2.png | Bin 0 -> 204 bytes ...btn_vinylcontrol_indicator_horizontal1.png | Bin 0 -> 95 bytes ...btn_vinylcontrol_indicator_horizontal2.png | Bin 0 -> 95 bytes ...btn_vinylcontrol_indicator_horizontal3.png | Bin 0 -> 95 bytes .../btn_vinylcontrol_indicator_vertical1.png | Bin 0 -> 94 bytes .../btn_vinylcontrol_indicator_vertical2.png | Bin 0 -> 94 bytes .../btn_vinylcontrol_indicator_vertical3.png | Bin 0 -> 94 bytes .../btn_vinylcontrol_mode_abs1.png | Bin 0 -> 217 bytes .../btn_vinylcontrol_mode_abs2.png | Bin 0 -> 217 bytes .../btn_vinylcontrol_mode_const1.png | Bin 0 -> 214 bytes .../btn_vinylcontrol_mode_const2.png | Bin 0 -> 214 bytes .../btn_vinylcontrol_mode_rel1.png | Bin 0 -> 219 bytes .../btn_vinylcontrol_mode_rel2.png | Bin 0 -> 219 bytes .../btn_volume_display1.png | Bin 0 -> 94 bytes .../btn_volume_display1_over.png | Bin 0 -> 107 bytes .../btn_volume_display2.png | Bin 0 -> 94 bytes .../btn_volume_display2_over.png | Bin 0 -> 107 bytes .../btn_volume_display_master1.png | Bin 0 -> 94 bytes .../btn_volume_display_master1_over.png | Bin 0 -> 107 bytes .../btn_volume_display_master2.png | Bin 0 -> 94 bytes .../btn_volume_display_master2_over.png | Bin 0 -> 107 bytes .../btn_volume_display_microphone.png | Bin 0 -> 93 bytes .../btn_volume_display_microphone_over.png | Bin 0 -> 101 bytes .../btn_volume_display_sampler.png | Bin 0 -> 109 bytes .../btn_volume_display_sampler_over.png | Bin 0 -> 107 bytes .../knob_crossfader.png | Bin 0 -> 214 bytes .../ShadeDark1024x600-Netbook/knob_pitch1.png | Bin 0 -> 183 bytes .../ShadeDark1024x600-Netbook/knob_pitch2.png | Bin 0 -> 183 bytes .../knob_pitch_sampler.png | Bin 0 -> 183 bytes .../knob_volume1.png | Bin 0 -> 183 bytes .../knob_volume2.png | Bin 0 -> 183 bytes .../knobs/knob_rotary_s0.png | Bin 0 -> 393 bytes .../knobs/knob_rotary_s1.png | Bin 0 -> 418 bytes .../knobs/knob_rotary_s10.png | Bin 0 -> 457 bytes .../knobs/knob_rotary_s11.png | Bin 0 -> 448 bytes .../knobs/knob_rotary_s12.png | Bin 0 -> 455 bytes .../knobs/knob_rotary_s13.png | Bin 0 -> 445 bytes .../knobs/knob_rotary_s14.png | Bin 0 -> 453 bytes .../knobs/knob_rotary_s15.png | Bin 0 -> 435 bytes .../knobs/knob_rotary_s16.png | Bin 0 -> 420 bytes .../knobs/knob_rotary_s17.png | Bin 0 -> 423 bytes .../knobs/knob_rotary_s18.png | Bin 0 -> 416 bytes .../knobs/knob_rotary_s19.png | Bin 0 -> 410 bytes .../knobs/knob_rotary_s2.png | Bin 0 -> 454 bytes .../knobs/knob_rotary_s20.png | Bin 0 -> 410 bytes .../knobs/knob_rotary_s21.png | Bin 0 -> 396 bytes .../knobs/knob_rotary_s22.png | Bin 0 -> 403 bytes .../knobs/knob_rotary_s23.png | Bin 0 -> 390 bytes .../knobs/knob_rotary_s24.png | Bin 0 -> 399 bytes .../knobs/knob_rotary_s25.png | Bin 0 -> 390 bytes .../knobs/knob_rotary_s26.png | Bin 0 -> 395 bytes .../knobs/knob_rotary_s27.png | Bin 0 -> 382 bytes .../knobs/knob_rotary_s28.png | Bin 0 -> 377 bytes .../knobs/knob_rotary_s29.png | Bin 0 -> 367 bytes .../knobs/knob_rotary_s3.png | Bin 0 -> 457 bytes .../knobs/knob_rotary_s30.png | Bin 0 -> 362 bytes .../knobs/knob_rotary_s31.png | Bin 0 -> 328 bytes .../knobs/knob_rotary_s32.png | Bin 0 -> 344 bytes .../knobs/knob_rotary_s33.png | Bin 0 -> 369 bytes .../knobs/knob_rotary_s34.png | Bin 0 -> 378 bytes .../knobs/knob_rotary_s35.png | Bin 0 -> 384 bytes .../knobs/knob_rotary_s36.png | Bin 0 -> 401 bytes .../knobs/knob_rotary_s37.png | Bin 0 -> 409 bytes .../knobs/knob_rotary_s38.png | Bin 0 -> 403 bytes .../knobs/knob_rotary_s39.png | Bin 0 -> 409 bytes .../knobs/knob_rotary_s4.png | Bin 0 -> 451 bytes .../knobs/knob_rotary_s40.png | Bin 0 -> 433 bytes .../knobs/knob_rotary_s41.png | Bin 0 -> 425 bytes .../knobs/knob_rotary_s42.png | Bin 0 -> 432 bytes .../knobs/knob_rotary_s43.png | Bin 0 -> 428 bytes .../knobs/knob_rotary_s44.png | Bin 0 -> 442 bytes .../knobs/knob_rotary_s45.png | Bin 0 -> 422 bytes .../knobs/knob_rotary_s46.png | Bin 0 -> 416 bytes .../knobs/knob_rotary_s47.png | Bin 0 -> 428 bytes .../knobs/knob_rotary_s48.png | Bin 0 -> 434 bytes .../knobs/knob_rotary_s49.png | Bin 0 -> 435 bytes .../knobs/knob_rotary_s5.png | Bin 0 -> 463 bytes .../knobs/knob_rotary_s50.png | Bin 0 -> 435 bytes .../knobs/knob_rotary_s51.png | Bin 0 -> 439 bytes .../knobs/knob_rotary_s52.png | Bin 0 -> 446 bytes .../knobs/knob_rotary_s53.png | Bin 0 -> 476 bytes .../knobs/knob_rotary_s54.png | Bin 0 -> 469 bytes .../knobs/knob_rotary_s55.png | Bin 0 -> 463 bytes .../knobs/knob_rotary_s56.png | Bin 0 -> 478 bytes .../knobs/knob_rotary_s57.png | Bin 0 -> 481 bytes .../knobs/knob_rotary_s58.png | Bin 0 -> 471 bytes .../knobs/knob_rotary_s59.png | Bin 0 -> 468 bytes .../knobs/knob_rotary_s6.png | Bin 0 -> 460 bytes .../knobs/knob_rotary_s60.png | Bin 0 -> 473 bytes .../knobs/knob_rotary_s61.png | Bin 0 -> 458 bytes .../knobs/knob_rotary_s62.png | Bin 0 -> 467 bytes .../knobs/knob_rotary_s63.png | Bin 0 -> 467 bytes .../knobs/knob_rotary_s7.png | Bin 0 -> 464 bytes .../knobs/knob_rotary_s8.png | Bin 0 -> 468 bytes .../knobs/knob_rotary_s9.png | Bin 0 -> 473 bytes res/skins/ShadeDark1024x600-Netbook/skin.xml | 6057 +++++++++++++++++ .../slider_crossfader.png | Bin 0 -> 113 bytes .../slider_pitch1.png | Bin 0 -> 119 bytes .../slider_pitch2.png | Bin 0 -> 119 bytes .../slider_pitch_sampler.png | Bin 0 -> 118 bytes .../slider_volume1.png | Bin 0 -> 119 bytes .../slider_volume2.png | Bin 0 -> 119 bytes .../style/style_bg_microphone.png | Bin 0 -> 139 bytes .../style/style_bg_sampler.png | Bin 0 -> 228 bytes .../style/style_bg_waveform.png | Bin 0 -> 15109 bytes .../style/style_bg_woverview.png | Bin 0 -> 183 bytes .../style/style_branch_closed.png | Bin 0 -> 138 bytes .../style/style_branch_open.png | Bin 0 -> 158 bytes .../style/style_checkbox_checked.png | Bin 0 -> 298 bytes .../style/style_checkbox_unchecked.png | Bin 0 -> 117 bytes .../style/style_handle_checked.png | Bin 0 -> 92 bytes .../style/style_handle_unchecked.png | Bin 0 -> 93 bytes .../tab_microphone.png | Bin 0 -> 142 bytes .../tab_microphone_over.png | Bin 0 -> 142 bytes .../ShadeDark1024x600-Netbook/tab_sampler.png | Bin 0 -> 147 bytes .../tab_sampler_over.png | Bin 0 -> 147 bytes .../tab_vinylcontrol.png | Bin 0 -> 146 bytes .../tab_vinylcontrol_over.png | Bin 0 -> 146 bytes .../vinyl_spinny1_background.png | Bin 0 -> 1568 bytes .../vinyl_spinny1_foreground.png | Bin 0 -> 299 bytes .../vinyl_spinny1_foreground_ghost.png | Bin 0 -> 274 bytes .../vinyl_spinny2_background.png | Bin 0 -> 1568 bytes .../vinyl_spinny2_foreground.png | Bin 0 -> 299 bytes .../vinyl_spinny2_foreground_ghost.png | Bin 0 -> 274 bytes 307 files changed, 6058 insertions(+) create mode 100644 res/skins/ShadeDark1024x600-Netbook/background1024x600.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatgrid1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatgrid1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatgrid2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatgrid2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0250.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0250_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0500.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0500_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_16.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_16_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_4.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_4_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_8.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_8_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_double.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_double_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_halve.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_halve_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0125.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0125_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0250.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0250_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0500.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0500_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_16.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_16_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_4.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_4_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_8.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_8_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_double.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_double_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_halve.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_halve_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_master.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_master_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_microphone.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_microphone_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_sampler_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_cue1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_cue1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_cue2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_cue2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject_sampler_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_forward1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_forward1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_forward2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_forward2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_fx1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_fx1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_fx2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_fx2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_4.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_4_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_4.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_4_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock_sampler_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_kill.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_kill_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_in1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_in1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_in2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_in2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_out1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_out1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_out2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_out2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_up1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_up1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_up2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_up2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_left_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_master.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_right_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_left_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_master.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_right_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl_sampler_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_down1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_down1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_up1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_up1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_up2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_up2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play_sampler_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_quantize1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_quantize1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_quantize2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_quantize2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reloop1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reloop1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reloop2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reloop2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat_sampler_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reverse1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reverse1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reverse2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reverse2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_rewind1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_rewind1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_rewind2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_rewind2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_spinny1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_spinny1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_spinny2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_spinny2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_sync1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_sync1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_sync2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_sync2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap_sampler_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal3.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical3.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_abs1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_abs2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_rel1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_rel2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master1_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master2_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_microphone.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_microphone_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_sampler_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_crossfader.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_pitch1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_pitch2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_pitch_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_volume1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_volume2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s10.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s11.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s12.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s16.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s19.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s21.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s22.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s28.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s35.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s37.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s38.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s39.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s40.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s47.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s48.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s50.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s53.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s58.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s6.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s8.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s9.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/skin.xml create mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_crossfader.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_pitch1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_pitch2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_pitch_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_volume1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_volume2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_woverview.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_open.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_unchecked.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_microphone.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_microphone_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_sampler_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol_over.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_background.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground_ghost.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_background.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground_ghost.png diff --git a/build/depends.py b/build/depends.py index ad71733e5a6..6af4da81c64 100644 --- a/build/depends.py +++ b/build/depends.py @@ -562,6 +562,7 @@ def sources(self, build): "effects/native/moogladder4filtereffect.cpp", "effects/native/reverbeffect.cpp", "effects/native/echoeffect.cpp", + "effects/native/paneffect.cpp", "effects/native/reverb/Reverb.cc", "engine/effects/engineeffectsmanager.cpp", diff --git a/res/skins/ShadeDark1024x600-Netbook/background1024x600.png b/res/skins/ShadeDark1024x600-Netbook/background1024x600.png new file mode 100644 index 0000000000000000000000000000000000000000..78759a7c213e83c958129eb63438b9fad0a3c76c GIT binary patch literal 2852 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 X{w8DR{%CD53xW literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatgrid2.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatgrid2.png new file mode 100644 index 0000000000000000000000000000000000000000..2264a2c82179792aa417758945eaebc26ebb4dd3 GIT binary patch literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q876lZ`>h-+9}c0@!3khG4N%>)!= zDhcunW@bLJ{k$WPr{d}27*cWT>iLC&3=BMn98Udne!S{Y)M0rozJ-kX7w>&N^1EC} X{w8DR{%CD53xW literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125.png new file mode 100644 index 0000000000000000000000000000000000000000..388e1349c8ea5a353aa0a187477a9442d261da02 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSkK5lEG|3c z^;$`wkb$R*V@SoVwdV}^7#LWb15f=uuj8TP(`&!4WW}AIulRf27&@cQZZb?%-1z6m cO~VSYOJ7(P39}_!0P1D%boFyt=akR{025UwT>t<8 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125_over.png new file mode 100644 index 0000000000000000000000000000000000000000..5bedf7c49c38ad0b8865a69e054b94143342bb59 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSShW|{`jwZ zzUkpWAp=hr$B>F!YtI?-F)*+=2cG(SUdKbnr`LX8$%;EaU-9?4F?2?q-DH@kxbe@A cn}!u)m%gwp5@t)d0MyIi>FVdQ&MBb@0Bva~fdBvi literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0250.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0250.png new file mode 100644 index 0000000000000000000000000000000000000000..b0731fd38d2497742045d1e7b316466078e7cdc5 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSkK5lEG|3c z^;$`wkddd0V@SoVwdV}^7#LVs9Q*$7m+WoR*wC3)#QN&;CHYfQj4Vd0F!YtI?-F)*;OIQISDFWK9sv7s}qi1pRwOY*0r7+H)~$Lpzo;c{c1 e_y3I8jkL$lSayjCNmu}NGkCiCxvXF!YcCk`F*vZe1RlHh|4hV5vt>#3KaE-i?q&XUmR4?X*}FG2@!AAE iW~E&*_jdjlI?H%Rj%8i5+N>;~2@IaDelF{r5}E*P8Z+&Yv-GzwDhKVUCxRwA?=j RDuDVJJYD@<);T3K0RZLPBuxMS literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_16_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_16_over.png new file mode 100644 index 0000000000000000000000000000000000000000..af0b15f0088748ade7c21e64a1170f542098115b GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAD zr;B4q#jUgF4S{+&oG!lk*UmRbY3WV#x||hz!~_K$n!io@IIrHjSLDMo(_d^%X}Nz6 RQ~>occ)I$ztaD0e0sucgC5Zq4 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..0b52819af99b98b5b7fe6dd490ce9615bef95148 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jUgFHwrQ^a2#@Y{Lk6A;bqPbBlp8QY@?JEPM7QKT*<^Ug{h7yJarvV1B0il KpUXO@geCwf`XIId literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2.png new file mode 100644 index 0000000000000000000000000000000000000000..e26ab62a43033219b2d0ab813771f1d8846dc929 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dC4 z)5S5Q;?~)IL!e#`rq%!LtvHla3fD^e3!3=p%=6H9C|fW~`RcR3zFtf$x=h8>CWOcX PwJ~_Q`njxgN@xNA^F1Bf literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..d7e809610aabeead06174d159cfeb1b4909e43b7 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAr zr;B4q#jUgbhCsa>OsoIfTX86<6t0!_7c}wHndhPJP_|%}^3`X5eZ81ibeW2$O$d<( PYGd$p^>bP0l+XkKOJg4v literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_4.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_4.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff8e699b854fc558ea231bfa7f8d2978f7a466d GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB< z)5S5Q;?~*ohI|YREG&-C|M!bUXEK@-ZlIusELo`agf$PCg-nZ8?^#;Tt~rWqc4+R0`rw^#0K# kxRdMc->;Q-?3cw}IKwsd{^GiKKob}|UHx3vIVCg!07Hr^kpKVy literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_double_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_double_over.png new file mode 100644 index 0000000000000000000000000000000000000000..5e7f0d27b4c6a91265798a2ccdfdd565035827b4 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Lk!3-psHvjYiQv3lvA+A80on2U?e3seDYxc)~ z1^nMG2NW{%ba4!+xb^m;As>SR2aDtRumAJ6?c@_O*p_3-8ouF^U&aSPMWrC_MDHI> kf;+k1{{32c$9`Gtg)>}J?=P-<2Q-1f)78&qol`;+00sdq*8l(j literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_halve.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_halve.png new file mode 100644 index 0000000000000000000000000000000000000000..3867c2ac1f42471b1f5e96fcf753270f05707d52 GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Lk!3-psHvjYiQbGYfA+A80on2VZ$lfa;HY7SD zEH3+Tc9+Jt<8 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0125_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0125_over.png new file mode 100644 index 0000000000000000000000000000000000000000..5bedf7c49c38ad0b8865a69e054b94143342bb59 GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSShW|{`jwZ zzUkpWAp=hr$B>F!YtI?-F)*+=2cG(SUdKbnr`LX8$%;EaU-9?4F?2?q-DH@kxbe@A cn}!u)m%gwp5@t)d0MyIi>FVdQ&MBb@0Bva~fdBvi literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0250.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0250.png new file mode 100644 index 0000000000000000000000000000000000000000..b0731fd38d2497742045d1e7b316466078e7cdc5 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSkK5lEG|3c z^;$`wkddd0V@SoVwdV}^7#LVs9Q*$7m+WoR*wC3)#QN&;CHYfQj4Vd0F!YtI?-F)*;OIQISDFWK9sv7s}qi1pRwOY*0r7+H)~$Lpzo;c{c1 e_y3I8jkL$lSayjCNmu}NGkCiCxvXF!YcCk`F*vZe1RlHh|4hV5vt>#3KaE-i?q&XUmR4?X*}FG2@!AAE iW~E&*_jdjlI?H%Rj%8i5+N>;~2@IaDelF{r5}E*P8Z+&Yv-GzwDhKVUCxRwA?=j RDuDVJJYD@<);T3K0RZLPBuxMS literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_16_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_16_over.png new file mode 100644 index 0000000000000000000000000000000000000000..af0b15f0088748ade7c21e64a1170f542098115b GIT binary patch literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAD zr;B4q#jUgF4S{+&oG!lk*UmRbY3WV#x||hz!~_K$n!io@IIrHjSLDMo(_d^%X}Nz6 RQ~>occ)I$ztaD0e0sucgC5Zq4 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..0b52819af99b98b5b7fe6dd490ce9615bef95148 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jUgFHwrQ^a2#@Y{Lk6A;bqPbBlp8QY@?JEPM7QKT*<^Ug{h7yJarvV1B0il KpUXO@geCwf`XIId literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2.png new file mode 100644 index 0000000000000000000000000000000000000000..e26ab62a43033219b2d0ab813771f1d8846dc929 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dC4 z)5S5Q;?~)IL!e#`rq%!LtvHla3fD^e3!3=p%=6H9C|fW~`RcR3zFtf$x=h8>CWOcX PwJ~_Q`njxgN@xNA^F1Bf literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..d7e809610aabeead06174d159cfeb1b4909e43b7 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAr zr;B4q#jUgbhCsa>OsoIfTX86<6t0!_7c}wHndhPJP_|%}^3`X5eZ81ibeW2$O$d<( PYGd$p^>bP0l+XkKOJg4v literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_4.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_4.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff8e699b854fc558ea231bfa7f8d2978f7a466d GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB< z)5S5Q;?~*ohI|YREG&-C|M!bUXEK@-ZlIusELo`agf$PCg-nZ8?^#;Tt~rWqc4+R0`rw^#0K# kxRdMc->;Q-?3cw}IKwsd{^GiKKob}|UHx3vIVCg!07Hr^kpKVy literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_double_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_double_over.png new file mode 100644 index 0000000000000000000000000000000000000000..5e7f0d27b4c6a91265798a2ccdfdd565035827b4 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Lk!3-psHvjYiQv3lvA+A80on2U?e3seDYxc)~ z1^nMG2NW{%ba4!+xb^m;As>SR2aDtRumAJ6?c@_O*p_3-8ouF^U&aSPMWrC_MDHI> kf;+k1{{32c$9`Gtg)>}J?=P-<2Q-1f)78&qol`;+00sdq*8l(j literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_halve.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_halve.png new file mode 100644 index 0000000000000000000000000000000000000000..3867c2ac1f42471b1f5e96fcf753270f05707d52 GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Lk!3-psHvjYiQbGYfA+A80on2VZ$lfa;HY7SD zEH3+Tc9+Ju6{1-oD!Mvd7(8A5T-G@yGywo2))r&{ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_clipping2.png b/res/skins/ShadeDark1024x600-Netbook/btn_clipping2.png new file mode 100644 index 0000000000000000000000000000000000000000..b56ed2efa593a310a82f382ea9c0dae92179fc39 GIT binary patch literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^tUxTp!3HEJ?-4l$q!c_|978H@y*;7G$iTqEWRSJM n;LG|Gf0-8u7&AYuIqf7jm#6aos_XhpK(!2>u6{1-oD!Mvd7(8A5T-G@yGywo2))r&{ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_clipping_master.png b/res/skins/ShadeDark1024x600-Netbook/btn_clipping_master.png new file mode 100644 index 0000000000000000000000000000000000000000..fea64ce9411c5ae2b59d0ce9dae4fa5f5240860e GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^+(0bE!3-pKt-j(8q}T#{LR=-~RqUNS*QqVp0Thw) xba4!+xOMlKBPWp8eBkE(EeSn8En+76>vI$^2;c5nJQpa=;OXk;vd$@?2>^*Q8_NIy literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_clipping_master_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_clipping_master_over.png new file mode 100644 index 0000000000000000000000000000000000000000..f03c9615a756088b95b7fda0af62f7f86801cb73 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^+(0bE!3-pKt-j(8q}T#{LR{^gJpVH=X#G(+0u+(- wba4!+xOMl4Auo{Eyy4+}lY_^qN}l91`kY|c`trU)3s9KB)78&qol`;+0HE9*8vpF84@|q2PO@Ai3 gLA=;`Lq!MkQ=a6xrx&LF14=M>y85}Sb4q9e00}u4F#rGn literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_clipping_sampler.png b/res/skins/ShadeDark1024x600-Netbook/btn_clipping_sampler.png new file mode 100644 index 0000000000000000000000000000000000000000..b56ed2efa593a310a82f382ea9c0dae92179fc39 GIT binary patch literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^tUxTp!3HEJ?-4l$q!c_|978H@y*;7G$iTqEWRSJM n;LG|Gf0-8u7&AYuIqf7jm#6aos_XhpK(!2>u6{1-oD!Mvd7(8A5T-G@yGywo2))r&{ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_cue1.png b/res/skins/ShadeDark1024x600-Netbook/btn_cue1.png new file mode 100644 index 0000000000000000000000000000000000000000..068cba05607c65ea9cd3be8b50c1e5625869a2d8 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z;pyTSQgQ3;L_;nH0}kff|NdY6>Lp{vp&4sC!Nnp*hF_=OY;tACf~?g(tc%YYYyLNX f*!NFu*;%$_OIRJB=2Ywln!(`d>gTe~DWM4fqb?|t literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_cue1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_cue1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..845bebb43e864ab65d558f44c9ecd420a0b11ee0 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zg{O;SNX4zU6Aif-3^OBQk(%kA+A80on1IAF8fDwx*(8a z;pyTSQgQ3;L_;nH0}kff|NdY6>Lp{vp&4sC!Nnp*hF_=OY;tACf~?g(tc%YYYyLNX f*!NFu*;%$_OIRJB=2Ywln!(`d>gTe~DWM4fqb?|t literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_cue2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_cue2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..845bebb43e864ab65d558f44c9ecd420a0b11ee0 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zg{O;SNX4zU6Aif-3^4PO2y2|a;)chH(Dgu32TvErkcwMd&s*~`C~&wqx(eR> zEgzAr@gZDtdCkorfB$?F$7Rf} U_uUpR2Aaj->FVdQ&MBb@02f$5F#rGn literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_eject1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_eject1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..426c05237f2bf4dd19c2e41f5158ae0ccd781907 GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESi0l9V|05zILLVlDzD*AO zoD%vaCGNRC70LSOmnvRA-(n8My5^2n);rfkl57E;;h#t_3?DdpP4PO2y2|a;)chH(Dgu32TvErkcwMd&s*~`C~&wqx(eR> zEgzAr@gZDtdCkorfB$?F$7Rf} U_uUpR2Aaj->FVdQ&MBb@02f$5F#rGn literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_eject2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_eject2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..426c05237f2bf4dd19c2e41f5158ae0ccd781907 GIT binary patch literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESi0l9V|05zILLVlDzD*AO zoD%vaCGNRC70LSOmnvRA-(n8My5^2n);rfkl57E;;h#t_3?DdpP}uk1U;Z_Q zA{rZw_7!b9tF}nwSGSfR>jPoywA|xupF04qm zW1ZoQLuM+De7zB7+0v%g^$KQzPT{9);W0(D8 zCCz&~zNtwq(BRVWVij4+&^3wCmDO%r>(9^oER)$Dy2mH);FK)je8CcW>k!Z|22WQ% Jmvv4FO#m5{Gi(3= literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_forward2.png b/res/skins/ShadeDark1024x600-Netbook/btn_forward2.png new file mode 100644 index 0000000000000000000000000000000000000000..f0ca1f2604cb5b975e523a0ff7263edf4ea53147 GIT binary patch literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaasfUeu0WcdU6_YgQc2ZNL)+ZO z(a+g4%qt)^AR;v^E}P|UU^Y;Nqo<2wNX4zK=M9Az1bAEm9b10K--zg4GbQI^m;GcV z&3ik(sYxx+;L`A76F04qm zW1ZoQLuM+De7zB7+0v%g^$KQzPT{9);W0(D8 zCCz&~zNtwq(BRVWVij4+&^3wCmDO%r>(9^oER)$Dy2mH);FK)je8CcW>k!Z|22WQ% Jmvv4FO#m5{Gi(3= literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_fx1.png b/res/skins/ShadeDark1024x600-Netbook/btn_fx1.png new file mode 100644 index 0000000000000000000000000000000000000000..877eef4f43d8e33fd0215daa390049cbd2b39f33 GIT binary patch literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)Qk(%kA+A80jZHW#E}L)vToxe5 z$kW9!q~g}y3Fbh(9L!h$+uv$>k;LfEui`XGb%P_bevXODm$h$tOy8c6y`@*h?>0Aa YzBTiCi+wwn0Ch8Xy85}Sb4q9e0Fn|V6951J literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_fx1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_fx1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..0aed02cd350b512056c1367a48779175954ab675 GIT binary patch literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)Qk(%kA+A80jZN78_^&*p|5`wf zk*AAeNX4zY6U>2nIhe2hx4+f&B8kzRU&U#X>IO$<{TvgQFKgfQn7%zBdrPm1-)(N< Yd~4?O7W;NC0qSP(boFyt=akR{0P@WyPXGV_ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_fx2.png b/res/skins/ShadeDark1024x600-Netbook/btn_fx2.png new file mode 100644 index 0000000000000000000000000000000000000000..877eef4f43d8e33fd0215daa390049cbd2b39f33 GIT binary patch literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)Qk(%kA+A80jZHW#E}L)vToxe5 z$kW9!q~g}y3Fbh(9L!h$+uv$>k;LfEui`XGb%P_bevXODm$h$tOy8c6y`@*h?>0Aa YzBTiCi+wwn0Ch8Xy85}Sb4q9e0Fn|V6951J literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_fx2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_fx2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..0aed02cd350b512056c1367a48779175954ab675 GIT binary patch literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)Qk(%kA+A80jZN78_^&*p|5`wf zk*AAeNX4zY6U>2nIhe2hx4+f&B8kzRU&U#X>IO$<{TvgQFKgfQn7%zBdrPm1-)(N< Yd~4?O7W;NC0qSP(boFyt=akR{0P@WyPXGV_ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1.png new file mode 100644 index 0000000000000000000000000000000000000000..4fc6d791bc9f6c5381787bc39ede8504246c12ab GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB} z)5S5Q;?~*o8wD8{I1V{H{^xAm@G|Fzk^A8twoyt7r^|JAu4Lkw!c@l;p1KaGfx*+& K&t;ucLK6VlJRf=h literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..0b52819af99b98b5b7fe6dd490ce9615bef95148 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jUgFHwrQ^a2#@Y{Lk6A;bqPbBlp8QY@?JEPM7QKT*<^Ug{h7yJarvV1B0il KpUXO@geCwf`XIId literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2.png new file mode 100644 index 0000000000000000000000000000000000000000..e26ab62a43033219b2d0ab813771f1d8846dc929 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dC4 z)5S5Q;?~)IL!e#`rq%!LtvHla3fD^e3!3=p%=6H9C|fW~`RcR3zFtf$x=h8>CWOcX PwJ~_Q`njxgN@xNA^F1Bf literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..d7e809610aabeead06174d159cfeb1b4909e43b7 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAr zr;B4q#jUgbhCsa>OsoIfTX86<6t0!_7c}wHndhPJP_|%}^3`X5eZ81ibeW2$O$d<( PYGd$p^>bP0l+XkKOJg4v literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3.png new file mode 100644 index 0000000000000000000000000000000000000000..4747f951b9041e9d0d97fc66423b5d3be192f5a9 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB% z)5S5Q;?~*zjhqY&9EaEZm)G54b|KKXEg-`%*JfhW0v1WH`+N3x^01y@DvkYGp$62$ N;OXk;vd$@?2>=tgAPoQj literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3_over.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d9dac6c7bc01a1cf558dc583dcd2a6bc3ec6a4 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oA5 zr;B4q#jUgb8#x&mI1aD*FR#19>_VV%TR?_kuFb@#1uT+Y_xJ4Yg N!PC{xWt~$(698x%AxQuL literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_4.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_4.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff8e699b854fc558ea231bfa7f8d2978f7a466d GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB< z)5S5Q;?~*ohI|YREG&-C|M!bUXCWOcX PwJ~_Q`njxgN@xNA^F1Bf literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..d7e809610aabeead06174d159cfeb1b4909e43b7 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAr zr;B4q#jUgbhCsa>OsoIfTX86<6t0!_7c}wHndhPJP_|%}^3`X5eZ81ibeW2$O$d<( PYGd$p^>bP0l+XkKOJg4v literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3.png new file mode 100644 index 0000000000000000000000000000000000000000..4747f951b9041e9d0d97fc66423b5d3be192f5a9 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB% z)5S5Q;?~*zjhqY&9EaEZm)G54b|KKXEg-`%*JfhW0v1WH`+N3x^01y@DvkYGp$62$ N;OXk;vd$@?2>=tgAPoQj literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3_over.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d9dac6c7bc01a1cf558dc583dcd2a6bc3ec6a4 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oA5 zr;B4q#jUgb8#x&mI1aD*FR#19>_VV%TR?_kuFb@#1uT+Y_xJ4Yg N!PC{xWt~$(698x%AxQuL literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_4.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_4.png new file mode 100644 index 0000000000000000000000000000000000000000..6ff8e699b854fc558ea231bfa7f8d2978f7a466d GIT binary patch literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB< z)5S5Q;?~*ohI|YREG&-C|M!bUXR;~yioPn-?N*Yk9745_%a^}ILGL>AY;{eRa-P7z%AWZFr|Jxmc_ px{_4C%nn>6dP!>M?LGhFme$Q@;@$JnLma4+!PC{xWt~$(69Af5El~gf literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_keylock1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_keylock1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..19583b137790fa10d4db80e84e9880cdb1fdc63a GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q876n}tEi0l9V|05zILO-X3eoqPg zk#hUX6mOsuOG%JlFaxvG#U?*-HXvWm)5S5Q;?~yl-ar#sTm$$2T^~6`aN(0_CnfhV rMSST>QvEVJaFOUGshzj?{Eu5&H=l`j&qoh&piTx)S3j3^P6R;~yioPn-?N*Yk9745_%a^}ILGL>AY;{eRa-P7z%AWZFr|Jxmc_ px{_4C%nn>6dP!>M?LGhFme$Q@;@$JnLma4+!PC{xWt~$(69Af5El~gf literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_keylock2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_keylock2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..19583b137790fa10d4db80e84e9880cdb1fdc63a GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q876n}tEi0l9V|05zILO-X3eoqPg zk#hUX6mOsuOG%JlFaxvG#U?*-HXvWm)5S5Q;?~yl-ar#sTm$$2T^~6`aN(0_CnfhV rMSST>QvEVJaFOUGshzj?{Eu5&H=l`j&qoh&piTx)S3j3^P6^ zZH)uUvX%t-1v4v=~m1_ch6i>IFb3-42m;c4P=kuUiu w)|9+)Wv%OmCK=|5krHO_-aY&MzC4@#buiPii_%vn0`)U^y85}Sb4q9e0JMTJ_y7O^ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_keylock_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_keylock_sampler_over.png new file mode 100644 index 0000000000000000000000000000000000000000..95e0941aa2967cac0d969376eaffdcd1c0c5f785 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q7SoB*E?*Z=?jM}R@-=akUzDWN}7 zUd&Ke2FkLQ1o;IsFta&be11Yo11MnP>Eaktack>&M=k~h4ws9kp8X5&Q;gwh;&G8L y`6$+uym4i%>xL#7=82IKX7AoT`~AK=oBee#)3S@wS0)1WGkCiCxvXzmP@XOOa6-mXfUkOd(yZAD8bgTe~DWM4f`y?6D literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_kill_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_kill_over.png new file mode 100644 index 0000000000000000000000000000000000000000..9f7ddde8f915e9d6d34baa6c1464a148546e209c GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^{2 z)5S5Q;?~g%hI|YR9ETh}{_i(CW8}EVyeubkuY}+iv6nW>UN2$%c$O)3Ldi=9pbiF4 LS3j3^P6jDzd09^8UJ1c3VlQo$y z)5S5Q;?~g%hI|YR9ETh}{_i(CW8}EVyeubkuY}+iv6nW>UN2$%c$O)3Ldi=9pbiF4 LS3j3^P6jDzd09^8UJ1c3VlQo$yYD5ykZJCR1v~kC{h-8W=oX L{an^LB{Ts52)!a7 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_loop_out1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_loop_out1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea4e8aa1042de44813a6b9c17aa5ac5b06b3b8d GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jT?!778*j@Ekg@?Kl5fSMk<|t}e%RtIzDdr!u!QBZ}$cO{Ua}A2W{vH86O( L`njxgN@xNAS!^R) literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_loop_out2.png b/res/skins/ShadeDark1024x600-Netbook/btn_loop_out2.png new file mode 100644 index 0000000000000000000000000000000000000000..d555fa1920b61ea6566ba69039a5ca085c6ad2f2 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB} z)5S5Q;?~g<3k4Y%cn%%d_M88#t9a`}SC?bE)n|6!Q<>YD5ykZJCR1v~kC{h-8W=oX L{an^LB{Ts52)!a7 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_loop_out2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_loop_out2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..3ea4e8aa1042de44813a6b9c17aa5ac5b06b3b8d GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jT?!778*j@Ekg@?Kl5fSMk<|t}e%RtIzDdr!u!QBZ}$cO{Ua}A2W{vH86O( L`njxgN@xNAS!^R) literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover.png b/res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover.png new file mode 100644 index 0000000000000000000000000000000000000000..3b91b1947bfcfc5d468d2a1f635dc174af65c984 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z<>}%WQgQ3;L{BaT0}ke^|Lu1z;R*S$=*z2y>JzTg0=8kAe?D<}yM}F2OI3{x74x6v h@BgkTbN|if43p-tnl6isSPwLX!PC{xWt~$(6970lDdYeE literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover_over.png new file mode 100644 index 0000000000000000000000000000000000000000..3d19e06e73aadaa40c8a695ab360479d5d58cb38 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zm8XkiNX4zU6Fs>Y3^6f?GfbMpYPu{kVm;6l22WQ%mvv4FO#m#EDZBsx literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1.png new file mode 100644 index 0000000000000000000000000000000000000000..957d9343476ba9349d72c0fe5326badf319fa628 GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU6_+oR9HkoN!3u# z$lk`$&)G99AR;v+IwLGDTdCc^38={4)5S5Q;?~x)mRt-9JgkAH-KYQZ>vYKb_^b@x zmlkwqM^%Te!!&8j`5dQPS1@f~dt^aX%OsC-<*mvqT)&7OR;xY7Jw@%yc6Ojq44$rj JF6*2UngCTLFZKWc literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..9b13fbd252b7267c0f85c5e89522d41080868bc4 GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU6_+cQb5H_$}?Fx zy-l}co#Bi_);nL>9{g;7{1=y-h9OXqy{C&~NX4zKXDzuH6nIzzO}kJ3<=5$u_wiX7 zye}>2&W@@MU59DXmh(AIx2|B?zV^t1s+LI}<;q)?SGax=J*-xHj(dvQm+kC8qZmA0 L{an^LB{Ts51P(Mh literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2.png new file mode 100644 index 0000000000000000000000000000000000000000..957d9343476ba9349d72c0fe5326badf319fa628 GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU6_+oR9HkoN!3u# z$lk`$&)G99AR;v+IwLGDTdCc^38={4)5S5Q;?~x)mRt-9JgkAH-KYQZ>vYKb_^b@x zmlkwqM^%Te!!&8j`5dQPS1@f~dt^aX%OsC-<*mvqT)&7OR;xY7Jw@%yc6Ojq44$rj JF6*2UngCTLFZKWc literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..9b13fbd252b7267c0f85c5e89522d41080868bc4 GIT binary patch literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU6_+cQb5H_$}?Fx zy-l}co#Bi_);nL>9{g;7{1=y-h9OXqy{C&~NX4zKXDzuH6nIzzO}kJ3<=5$u_wiX7 zye}>2&W@@MU59DXmh(AIx2|B?zV^t1s+LI}<;q)?SGax=J*-xHj(dvQm+kC8qZmA0 L{an^LB{Ts51P(Mh literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_up1.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_up1.png new file mode 100644 index 0000000000000000000000000000000000000000..e37f5168806c987923d0174f0db4ad1ad09e7bd8 GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaDgizru0WcdU6_+ol!sSRSVTce zMngl}T+hhf#?jB&Gb|t?H7qXMzWQzmP@SEpi(^Q|t*z$``4|*9oCCEr7JT2ozFGKI z;Qt>^mjWt2zmnx^d&TpR-7J)0p5Sf;jh{wmv|4yLBj0~nyx$|b@e|J#EtU)dl3mEEaktack>&Lp}xt4(C8^jRoKLuWuH< z75M*$)1`ol&#z?p+FtQIWH$?Cm?yYfLF1><8Lbu`&dB#)7Vr0nZv4b^MfuLg`9PBx NJYD@<);T3K0RRb6HB$fp literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_up2.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_up2.png new file mode 100644 index 0000000000000000000000000000000000000000..e37f5168806c987923d0174f0db4ad1ad09e7bd8 GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaDgizru0WcdU6_+ol!sSRSVTce zMngl}T+hhf#?jB&Gb|t?H7qXMzWQzmP@SEpi(^Q|t*z$``4|*9oCCEr7JT2ozFGKI z;Qt>^mjWt2zmnx^d&TpR-7J)0p5Sf;jh{wmv|4yLBj0~nyx$|b@e|J#EtU)dl3mEEaktack>&Lp}xt4(C8^jRoKLuWuH< z75M*$)1`ol&#z?p+FtQIWH$?Cm?yYfLF1><8Lbu`&dB#)7Vr0nZv4b^MfuLg`9PBx NJYD@<);T3K0RRb6HB$fp literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_left_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_left_over.png new file mode 100644 index 0000000000000000000000000000000000000000..3da9d9ba7064ec5b1dfe4e0553f76b337364a92c GIT binary patch literal 144 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q7SoB*E?*NBLSurrCFZ<9m6r-c3h zA^)iy`+>5oB|(0{%-q_IH_kXM>;(#Fc)B=-RNUHo!V+jI2eU!{xBu~CM;s4^8oSM7 l^q!Re($ih+b*Ae%mdM2nlROuxL;>|Mc)I$ztaD0e0sxbLD? literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_master.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_master.png new file mode 100644 index 0000000000000000000000000000000000000000..fa1dca05103d559c339061266a44f09a8ae9c130 GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESh-*YdM0iYESZqdETvk|I zHYWPT@;@7>n5!hnFPNEIRJ(EdiW~2K=$bD&0~EFKba4!+xV87hX+8!49+!(oOV9kT z->~V^4bL5!v(8LBKlAH+opT;a99$cfM4$9Ud9Snnb&&7NI>BVu6Pu-U0~lwu>N>s$ Pn#17f>gTe~DWM4fXjVU^ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_right_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_right_over.png new file mode 100644 index 0000000000000000000000000000000000000000..8d86397bb3be0164bd670ede3da9a4e88a94e96a GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESh-*YdMCh5s(1%GOZ<9m5 zri6S4qaP_DFr=;%*926|RTAVE%*-vSozpn|#=9Q}SQT7>qPCtcjv*Dd_MS21V^H95 z4&0)2;xE5W;^N)=dgqJ$Jag)uVOOEcCx`x7e8&$7Mm$_FU+HoT`_YuzzFlfhW^#uW WFrNN+j-eW85QC?ypUXO@geCwS8A9Ly literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_left_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_left_over.png new file mode 100644 index 0000000000000000000000000000000000000000..3da9d9ba7064ec5b1dfe4e0553f76b337364a92c GIT binary patch literal 144 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q7SoB*E?*NBLSurrCFZ<9m6r-c3h zA^)iy`+>5oB|(0{%-q_IH_kXM>;(#Fc)B=-RNUHo!V+jI2eU!{xBu~CM;s4^8oSM7 l^q!Re($ih+b*Ae%mdM2nlROuxL;>|Mc)I$ztaD0e0sxbLD? literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_master.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_master.png new file mode 100644 index 0000000000000000000000000000000000000000..d50ce3b272018f65e57e4fe4a5f4c608ccae088b GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87luCe4h-(BGgvX?X#b$)XWrf9M zW1@WvA5H+O=Pn8I3ub2H7S(Q?zT(Ea9~)dY$^nHPJY5_^DsFAPaFCBdfQR|Q!sKiJ z|HnT(@<`>P^W=TWPbYm5JewA-Z9VBkliwT{D<=KUHfI*;C@ABkeU&o_}V#hvkK U?yBRjfo3syy85}Sb4q9e08MH_%m4rY literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_right_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_right_over.png new file mode 100644 index 0000000000000000000000000000000000000000..8d86397bb3be0164bd670ede3da9a4e88a94e96a GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESh-*YdMCh5s(1%GOZ<9m5 zri6S4qaP_DFr=;%*926|RTAVE%*-vSozpn|#=9Q}SQT7>qPCtcjv*Dd_MS21V^H95 z4&0)2;xE5W;^N)=dgqJ$Jag)uVOOEcCx`x7e8&$7Mm$_FU+HoT`_YuzzFlfhW^#uW WFrNN+j-eW85QC?ypUXO@geCwS8A9Ly literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pfl1.png b/res/skins/ShadeDark1024x600-Netbook/btn_pfl1.png new file mode 100644 index 0000000000000000000000000000000000000000..d564354140f4599770945132dcf876e0298802d9 GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)QpN#3A+A80jZK)FTS7okPE<@; zN=8#f-9%HzQqRcV*xbd!*2~t(-^nA?-8af7I3X}1H7qXMg6U>4P+PI5i(^Q|t)&+o z`Hm=XI9y~D)xC7^_xv)WmAc#+vzOJURv7C1xMQkUbwuk-(Xpt9^4lhuGM7&Da}X3> zcRShj+nR!$wT8Ht}pWB}`2$r#ZICE0s=0}EPiFw_HycIgu f;ih$^?`PNX+sUv__`Mvnj9s_DC_H=O!skpWD zq9flC1rCRcjH0@i4*s5BX0%e5J7e~;`qT1XJeHiGB`( z!s~7)yM9|!kn{HBN3+?gg?G9NGa2hE-(Bl4p89k9vj)L3whw1cO5FU&kSsB;yO6g+ h$2#1!uJrxvI(|DD)(O8?4P+PI5i(^Q|t)&+o z`Hm=XI9y~D)xC7^_xv)WmAc#+vzOJURv7C1xMQkUbwuk-(Xpt9^4lhuGM7&Da}X3> zcRShj+nR!$wT8Ht}pWB}`2$r#ZICE0s=0}EPiFw_HycIgu f;ih$^?`PNX+sUv__`Mvnj9s_DC_H=O!skpWD zq9flC1rCRcjH0@i4*s5BX0%e5J7e~;`qT1XJeHiGB`( z!s~7)yM9|!kn{HBN3+?gg?G9NGa2hE-(Bl4p89k9vj)L3whw1cO5FU&kSsB;yO6g+ h$2#1!uJrxvI(|DD)(O8?4P+PI5i(^Q|t)&+o z`Hm=XI9y~D)xC7^_xv)WmAc#+vzOJURv7C1xMQkUbwuk-(Xpt9^4lhuGM7&Da}X3> zcRShj+nR!$wT8Ht}pWB}`2$r#ZICE0s=0}EPiFw_HycIgu f;ih$^?`PNX+sUv__`Mvnj9s_DC_H=O!skpWD zq9flC1rCRcjH0@i4*s5BX0%e5J7e~;`qT1XJeHiGB`( z!s~7)yM9|!kn{HBN3+?gg?G9NGa2hE-(Bl4p89k9vj)L3whw1cO5FU&kSsB;yO6g+ h$2#1!uJrxvI(|DD)(O8?7_C2 VzZ0Kan}J#wJYD@<);T3K0RS)(B!2(^ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..805434339dc5aa57a944e6d14f9bb690ed0f95fb GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaLIFM@u0WcdU06B2&1~g0+k>C& zkN-OIHYpD%s_yCH7*cWT>Mz3JkTe8#>HPkl36$@J2g V_1}q4uFXI#44$rjF6*2UngBGqC@}y4 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2.png new file mode 100644 index 0000000000000000000000000000000000000000..1addedb816d86e71b442af449c0806cd0fb6ed8b GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaLIFM@u0WcdU0Bb^-YXzBBswE3 zF8gtIm?%(G-P6S}q~g}u6P7?jIG7F2|F6HfbV0(wt385x)5RtEjC~)T`ewG0>7_C2 VzZ0Kan}J#wJYD@<);T3K0RS)(B!2(^ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..805434339dc5aa57a944e6d14f9bb690ed0f95fb GIT binary patch literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaLIFM@u0WcdU06B2&1~g0+k>C& zkN-OIHYpD%s_yCH7*cWT>Mz3JkTe8#>HPkl36$@J2g V_1}q4uFXI#44$rjF6*2UngBGqC@}y4 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_up1.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_up1.png new file mode 100644 index 0000000000000000000000000000000000000000..d78dbb4c2321745f1a0fdc1046579479b5284f7e GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcdUD(vh%_|@_EG~P) zEQaksA!AP$$B>F!ThBUjF&J<#A6)kFfBw7Fe6FS&wtN!)u}i*PIG3@~Id6T6m_xVK fk>&SaYEQGh8q2cwYm?qYpmqjNS3j3^P6xAzo%7bGh&gmy g9a(<=rS>%2tFbI=zc%Sj1ZrpSboFyt=akR{0Kbha(*OVf literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_up2.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_up2.png new file mode 100644 index 0000000000000000000000000000000000000000..d78dbb4c2321745f1a0fdc1046579479b5284f7e GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcdUD(vh%_|@_EG~P) zEQaksA!AP$$B>F!ThBUjF&J<#A6)kFfBw7Fe6FS&wtN!)u}i*PIG3@~Id6T6m_xVK fk>&SaYEQGh8q2cwYm?qYpmqjNS3j3^P6xAzo%7bGh&gmy g9a(<=rS>%2tFbI=zc%Sj1ZrpSboFyt=akR{0Kbha(*OVf literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_play1.png b/res/skins/ShadeDark1024x600-Netbook/btn_play1.png new file mode 100644 index 0000000000000000000000000000000000000000..827d58c313e32bffb7e71e55dabaa07e025eb00c GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQYryHA+A80on4rdQ&d<)K}tqL z&&b}?%FV{n&)GA~Dz>(K+tRjn^lFyotdcNlc0B7OBQYryHA+A80on4rdM^Zq=OvKbn zIlWDze3ovQ-<%vN5rJ^0!F_^&Hz=jDLv{5)M8Ln>}vooLB*z<`Hk?>^I6#EP3exG?~HE)z4*}Q$iB}RJ=jw literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_play2.png b/res/skins/ShadeDark1024x600-Netbook/btn_play2.png new file mode 100644 index 0000000000000000000000000000000000000000..827d58c313e32bffb7e71e55dabaa07e025eb00c GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQYryHA+A80on4rdQ&d<)K}tqL z&&b}?%FV{n&)GA~Dz>(K+tRjn^lFyotdcNlc0B7OBQYryHA+A80on4rdM^Zq=OvKbn zIlWDze3ovQ-<%vN5rJ^0!F_^&Hz=jDLv{5)M8Ln>}vooLB*z<`Hk?>^I6#EP3exG?~HE)z4*}Q$iB}RJ=jw literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_play_sampler.png b/res/skins/ShadeDark1024x600-Netbook/btn_play_sampler.png new file mode 100644 index 0000000000000000000000000000000000000000..e5753f4b2fcadc8d17d1f7dc0203bbc68af6ffec GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU07H|K}tqLL)%=> z$llb-&Dk?7AR;v+IwLGD`(V+w{Xj+Lo-U3d6}PsYHxyz};9z!iWc~P`|C(L+tEtfw z78XWVwX9}X&bqKt^Ip@1IR>KEcYI>ye_YHz!dKkF-IG+WBnvcx!PC{xWt~$(6996p BEvx_l literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_play_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_play_sampler_over.png new file mode 100644 index 0000000000000000000000000000000000000000..a1569ce0dc67319b311fc2ccfb46baf5678d5110 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU06WHOvKbnF04p7 zy-lNhmf?&;);nL>9{g;7{1?y8-?>0V=AJH&Ar-f_o;MU?P~c#8bY%VbpZ}U&_^YYW z6BZUmSGBBWSkAhzQuAKZg*gVI)^~hj<$qkvKf+hs!rhZpuOtgJg2B_(&t;ucLK6TD Cc`^?G literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_quantize1.png b/res/skins/ShadeDark1024x600-Netbook/btn_quantize1.png new file mode 100644 index 0000000000000000000000000000000000000000..833e896964942e39727ad5fc90b49430cd05cc85 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lyrbkh-*YdL|ANQSX>qiWrxK< zk%RjcVW2XOk|4ieW@RI%po0%S>=S#K1QfIKba4!+xV873BNu}Lhs#Az&xJW{ zUdNtir}RjlTW-b6X;&n!WTg1T%;DK`PbO&k9UZ;Ee>KU>t(%#29b(Ei15IJ@boFyt I=akR{0Mk)6wEzGB literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_quantize1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_quantize1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..468833d8bfb9fd459889348a452232aec205caef GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lyrbkh-*YdMChyJ(2vQXpHo7= zq=bG?3H<>@ZRs7yfXX;Zg8YJ+m5rQ&4nFv>PwZh5P|V8H#WAGf*4}fDTnq{vE*CwG zpZ`-o7v{8i9ebXg(j$FtxfL&`U6Htwk>VFKhiA(@nV{)+bo2uM)g&{wZf4STh$-I; PG=;&_)z4*}Q$iB}HcdPC literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_quantize2.png b/res/skins/ShadeDark1024x600-Netbook/btn_quantize2.png new file mode 100644 index 0000000000000000000000000000000000000000..833e896964942e39727ad5fc90b49430cd05cc85 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lyrbkh-*YdL|ANQSX>qiWrxK< zk%RjcVW2XOk|4ieW@RI%po0%S>=S#K1QfIKba4!+xV873BNu}Lhs#Az&xJW{ zUdNtir}RjlTW-b6X;&n!WTg1T%;DK`PbO&k9UZ;Ee>KU>t(%#29b(Ei15IJ@boFyt I=akR{0Mk)6wEzGB literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_quantize2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_quantize2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..468833d8bfb9fd459889348a452232aec205caef GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lyrbkh-*YdMChyJ(2vQXpHo7= zq=bG?3H<>@ZRs7yfXX;Zg8YJ+m5rQ&4nFv>PwZh5P|V8H#WAGf*4}fDTnq{vE*CwG zpZ`-o7v{8i9ebXg(j$FtxfL&`U6Htwk>VFKhiA(@nV{)+bo2uM)g&{wZf4STh$-I; PG=;&_)z4*}Q$iB}HcdPC literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reloop1.png b/res/skins/ShadeDark1024x600-Netbook/btn_reloop1.png new file mode 100644 index 0000000000000000000000000000000000000000..f554efbf8e9f4335a5e2a81eddb2ba87097cd6f8 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z?CIhdQgQ3;L`R@r7M73y-Loxoh2AYQneN#ovq3hV>1xPmdKI;Vst0K%0eAOHXW literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reloop1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_reloop1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..16daa7ccb64eb951cda7396fd0275f83b9a87122 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zv8Rh;NX4zU6CHtiSy(>)ch9!W6?(VKWV&aU%m&$brmGYcZKJ=gJKz3@{x Y_U3k0ZNozopr0JsPw_y7O^ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reloop2.png b/res/skins/ShadeDark1024x600-Netbook/btn_reloop2.png new file mode 100644 index 0000000000000000000000000000000000000000..f554efbf8e9f4335a5e2a81eddb2ba87097cd6f8 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z?CIhdQgQ3;L`R@r7M73y-Loxoh2AYQneN#ovq3hV>1xPmdKI;Vst0K%0eAOHXW literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reloop2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_reloop2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..16daa7ccb64eb951cda7396fd0275f83b9a87122 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zv8Rh;NX4zU6CHtiSy(>)ch9!W6?(VKWV&aU%m&$brmGYcZKJ=gJKz3@{x Y_U3k0ZNozopr0JsPw_y7O^ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_repeat1.png b/res/skins/ShadeDark1024x600-Netbook/btn_repeat1.png new file mode 100644 index 0000000000000000000000000000000000000000..00317532fcaf30f8c1d07c0478675a7318944b34 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lzMo$QBh9>3id3-Mt-` r8fSSlFn%se*9f01)wiDiW+~${4Hm{_?#I-DrZafD`njxgN@xNAj}uM# literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_repeat1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_repeat1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..504abded9cc94d20a878ea5e81a9cda17ad5a775 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lzMRI zn;iNzCGo$QBh9>3id3-Mt-` r8fSSlFn%se*9f01)wiDiW+~${4Hm{_?#I-DrZafD`njxgN@xNAj}uM# literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_repeat2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_repeat2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..504abded9cc94d20a878ea5e81a9cda17ad5a775 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lzMRI zn;iNzCGP~dU7xN_OE z#DD**x!d@}c70jbHaYggPn0=|?e&n&t<$UL^SpvGe4B?7L-b)$JU2&4l lFoRFYDfQ~cMC+eR*;TBVh5vq&+y*q8!PC{xWt~$(69AzMN%jB$ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_repeat_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_repeat_sampler_over.png new file mode 100644 index 0000000000000000000000000000000000000000..18a2d50ee2c9c8c88da6456ed49130262b07c116 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lzMz_;6RjinW|9+F)1~i+&)78&qol`;+0LWBNQvd(} literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reverse1.png b/res/skins/ShadeDark1024x600-Netbook/btn_reverse1.png new file mode 100644 index 0000000000000000000000000000000000000000..bafa178b5153eff3a671f1b8222e948b49cfabf3 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQv3lvA+A80on4rRS27?XH7qXs zygh#zP{_>F#WAGf*3~nLLJSH#M;sRX`G0om#GO_h_fATcY@Rn|>Zw~wjY^GbjW0j1 iXjuh>vzGLy#xc*{%(X{fzhW-X1O`u6KbLh*2~7a9VJ!au literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reverse1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_reverse1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..06e14aed300aeaf08a3ccf5005df908fd7da3a42 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQv3lvA+A80on4q)M9F&REBoWW z4*bhm1Qas!ba4!+xOMf6q7Z`u&k=_OfBv7HI&r5}$GwwMC7b6>nR@D$QlnC%TI0*l jD_T|o;jAV7sd3D+H*@XL*RPlhG=ag>)z4*}Q$iB}sf;iA literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reverse2.png b/res/skins/ShadeDark1024x600-Netbook/btn_reverse2.png new file mode 100644 index 0000000000000000000000000000000000000000..bafa178b5153eff3a671f1b8222e948b49cfabf3 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQv3lvA+A80on4rRS27?XH7qXs zygh#zP{_>F#WAGf*3~nLLJSH#M;sRX`G0om#GO_h_fATcY@Rn|>Zw~wjY^GbjW0j1 iXjuh>vzGLy#xc*{%(X{fzhW-X1O`u6KbLh*2~7a9VJ!au literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reverse2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_reverse2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..06e14aed300aeaf08a3ccf5005df908fd7da3a42 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQv3lvA+A80on4q)M9F&REBoWW z4*bhm1Qas!ba4!+xOMf6q7Z`u&k=_OfBv7HI&r5}$GwwMC7b6>nR@D$QlnC%TI0*l jD_T|o;jAV7sd3D+H*@XL*RPlhG=ag>)z4*}Q$iB}sf;iA literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_rewind1.png b/res/skins/ShadeDark1024x600-Netbook/btn_rewind1.png new file mode 100644 index 0000000000000000000000000000000000000000..b2075d8a859cc3fab9dc4dddcae8baca1b266264 GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa>H$6>u0WcdU6_YgQdmSmN=8FT z)lfs*+{V$**)z;5AT}T(H6%JCEG|2%Sjz^e)XUSwF{I+w)_zN&0}29%lZqI)|JUc| zSn)@zX_+LK+7&%JR&F@WrYwM0Oj{wc=0NS?MQR$|vAZKx7RrjbK9oDEv~!7Z?U!hu aO2#`{ye`k=G-m@%W$<+Mb6Mw<&;$Shr!yx2 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_rewind1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_rewind1_over.png new file mode 100644 index 0000000000000000000000000000000000000000..fd5842795d0e495d4e825e8cbda63d29d4647dfb GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa>H$6>u0WcdU6@-$NkGL+#MDd5 zGg&UINVj91;fzCOE3aAad}Vv^v;Fa3bzKjNfl9qRT^vIyZf)(i6gr?Fa5$-mf%|`b zevTD?w3?Pla;aU>vt#9k(`?EDc*V38B5MxR9$uuT(H*-xQe~m6nCnBiqe?rM7}tJ@ b_NipNqs8m;Oipt)&{PIbS3j3^P6H$6>u0WcdU6_YgQdmSmN=8FT z)lfs*+{V$**)z;5AT}T(H6%JCEG|2%Sjz^e)XUSwF{I+w)_zN&0}29%lZqI)|JUc| zSn)@zX_+LK+7&%JR&F@WrYwM0Oj{wc=0NS?MQR$|vAZKx7RrjbK9oDEv~!7Z?U!hu aO2#`{ye`k=G-m@%W$<+Mb6Mw<&;$Shr!yx2 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_rewind2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_rewind2_over.png new file mode 100644 index 0000000000000000000000000000000000000000..fd5842795d0e495d4e825e8cbda63d29d4647dfb GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa>H$6>u0WcdU6@-$NkGL+#MDd5 zGg&UINVj91;fzCOE3aAad}Vv^v;Fa3bzKjNfl9qRT^vIyZf)(i6gr?Fa5$-mf%|`b zevTD?w3?Pla;aU>vt#9k(`?EDc*V38B5MxR9$uuT(H*-xQe~m6nCnBiqe?rM7}tJ@ b_NipNqs8m;Oipt)&{PIbS3j3^P6sNxHCaFlumf8#We=)z$1G_J*^>tBMJ{;%YFLLX zK+UHSzgU&TLUd6>@CbpB#Dqd3r8uyaSct$(cJoj8?R1`4h`=oh1D__6cnv~FM|cN9 zMG+o!MqbIwP$kO9c5sNxHCaFlumf8#We=)z$1G_J*^>tBMJ{;%YFLLX zK+UHSzgU&TLUd6>@CbpB#Dqd3r8uyaSct$(cJoj8?R1`4h`=oh1D__6cnv~FM|cN9 zMG+o!MqbIwP$kO9c5OBQk(%kA+A80on1IAF8fDwx*(8a z?&;zfQgQ3;L_OBQk(%kA+A80on6@e_^%m!toA^T zxu=U`NX4zU6Ak$o7+9D;{&!#8%%_xC&OIqxyy!sLo|zgQ6SHF%%=D39E>>?jsbcbe e?t}e1k27|Au^O5bBv$~9VDNPHb6Mw<&;$TXu_lWE literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_sync2.png b/res/skins/ShadeDark1024x600-Netbook/btn_sync2.png new file mode 100644 index 0000000000000000000000000000000000000000..f4b5ed9fdb15ed6e30b2960a1ad4b6b131e2fe2e GIT binary patch literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z?&;zfQgQ3;L_OBQk(%kA+A80on6@e_^%m!toA^T zxu=U`NX4zU6Ak$o7+9D;{&!#8%%_xC&OIqxyy!sLo|zgQ6SHF%%=D39E>>?jsbcbe e?t}e1k27|Au^O5bBv$~9VDNPHb6Mw<&;$TXu_lWE literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_tap1.png b/res/skins/ShadeDark1024x600-Netbook/btn_tap1.png new file mode 100644 index 0000000000000000000000000000000000000000..6a5d1bbe0b9e3ab7d12ebef873b7e7beedccf58b GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^AS}Yc3?wx_&$t<7zx;^w2_ToTB*-tA z!Qt7BG$2R9)5S5Q;?~=PhKxYoA%)+|G8)<^Qv<02u+EE{-7;x87de&Bq|Xn6`@ z$&0u0m~bst%=_dv6+!n+%9@RnTqZ9y?~T56vF@iNv!mYWMccn@NZEJV-&4@yt<7zx;^w2_ToTB*-tA z!Qt7BG$2R9)5S5Q;?~=PhKxYoA%)+|G8)<^Qv<02u+EE{-7;x87de&Bq|Xn6`@ z$&0u0m~bst%=_dv6+!n+%9@RnTqZ9y?~T56vF@iNv!mYWMccn@NZEJV-&4@yp=fS?83{1OTbO B9!3BF literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_tap_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_tap_sampler_over.png new file mode 100644 index 0000000000000000000000000000000000000000..d26e6043ad4998f41c5d9b0fa5bb04819fb9e8fc GIT binary patch literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^IzTMM!3-qDwa&Z)QfvV}A+C~=lA%9RY$vH+1&Ww^ zx;TbZ+Rs d{9jt1y||0%(kFS{_dp{UJYD@<);T3K0RW!PDB=JB literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot1.png new file mode 100644 index 0000000000000000000000000000000000000000..980b5fc2b19e28ddfaf64229410a8f3033c38316 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRh?c)B=-RNT5c(UI$b0uRgOCI9P-U%y!C6%o`@mTmsZb+c~y*%y3fPtXu; tyPbzu#4P!>QOr@OQ_o55+#4}hy(7I`iv5?BYJm1Jc)I$ztaD0e0sz#QMgaf- literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot2.png new file mode 100644 index 0000000000000000000000000000000000000000..980b5fc2b19e28ddfaf64229410a8f3033c38316 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRh?c)B=-RNT5c(UI$b0uRgOCI9P-U%y!C6%o`@mTmsZb+c~y*%y3fPtXu; tyPbzu#4P!>QOr@OQ_o55+#4}hy(7I`iv5?BYJm1Jc)I$ztaD0e0sz#QMgaf- literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off1.png new file mode 100644 index 0000000000000000000000000000000000000000..17b655ff2b5b4b8af2920faf128aa3e7262990ac GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhCdAc};RNT5c@gmm&1rFD{cmCIZ4mx*QF33}<>_FZ{ zM=yk~bP0l+XkK_*F^c literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off2.png new file mode 100644 index 0000000000000000000000000000000000000000..17b655ff2b5b4b8af2920faf128aa3e7262990ac GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhCdAc};RNT5c@gmm&1rFD{cmCIZ4mx*QF33}<>_FZ{ zM=yk~bP0l+XkK_*F^c literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one1.png new file mode 100644 index 0000000000000000000000000000000000000000..ee32e7217a4338cde70c828680927c5c13e8ea40 GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhCdAc};RNT5cIgqQtfX8|9*MI3{+>9TSH?HRS;&}3y zT&lgJienCM#I8W0<~zv>MPF8JP?9~myZ4EsiH^u*g+3A0LUr9=f!iBTOw>8q*^+oP zJM(XU-_%FpQ5)CZ+ibuu&Xl{Swznnik@?ztFwla9S`njxgN@xNALbOSK literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one2.png new file mode 100644 index 0000000000000000000000000000000000000000..ee32e7217a4338cde70c828680927c5c13e8ea40 GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhCdAc};RNT5cIgqQtfX8|9*MI3{+>9TSH?HRS;&}3y zT&lgJienCM#I8W0<~zv>MPF8JP?9~myZ4EsiH^u*g+3A0LUr9=f!iBTOw>8q*^+oP zJM(XU-_%FpQ5)CZ+ibuu&Xl{Swznnik@?ztFwla9S`njxgN@xNALbOSK literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal1.png new file mode 100644 index 0000000000000000000000000000000000000000..be8ffc82df409d28d2815d93e53b29e2a7d76626 GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0y~yU@QW%nK+n%WYPNNen5&jz$e6&;f!(p(<6OAuDGX* sV@SoVw+9({fjkF;zrn8)>$cf2GCgBp+@$P02Pnwk>FVdQ&MBb@0QjC5v;Y7A literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal2.png new file mode 100644 index 0000000000000000000000000000000000000000..82587917fedf138231051a358064999f7a8472bf GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0y~yU@QW%nK+n%WYPNNen5&jz$e7@|9@jsv8lm8uDGX* sV@SoVw+9({fjkF;zrn8)>$cf2GCgBp+@$P02Pnwk>FVdQ&MBb@0PVaOV*mgE literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal3.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal3.png new file mode 100644 index 0000000000000000000000000000000000000000..51bc3a21f758ba7090c5d8dbb7816732da5bef7b GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0y~yU@QW%nK+n%WYPNNen5&jz$e7@3$cf2GCgBp+@$P02Pnwk>FVdQ&MBb@0NpScDF6Tf literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical1.png new file mode 100644 index 0000000000000000000000000000000000000000..fd75decec79cbdecd02628c62475e2918189f1aa GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^OhEjLgBeKfS(%muq?iMILR=Zn7}q~N(g);Q# literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical3.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical3.png new file mode 100644 index 0000000000000000000000000000000000000000..d707bd2d6f1559294574f006950d14fe53baf18e GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^OhEjLgBeKfS(%muq?iMILR`-cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhyd%8G=RNT5cF_5dlfXDgutH0-Sq%#9A3b6hDaFCUw7T0gX(b&Ur2k97?mcI=VJIP+DR% zWx3-~SCdZFMaO2zzHi;L?aHIY$6xJRAFa}AEa*I0s8u%l&ry9FhEG4Z_%!TnfG%KQ N@O1TaS?83{1OTVJPT~Ln literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_abs2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_abs2.png new file mode 100644 index 0000000000000000000000000000000000000000..25f0f41d8c0c9f972309f737290dd3f893e1b99a GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhyd%8G=RNT5cF_5dlfXDgutH0-Sq%#9A3b6hDaFCUw7T0gX(b&Ur2k97?mcI=VJIP+DR% zWx3-~SCdZFMaO2zzHi;L?aHIY$6xJRAFa}AEa*I0s8u%l&ry9FhEG4Z_%!TnfG%KQ N@O1TaS?83{1OTVJPT~Ln literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const1.png new file mode 100644 index 0000000000000000000000000000000000000000..7bbcb092c30441eb5e0d65915dee8550cd013065 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhyc)B=-RNOi{sgbL}fWt-i@Be(gSWBZz+5%MueJ7pS z%##EjrM3rM`JuAO@X;RIjxw7Cy*>FMLK|z6oo4@7%;mo1Y|Fb1T-q03YrCGZV|6U) zsAy&S=oix~X|zopr0OU$bWdHyG literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const2.png new file mode 100644 index 0000000000000000000000000000000000000000..7bbcb092c30441eb5e0d65915dee8550cd013065 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhyc)B=-RNOi{sgbL}fWt-i@Be(gSWBZz+5%MueJ7pS z%##EjrM3rM`JuAO@X;RIjxw7Cy*>FMLK|z6oo4@7%;mo1Y|Fb1T-q03YrCGZV|6U) zsAy&S=oix~X|zopr0OU$bWdHyG literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_rel1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_rel1.png new file mode 100644 index 0000000000000000000000000000000000000000..338eaaf6051c4c84730fe7ed3a7bd780729d5035 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|NnnUd6lrZ>{qW| z&+2Qx1{7i{3GxeOU?`h>)&j_@^mK6yskpUuvNsolB9F86FB))V;{_!Db7g`;KfU1(L!fgH(8QgR~Cs zoE5TT|32+mEQgZ(SzacjXHB!{b?Wuo9{qW| z&+2Qx1{7i{3GxeOU?`h>)&j_@^mK6yskpUuvNsolB9F86FB))V;{_!Db7g`;KfU1(L!fgH(8QgR~Cs zoE5TT|32+mEQgZ(SzacjXHB!{b?Wuo9#WAGf*4rZ+d4W7egFp3KDh{SxSQs$HxZpF-9#4j>P4^3?0i_r`UHx3vIVCg! E0Hf_7i2wiq literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display2.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display2.png new file mode 100644 index 0000000000000000000000000000000000000000..8e0d1d7667a6b6e34ec27bac88914152fdb3b227 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^tUw&d!3-pI*Ud2lQp^E9A+D10Dy~=jvw&PNPZ!6K pid%0FGBN^r4hsLnrJXi1Ffu-4U_2(;C#WAGf*4rZ+d4W7egFp3KDh{SxSQs$HxZpF-9#4j>P4^3?0i_r`UHx3vIVCg! E0Hf_7i2wiq literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master1.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master1.png new file mode 100644 index 0000000000000000000000000000000000000000..8e0d1d7667a6b6e34ec27bac88914152fdb3b227 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^tUw&d!3-pI*Ud2lQp^E9A+D10Dy~=jvw&PNPZ!6K pid%0FGBN^r4hsLnrJXi1Ffu-4U_2(;C#WAGf*4rZ+d4W7egFp3KDh{SxSQs$HxZpF-9#4j>P4^3?0i_r`UHx3vIVCg! E0Hf_7i2wiq literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master2.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master2.png new file mode 100644 index 0000000000000000000000000000000000000000..8e0d1d7667a6b6e34ec27bac88914152fdb3b227 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^tUw&d!3-pI*Ud2lQp^E9A+D10Dy~=jvw&PNPZ!6K pid%0FGBN^r4hsLnrJXi1Ffu-4U_2(;C#WAGf*4rZ+d4W7egFp3KDh{SxSQs$HxZpF-9#4j>P4^3?0i_r`UHx3vIVCg! E0Hf_7i2wiq literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_microphone.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_microphone.png new file mode 100644 index 0000000000000000000000000000000000000000..ae180c850c5c8b1091b16219d8513675499bad34 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^>OjoO!3-onO0rr3Ddqs55LZcg71t~NSwOC+r;B4q o#jU#s85x1Rh6(@He^Z{tz;J?r@%>8CJfIMRr>mdKI;Vst07gOOjoO!3-onO0rr3DYgKg5Lcna9{>L{tbgPd0u+(* xba4!+xOMiFA|sG@$lyi%FP9xDRf1OU`Pr=)M46{tQUZ!Ic)I$ztaD0e0swFP8ZQ6< literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_sampler.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_sampler.png new file mode 100644 index 0000000000000000000000000000000000000000..28ca03b3c78e8c5218f1b76b060499c0954e7afe GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^tU&C}!3-pSDAsNOQfvV}A+GG~!jkeTs~qkx1Bxhl zx;TbZ+&0001;NklK4vvAbVnrs zP@7Wfo2+5H3$(*z?o)I;i^Vsj_-d+=qQmPQo#Zt20t;JoyFbZjX3#q_%%}wO#UO(s zD%PVt`ZsiwEaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knob_pitch2.png b/res/skins/ShadeDark1024x600-Netbook/knob_pitch2.png new file mode 100644 index 0000000000000000000000000000000000000000..fafd15e2804ba34dc14ea7058d062b39c673fd07 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM9!3-oF+x2$?DfIxK5LX#_Wm$O@AdiueQB6}{ zL&r!<*TgR@DIg*xFd{W5Dm^47GxSG_<$)9YKt)U?L4Lve_xlN41es{->Eaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knob_pitch_sampler.png b/res/skins/ShadeDark1024x600-Netbook/knob_pitch_sampler.png new file mode 100644 index 0000000000000000000000000000000000000000..fafd15e2804ba34dc14ea7058d062b39c673fd07 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM9!3-oF+x2$?DfIxK5LX#_Wm$O@AdiueQB6}{ zL&r!<*TgR@DIg*xFd{W5Dm^47GxSG_<$)9YKt)U?L4Lve_xlN41es{->Eaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knob_volume1.png b/res/skins/ShadeDark1024x600-Netbook/knob_volume1.png new file mode 100644 index 0000000000000000000000000000000000000000..fafd15e2804ba34dc14ea7058d062b39c673fd07 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM9!3-oF+x2$?DfIxK5LX#_Wm$O@AdiueQB6}{ zL&r!<*TgR@DIg*xFd{W5Dm^47GxSG_<$)9YKt)U?L4Lve_xlN41es{->Eaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knob_volume2.png b/res/skins/ShadeDark1024x600-Netbook/knob_volume2.png new file mode 100644 index 0000000000000000000000000000000000000000..fafd15e2804ba34dc14ea7058d062b39c673fd07 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM9!3-oF+x2$?DfIxK5LX#_Wm$O@AdiueQB6}{ zL&r!<*TgR@DIg*xFd{W5Dm^47GxSG_<$)9YKt)U?L4Lve_xlN41es{->Eaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png new file mode 100644 index 0000000000000000000000000000000000000000..69e64c74b2596c856cce1cf20b0c649057438bca GIT binary patch literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5LZcg6$oa zWwRG!KhPrSk|4ie1_1?K1BZZ+go5&h2~!rVJ#g^Cr3VjQefaeIFPl_<3Q*YrPZ!6K zid(f4ZwfUl@VGW-+}%*RG*svR|CwqFFN;W4$ItnA#+HR?(x%s4O3f-u@9zD6Zs*M- zA<^dxWfr%XNH^EMO;1byH+ApJ|4SGbXDvG49};}rC?R8o_#Ey@jCDbZ*^%zD9Zt4E zQ8w9_J@-NEpAfya*V)lOjPWwqH8AAH5Sy!jzlsLGunlSLDbUr1k+ z(ewJ@|G1gG>m}~q6wiA3St|0B(vqo59hz6?usq%26<7Qs&@*dcF+*jVaHf@B@duz^ O7(8A5T-G@yGywnz$(h0c literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png new file mode 100644 index 0000000000000000000000000000000000000000..f06258ae5beb286177a80d9d788350391a630c41 GIT binary patch literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(r03aiAnCd{7$WX=I0VS>z8ndXIv4J7A`Hmzy%Pf@%{pEDAIAFKi-B;1-X#Wq zN&<@OW?F@OO%DB@0<>lNy2-&nOXW&}{DK(-6m$(70zwjU3d$QMOj)q|z`+w2?mT$< z{MFkJzyDfXcLcfhl&6bhNX4zviLb?)6a-qtFYbws+oYrV-4oeuCiW|ByrWgC;>&Fu-H?;Sko0FdV@cr8DOJAItcuJSm qYwy0K_N17n-=>%wSO=MYjWuK6riO!g6>{GH>i{Z`2{mD2q0yCvCg4pIM)*sS{~lI!AIzkZq^` zvo#mg-zfLiiRHhUyt+~7&Gl!xZ5j!WuKivv#o0OWR%N~X;>pF{rGMI!Sj@H?o)S3G zSLLy#=V0H-Cm&+=UYM+HTzKET`q{;{!nXO>w|(o0|FwF{UXgn|1p!GQayuO2vJ=F1+~^M^JV+*CJV)iN5_w4z8CT91GZiZ>S;Tum$9%$(E zXI<|esl9rQ(#3_7tIBUbxV>b-+7_m@4>H3TxMXYE7VMtD{Z~V9+RHN^E;xKhJm>an zwxZFc^CvZb$7C)E`Nm!O+OW&SaJtPb520kM%KtMKO{(;MZaL*b+_%|_xEfz%96OT0 z^LEBR`FV3>jJ1_FN-T2v+|)bykBU1o;84`Ku4V|BAcR7Xnq^_jGX#skoIp>E)zD20YEaUe1lW zu|a*CWKwyb{FN8(jQr5Q?3GuqE&tCb f?;{`m$%b+B83~)5iJ87YuQGVL`njxgN@xNAC4;tF literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fb3fa1df5cb35363e78da6d707504baf142afc GIT binary patch literal 445 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFA-p zPMZUO49f|wK+^kiB#?C88sM@s5XjI@F!wwg4rI9Q4F-}x91tE%{xxM(aZ_0|-Xk$k(` z`rzlVc_+2_**4Wa{I|v7ZP5j_&I~qxTV@5G50=-$mEC^^r{(k~@%ERn$Uj@Nw*R$K z`roFx+s=Ddoyy%mWBa?sEuF1TPdZGSaq8K}9Xx?6cO6;sRO6J&*1v5loD{5f*%eAO z%YOd&Q1yo4J|kiN*ph^}Et%Kq?+LsWjsLaU)?T%4S$@c0{>rPzxCLqr{^nf#%=R`{ WeC3rJyDtMh$>8bg=d#Wzp$P!Xq_&0t literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png new file mode 100644 index 0000000000000000000000000000000000000000..fee0a6942c02e0dfcb4afbcc0de57d1f215ffb96 GIT binary patch literal 453 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDgXcfmy}llBFByX zPMZUO49f|wK+^kiB#?C88sM@sP%qmG$o4!N4kTUo1_MbTayuLXVR)VciF;oGsrSAX z<^M3&?_La$?Rz&S=v`v)rz9ZTZi%7Xr6kBNn1Mk+L0i|r z+QB0rFeE%7r=YxH!juIE4qmwQ;K}n>AAbM-`)u0DrZ92V@5NU&R@+LP_|*9Qtw{OwW;tbNLz{L9hs1{Sf?LElE!0mvbZy6y4*~8U z7M`DbrE~WQ&%FxE&um^guW07^PnYbvObQG4&dpHp^vO6D!y98JOL_Bu1^v}uSeDq$ eurrH(B787P!t_G&RY{;P89ZJ6T-G@yGywpoK(_q= literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png new file mode 100644 index 0000000000000000000000000000000000000000..a9f30d663eca38ecd1cbebda0ffc176294911d24 GIT binary patch literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBB#v( zK+yf=Ro4#S3rvWAIAFKivco% z-X#WqN&+&RwgmV;jtlvk9Qr*4Xi;*_jd?(OO?z%BLRU(g~`_Q2cQrUA^>>s!`DSlhMz1Ui8{| z#yutey<)8L>P|zu8y-T*UNc_nu9_))>fb`1#R>{v9D&L@YiNT+efNYH* z_TdhhDi32|Cm zzT3wCiR#+EuiN+)88ZGjxKG&D$n^AXi&=QnzaZ0;QnN*055GEo<=HxuTdM0#K5t=Z zxmVNodGe&@niT=tQs-2rsC|x1SzUX{vWWLVsm=Yvfl5;ylqWH4Pwf#>dv-^_Tevuq z@v8Z%hN&X|0}>m*ny)gQARFs5N48n^^!;}6_07L4Q`O!*&p5y5V4G51oc)dLeP5z) szYJD+-H_uXd7*IUg7ZF;b3fQHuFw~rf6D!$4A657p00i_>zopr033Izr2qf` literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png new file mode 100644 index 0000000000000000000000000000000000000000..7280d9a0b883d3ba32398313f081f96398d9daae GIT binary patch literal 423 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1k5?Pcv{te1g0&F%#8Bh`M*17cGJSLe;+nn zFE&$i5jov|rq-y9Ua6+X$`kSwF*`j=;}Zr!>KL5(+tN;K2 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png new file mode 100644 index 0000000000000000000000000000000000000000..07ea2fd98c27f7e87c1d80370cdb2d9a0de87896 GIT binary patch literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 z&RYXqb_N0&cFTM`&xXUr-42I9*q-M=g5Fmmfu#S#SigHQKt{l`_@H-*Kr;AKQt-zl zporZP?~t#_q2E)0wydy!9RRdct|Z7Wm_a~6*T5klFeD){r=VfNlm!P4Ubyt&`Ku2f ze*gLVZ|@t~lR%XxJY5_^DsGidd@I$Yz|+cjaFK|q(c5gv@AVUNC(Ai~{v-LJich+0 zN$h2b#Yen;Cvqt=IJ_^4;%Q{nQ5Uwfk+8{+J%8r4zU97;HCEzVrioq*JZkcZheyKd z)7hEgD(WAVyVophJ#=lIlJHNF>(}?5n0Vy3OqkBph!ux^8uXvt`Do!o{-VMuf~pSN z-DDj!--vAzd2#(jvci3h=`Kamx6gilvGW^6JBf-+%vw z2Z`kY6&~|+aSW-rRXXt{Q`;jNdpBnRLBuX=m2(w1lEH`Saea14mMo|%=n zxMs$*RL3Ltj+;n|33lz_OsN+x*uQP>hI4yQ<}LgAa>j=nQ+(WidnKR$z-c(I_SU=( znFrpi8@t%Wd9 z{tsjQ?!^Gv0ng$Cp2P*cOZ0yd7yKzH_+t`KP|->U$O!qG9Qr*4X!~3?g&jc0sFnoz z1v3aJ=o(l%1O$d8B<2*9H%yqaV9V|U2hU!(^x(GBm`k*y=PUNdEDYqY0PxfqbNqF&ATB~?*mkYtABPsnFl zt?4CrV)K(39P13@=c$_;PxgJ3-0^J=d+Y(e$M>bbx>sIbsJhUW>+QU)MLIOA}2JSh{gT;r`kuQ7PK`njxgN@xNAsM)%> literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png new file mode 100644 index 0000000000000000000000000000000000000000..571a07dcc19ef12fa48991fbd09bea058ee1416c GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(q}?(f&$HohLAS#pK(^O~2q5Ws4kYeuIms>LYjWuK6re5h)MO3*P+O z2TN7;OJ-ev;{Qlpl)-s2Peq)I(5FoXRx_1eGFbP24(N8xIuIy-I`h!7XY7{>ri!s% zSQp4GvgnqRsAyUC6~LhTfK1e8lgnMY}*pMnQB` zPoki1k4zo@S`Z4O@{A^xJ~tb z+Yb56`7dtv@rlh`v7T#nt4t&B)w-813(rkw-(SRHxN^k;foRQ(3Vzupie^svioR`W zy*!eaIs=3jWaq^Hdq4SR;hBpk>I#!~$-Eb^SM{@6j(#EY7|wb-(Ok ZbhssS+NE^U8=!9(JYD@<);T3K0RRxKnj-)J literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png new file mode 100644 index 0000000000000000000000000000000000000000..f50be60339603fb705844bb266d502373cda6845 GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILm*s_6JZdB-@OtW1C6yP z>zo9%MY1HwFPMQrKtb2QAs{57pkcz41qV)Cc<}1Ohu?qyP2kiQ0xCM>>EaktajSOX zZJ}lb9v5aYQCD3L{oAMh|4&m_h}v2fKcnD=FN4sO(DK+wv0}xR6D#DF3otRH8~J+l zDRKq6T~hkm@t{&ZCM7I2=tnrm<*!12d4uiSy}0+}^8{tym~Ir!Fl*lu#w~_@(wE)d zXkU8uM9g!#pY?);bEOU}-IK)qDeWXXUy*~SsDaEyhL9!aCp>egcA3V!bMYPFlgV@V z|NlQV=SF_u{P$UPw(BG9Sl@cx`Woy}>9E#=u}*Vo>g6o&dHntS0%uBn5~YE@VeoYI Kb6Mw<&;$VcSC4f7 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png new file mode 100644 index 0000000000000000000000000000000000000000..bdaa5a414364a2d8b1c670c67710f64465d40fb2 GIT binary patch literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx803t!Rj#_mxQ3eZiVhW@V-k>Ix5PW-YjWuK z6rjlmvmO+IY%U4%3ua&tP|!7S2nb0iXqYf%!PWy8E?v3(;K_&I|5l{g2?JFf_jGX# zskl`;@vT&o0#7S@S6hJR(kn*q{u@ar+b&Cc&VT#f+YhW>jV}sp%zZ57*3B@pJP6jh(!IE zw6rOC_Z~x?DbH7J;;ZWX60j@tWx~?FTix+nm4b41Bx-KUTHyL-`wI7*gYOh)^Bxvs zHF_fZbykMmv3C#u$IX=W_b&N$=j#8eIQ#mp*`2fPS$$kgjEy=b2B{VssU+JhzSrU} V*u0Iw;yBPt44$rjF6*2UngElko+khR literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png new file mode 100644 index 0000000000000000000000000000000000000000..393a509891b5db1e753f9ce029442e9583bad86f GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#!gXBd2W0r(i2;&+_dsOOyTss+NkE38m5$vK?~t#_q2E)0#?A__ ziU-;vQ4-`A%)lU^VBp{p5Ry>PFk#Aq1E(+CdGPAP?|&1k@;3n09QJf^45_$PJMkt{ zlY)S2yRP|}jy1O*{r{hKZiV2ix#ALh5Jw0NK2xiW_#bP0l+XkK1~!@> literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png new file mode 100644 index 0000000000000000000000000000000000000000..6a7e0e737b97f5bd8a28e84d055a2ee3e95017f9 GIT binary patch literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx7~$g$RhC_mxN>!~0qkkOZm>dY1@fC|c>b9SRBgnjHE)1!!#3 zUyD0HTO>+?{DK)61Qc`)90EcT3K}L%S+MlLg-Z{feE9nN`h{D6foe{8x;TbZ+^U`U znyE>Fr78VRMp2Z4!A3dx^#ad+TX24!CaGM!^jn!7!*AfVn67A01${{6K&R?cg_=lpi2X>OUun@qg2LO*TPynSTxy;rXUqU>iK ROa}Ui!PC{xWt~$(697?7ndkrj literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png new file mode 100644 index 0000000000000000000000000000000000000000..a0c644070588c549167b0da9a0a8533b5248cc35 GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#B)BUGBItQK9Kr~Cmk4AiTIskQ3JLj|3^dK;i-|VSaIumgzhDLi z0R;mGkARSbf`$oG7VJ52;m(5(zZs8RJ_%HD%+tj&q~ccX#J5sS3Oubp&j^@sMxXo} zpK-gR{lc3+jUV61v~y@K)!5}XdF!n0lR_r$%ePEt@MzRfR?|A5Eu6%9bi%|=hVyf_ zAD=YCH8{yIXX%35tzM==v4&GtM>u;)o(T8qk5=FJ+c*R!Dz{*4-UF zNy`Oqd(V64XuIXkh3>^`Bp9YCM=`KHJU3zSfeIHv=AYeJ{vy4Fc7N_`7eBhQc;`LS zKfm@awC&12vfW)Ujn#lxvO(1Q=f{aFZ=O(oDjmL$-|isChFd@%F?hQAxvX0H|LeGegC_U@y&D2 zFsncCcSd1iriRiBqw|kfY?KYS+rigg8avaHb)kcTmcf1&M@L4cnGsy)zt+YJMVK5~ zAuw0`vD|fUya6FH&ue}-dYmzQ&|Ggv5bB`>4Ju9R?WYzN1k22Jc&&is1 zZ*i1N;R5kRZnI`gSB{#YzA62LSAxx=2&RwjTh+O8=a~Kfe#&Fd*B5OzH|lKiGn_y6 t{xB0Unt9VCWya=%QyvNjZ|+Q&u=CO2Yc9~^bO(Bf!PC{xWt~$(6990XkDCAh literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png new file mode 100644 index 0000000000000000000000000000000000000000..5b3f01b10d6a92616515403b8c8e20656f58c42b GIT binary patch literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDUSf35ZBNjDgXcfmy}n5Aj=7^ za5kLbb~psihA@KOB?1|COT2w=MTdM%1{%WiHtZqLOyQCszhDLi0R;nVhk%5Fh6z&^ z9Juh{!|#gbm)n5Shdo^!Ln?07PP{GDtia=PcqzxCrlnW^|G%u}E!)+1SKL5-HBNhZCg0uY2d9ko&HOw irij)gWtn}{_{!KT%xh&GG7ab(1_n=8KbLh*2~7ZYA&s^G literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png new file mode 100644 index 0000000000000000000000000000000000000000..72782ab6a738a7b190593ec722609d79c696e74d GIT binary patch literal 457 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspXHth)a z^3LL93p*ax96ovKEYW)&i{$>7Te6FEcV2VWQ1?%Do}n;b^S#Hp4~qpN7galdYM6P# zed3Ln^UpgTRx|ClIL|BARcO~>`;685$*UtG&l{&2Tx{jE7c z1-BiX-V!Li>u1gGN~xWt{an^LB{Ts5)H1yl literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png new file mode 100644 index 0000000000000000000000000000000000000000..c48b7e5e7dc4f8b198cbe10b6af25d107706aac2 GIT binary patch literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDc1m>5ZBNjDgXcfmy}n*h1?E@ zph>yx3=Ddg2owx_oe=sx1!#iLvzPA^4Aof*aSJutzV>KI zzv`?|(YU^2-=F*Hs~;OJO0CYetdGmR)i$YKCEt0XV!6_P=Xon$Uu&D0xIE_T^B)YX XRXn!~^#2qCJ;UJX>gTe~DWM4f*;a>W literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png new file mode 100644 index 0000000000000000000000000000000000000000..4d50a6d306da9c27d3f3b145dd357bec922f273d GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDdPa25ZBNjDgXcfmy}l_9(~J< zQ3h%kC<*cl22u(J4gm=T4HFg|xbWb^-p?K;K)EfRE{-7;w`wQe7HU!8ao)}PL`mWQ z|E;&pPHR}@#Z8!}*08j{D@3nDX6Bk7%Q8nh6Yir|_A8gKRH<;gTe~DWM4f*{yOt literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png new file mode 100644 index 0000000000000000000000000000000000000000..95f9935432605e11cbe875e8adb6b63bfbbf771a GIT binary patch literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDW?FR5ZBNjDU$LkK&0wp2w|X+ zZihqAM0{^W2fa%K3I=~n0&2Hn+?5P8O}He;FPK3v!*k zgA0Jtn><|{Ln?07PQ1<6tRUc0u6e?h_14P&|5u;gTVG@M*`}c_P$g;_N zyc6eL-1wnm$BTDNELN%vuC>p4N-mykGh9`)h2eKkS%PAH>mH8JJ)1aq8SI4F64vq8 z|395{$8C%0{AmdK II;Vst02hgV;s5{u literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png new file mode 100644 index 0000000000000000000000000000000000000000..8df606d5406dd440388088dd23566d4dd5207a7d GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDZc=p5ZBNjDgXcfmy}mAuC)h} zKxEPD0%0JNZihpVxj=T%yF?)Av?aj*aa_pPAA8D0bsL~h7(8A5T-G@yGywp!2ah)Z literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a4fe1cb26877adc174763146d154416c4a68f3 GIT binary patch literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bd-e) zK(@=yK)48y;dVF#NJ6CG;z92cgFht!xt5dMd~ZdEd`$+LmY3eG0yJE#B*-tAfk8mQ zz`->jB%z>T!juI|4_tWg;rGAu{f!HNDh_$NIEGZ*s-1XS=#T=B%i*(L2cx>&p8mHF zaWZP&68yb&!sOTt#-$D^b9Ucf7qePRAn8$C$I>b3CQgzsJ5-$VkF1as+O)DUx3=cg zYO#||CsJOW&;4c;Tre$Q?uK@cibuICmbT0Pyv3p0XJgP&({|mr-y`29cDiKQtk)LL zXUa}owzx4+=4pacOn-*!oCEU}{Tp9)2(e!3tnxRBef;OsfBTt2(*<>P`*V-qm;SxD u#m}i;WP{|zvJ^q%+@tDh8%_IvF?~J7&vD_<#=k({FnGH9xvXV@SoV+KCsXniP0i*;yo; zq};SC|9|iC{mF6m9Pftu=N2q24Zf#lB<}baHi0Xs&h5yQOP_Y+h|KWlUo+*ao4%yc zrN)Ccw$rD?bvTMIQMy|FZtp!!3$28cZ!{HEZPvI?-!8TFVdQ&MBb@ E0K%b_PXGV_ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png new file mode 100644 index 0000000000000000000000000000000000000000..b04d702333736cb3437d686ca4746b27b9d545a1 GIT binary patch literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}mgFxLi> zPMZUOBm_Bc4RF~R2I9A;hS2&Dx6p$8+@0O|UBaci`Rj=M7B9Bm#B{ted_$hnG9;QIgw{ zpi|H5f-ID!14AO-XRtB}&u4k^@u%XKLr)wPcXa-qdqyswUrUW$>9ibQpsa;W=FQ97 zTIIK1dGR#wO=Ihg=Ot&$I&-dNMp;FyTwHjzID5(21I*r;Aq{MWcNPh5Y%Js7Dl=o7 zM&N=u8P|_|GX19i|NCUs!iYtVpRR2CyLY3VSG;GuJnNs97uugXgr}x`+qm-9k>q_J XIR)QdJ12hz=qUzIS3j3^P6P0|$?Qz>tK3h6z&^95{dB(t}qYe*a_2D%cHFb%sX*l%fx3V zFSQ-!vI{9(c=6^DX5-HDw{Jd{NzC8=b4B99S?i6l(GWrJE0G?j!gW(Efn4uvQ9v^2U1IRZBp}0SOF+oi zWT45PdITkb7D<-``2{mD2q>uN8aM=mBos7En6hB&feV*z-+l1p!|#7(71+)w}dZ@#-=xg_l~`;C6du<1P=3gIHtKXXPmziQu6 zmhw#Ev5h7x(?(rq!C8C_4s{F?zmE&Qc{g>UfcTcUXd&74#WjTmERkQgFf^E3^1Zxq zV%sA9(4dS9v#T-`b5AWT-Qt~cyd-+stguy2t4q_qvpjO$ttOWH#_a$1$rEqr2DaPWsQYuzjDP9LB`2K~fu!5v5D44jL>PqOcP|FW2zVAB^e!>@Q&RB9Bp}!KRwEp_X^Q;|k3ZwzbpPCrT_GJDmr`$*s@sUI zsO!wLHEtKUm%PM5VO6Sz!(t{DBfbOt+u3jYoiU-scjG+`Az8im0`n%TdARy>GgQdqF$) g@;;k$zK9%UUo9Y%Ait>KE6`&Mp00i_>zopr0Dt$LM*si- literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png new file mode 100644 index 0000000000000000000000000000000000000000..8cc83bcf3f0e90228dd17a9fbbb109f0e3026ef0 GIT binary patch literal 451 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsp$aOmu67n@U^m_`>-s!z7*n!SaDGBlmW?&Fd z&^54j2nb0?%qeJ?FlE8g0|!r>y>RK)gC|d)zxwdu_uqdaSW-rRXg#v zP?G_Vi?imU3eB#rMNJ7u{{0u9wN6$EC@Kw_9!r&HAZe?`qxR{-+E47aP3^!zL4E#WSK7QxU>ATTAIJT$|JKG zXH!?SpWb<@d(PtDUMDNv)@?f9$+jkSzyJN56;3A0Hu3jPI5vat+O_1VhUZ^eEENjB z(G#lID&frEdq`sP;s3Fcz0ddETI8?scJkzh&rNq+4zGFHclP`8fDl9Xcu~XfyNgc= ezDhd5l7Cu2GW7FDh7CaPF?hQAxvX~Up-Y?h0P$<&`)ryEY%d%nzD zSLmwV-QTH(90!>=S0;pZOj;4WsBeWx7O$qnEL9_eBjTxEDz_r?|NWo%@W!zfyy;h8 zz7IX(ny7Ks$AD9gtIbB>sqv}KwmbJ_uPn^aGZy-&|36K1V$Qzv-+`WE@O1TaS?83{ F1OVpOth@jK literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png new file mode 100644 index 0000000000000000000000000000000000000000..61674b87aa15b34a956ebfa0d1567a3891a5b618 GIT binary patch literal 425 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4ac-5U&HxE&5rw9Yq~E<5AQ|*7G5Awb@W&(| zTO-IA$O!qG9Qr*4XiH?rt?NKbWlMtmf*BYD6m$(70s=!43d$QMOqsFZz`+w2?ml?+ z;rHKv^SPhT2P(Ye>EaktajSOHL$M|Wo>orQVByP3Tcc*a|3Bev-($m%H~uQ_$luMp z+{%H=Yt7o%G24WuvOij{ zr_EP3-m&l)r@#&4U9H!gTe~DWM4fA=IY0 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png new file mode 100644 index 0000000000000000000000000000000000000000..1fbb754f79f21f9e53bcedfbb1fd1c1af49e9851 GIT binary patch literal 432 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsr&$+5ZBNjDgXcfmy}nrnBWQ| zoi+ymNeFV;83<(9E%Wg_9R?S4I~)RJdtHbClAhrLY=Mmf!&Hla<|?# z-f=s{c;03f`!AE=JPFUhmAVbD#6u4p+N3$faKk@WQ|6zp=4yS%XW0F@udQ8Z?GpX; zqS?Q_k}1D*ZaThx_v_2geb&oOwq`njxg HN@xNAi6*QB literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png new file mode 100644 index 0000000000000000000000000000000000000000..ebe7a5011267837056fa5e2a354e8a5d88504a9a GIT binary patch literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBHIOC zj_drKHU~hsK+@P^ZpeiZ#PwgOc8WX`Cf|Sqo;_O1@pDR&e#N=Qj(^bZr-i@H$%sq-`sx4tHL~;195HfH{hTqo zHlJzMKHrmSN^@6i&0m(uXspP*CFWOJSLUY5^V0WziRPa?w_6tIHwI5vKbLh*2~7Z> C1E`b$ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png new file mode 100644 index 0000000000000000000000000000000000000000..f91a7a1452e06f180d3cd41dbb9852d3813b274f GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFByX zPMZUO46{yWh=|M1Kp?|znUCk$a3Dj$TpJ?pb~psW@H__+_r4MdB>f-8`rV5GG6J5( z2fa%S{*)B_F$u_3w9;`q6cX|^IrMu9(7KPeez^c`S11Yc3ua&tP|(&juy6u|dURD0di#WAGfR_VmsLQMudF3Ex{flInpJW%cW z^Iw0K?+nMTcix$PWw(W*99Ud4cimrqvgHwjQhnO9eeZW4(+~)I9(i^3tQ!JTu5g{Z z`gY-)eTao@l@ zd*{8F_FY6IL;dYE@zxVvno>@RT1$I&&RX1Mx%iL#DZz;IfxQlMUN}7}=)FH(zhL`( z|FTQ(+RbH_y;xAWp>@A`x-qY_rhAj;m2+(~x12fIA@nS4yP3}nx7k%MnECFB**#TH RZv=Xg!PC{xWt~$(697~Tv`GK} literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png new file mode 100644 index 0000000000000000000000000000000000000000..8e798dee8b1186839a99ca4e929fa9b32f33c4d3 GIT binary patch literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk8u}5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1kaQz>~`rV5GvV-0w27gKd zGVGRkhkQ*A{hk7}La4G*0BECZNswPK1A~Bqu7N{9U`RqvL3zW3DGLr9Ja*#3gXgb4 z{QhgU#(Nb|;U!NO$B>F!r4wI^H5u?UziwFA;TJG-LEyK4`kH6&1O|Lw9=(6^o1n;* zE>k0`EpnbLT{-jDIc5$;n_s?8QwkR_U45RS;_Ck3_Uq<1+q^ZTx5P`wK2Klz*i31$ zocn%q!dHtzzCYb8!vNK2P>EdAB4#rtMoAbZ5TjA=(cNZIShGynTOx%WA{;l`PZ)%IT1-M^j5H&df} v;UyI-;r2)#hmUTo1_jdXlD;S1`fV5^N<`eAsInXb`i{ZV)z4*}Q$iB}nS-Zt literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png new file mode 100644 index 0000000000000000000000000000000000000000..36daa7aaabe5fe6b9258a9cee059be4fddb0384b GIT binary patch literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspJ5k5ZBNjDgXcfmy}llBB#v( zK+>$!8Nzkh83BP5UO$q`of~G#7wN2KVJ^nj?(_KO1i_iEsd_VJsNh?WH!))2Q z^QXF3d+&YKdBW11cV5*3?Isx$6>TqFhx>MF8=j{+e$@EtblUD^%f8wbV*acQ79IKr z_oYl)Egw7cu7JAS)`>qK1xu{^6Fes8+!9WxwZ$x4-x@Y0<+y6BJIZ=vi@h z@idL#pY~I{7u;1o-6-MQ?vlLi@I&r1WhJS1A2&|8krUW#bEE0`S)v1>2}Ev(Lm&*#b0Bf=D@?3he85hCxm=W4*i}2wCLMrj#WT=?yJD|#oo-U3d6}L(!UKDFm5O4|CF*X%8HCe0s`2YWnu@0VF zDjy!OH9o`SB4}|mGwQnfjHl;Q`#YjuXPyZ6@G!aLc34nM^;E;YTAw!C_1-s5JYULi zX=n1UojXjvc`)T!Z#bV8=EC*kV(*Q+M>evDCCPuEQqmu>__2#==F&*yd5Fa9u!A@J{aj8qd4m+{O0RD){AGTQM_z z>ch`s9%}zJr>g%EJ{3E$r=!!I^U;au=gE_|N1hkw&=qY`|M$lQ=s5;YS3j3^P6AURzSAr*>E7~x;Gd|0+HL{5D3Hb97x>z3P`>8wJ86Gv3~br zfb5`miT;mbgFht!*=~nIyv|32d`%Aho&vPaOqFpr&~}BAAirP+0R>$HYX^^jz>x5S zoPzR(2~!pvIC$aGgD1~lefa(7ZzH_ju%?AhGcA!TC2BG{ZhmQ)9VK}{eCEY z?el*phRnN0wQpmks<$vK|8i)b=4O>e58U08(xs2faU8LGxALWo;G&WRv%V~?^2~mCTk(V)3l64>JM1#^CAd K=d#Wzp$P!dSgWT1 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png new file mode 100644 index 0000000000000000000000000000000000000000..80991f557d771f79021d8326243ebd2cbca1977f GIT binary patch literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#U`shwc%c{Uu#aNQdWB!S57a0rCqc`n@JL>Q3meFdc6X`{dY!&tw2 zF+g_EyTss6Ng-d8fo$VC2e(5ZKz8W&6rgqGrrEqe+Z9TJ{DK(-6m$)&9XtX;5>j&t z${Qw3S#aRsi3^t=Jb(V`!|%WUUY(f30aSaz)5S5Q;#TRzhfGZhJS~nVFBt`%)>*6Z z`EUG2=>V1+|5nu8%Qc^|TIKYss-ug%Qr1tI^}JH5h`*q-xH-W2RmTgH7q?$8xtaUl zRQlzfCtvp23D&+{Bb}+zu(E_a)=73^iry&CKFl;f^TdNfDO(gF_+(@V>*PCRk4=NYg3buoV~W{0UjzcF~a L`njxgN@xNA=}WPn literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png new file mode 100644 index 0000000000000000000000000000000000000000..a99fefcc2a34e2a834cd0e3e9eaeb21d736c9c69 GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llBB#v( zK+yf=fXWsg!$fyaa`x;b0Z2U z=zRsG(f?tr-@OfAt7IrL%*j0t*)rn_6NE|r6kBN zn1Mk+LD#_A!6P6rBq67uykWwW1qTkEIC1*yg-Z{fzxwdu_uqd$Wea};)!*@SaSW-r zRXQXJI7!pSsU;^-DF9j)xO zhR@bSdnfHV#N~FO&m(Yl(2>XcY`ahY4Qn;s7e3SdK;-7VyHqOXXu5Pvt~($PRj!82l*-$S|&R2>F^E`aK0`omS-9nLyhWOM?7@83Yt`4XhnJ0zwiJ za|((ZCQO;TVCjK_7cO0U@Z|Zc55NEV^ocD7Dn9S&;uunKD|g~sDJKVk*7S>EUSUFl zQn!BZKbqEdOYHf1z4yiEYIH1Eqwa4~UL!I?N^tGg{ie6#v{ z$m-vGjeYO6m%aZy=a}4K2a#=WW*Y@78UA_c_eQhu4Ws{?$9B9|1MRq0#(ArsoqWJa z^sM<)R_`s7U3@Dy2&twOIOtqkvHcs@N3|-R!>!DxO|=BKZdo|Zr7-{YS@kQoznxe$ zjny;h-Qqvit0UK$KU=%Y-Bb4AbdBdif$b8OB8p{yEPvm4k~8@^^A{bl|9VqmL6O1W M>FVdQ&MBb@0GyYs`~Uy| literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png new file mode 100644 index 0000000000000000000000000000000000000000..4fded0ba158a603a1dd2aaf41ac08b43ac9d53b3 GIT binary patch literal 439 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsnP(S5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#W(Y%3tc^K3Yfbln>aB!S57aEQy!ARxnWqd$=JJQwb9A`Hmzy%Pf@ zy{|?2KaBOe2Vw`kOAP*$6!JA0$TqHX05U?qrvPpAY@MzRaz{y!UoeA!g06wJgGWF} zctTD=dBcP$a~CW-aPY*1I}e^bfA!(_-+$2x3ugjVpZ9ce45_$PI`OSolY)SY<4Ly`Kl8~&mWaaUHe4=}WlKj^z6@6bO9x>xx zXW_3Vw(CJna@-adZ9g9af0jc>oN^de36x%wbN$6XSK~sgTe~DWM4f2Z^u~ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png new file mode 100644 index 0000000000000000000000000000000000000000..d979b1b4f9723b2b709532b99d88bcc761013dc1 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDU$LkK;*PJKsVD0 z$grH?3K4PM8sM@s5XiP$<^vG{l5U4XTy_Ql8IBwMeQ!kr8J_3Dfu#49NDz4~%Ku@k z-@OxDNu3ZffKR&(-p zda;#Ww4Z7F?f!mj=S9<-Q-x0ir$wcRtSso; z64>!MswQcEh{dW6_os;O$T-_+_>9y4hQ6?L!y)&**W4%maaQNjsV$K&J(le;-J{`p zR^?BT^5}Is*>(zkXL6_1@6!5SA?z@D=ago}65*#dO0O7{nw1**?9%0D_T+xr!{`$t W!TbE@Q&RB9q>!)4KoPI=5fFCh_Y|NT&Teg8 z2XvBpNswPK1A~BqlCFWZgGWGMNJ36QdBcP$a~CW-aPY*1OAnqrdH(9dhu?qy{R=X> z768<8*VDx@q~ccT#G7JG20Sj#f=5^{DlOX47#;KJ|NZnJFVPE?FEjJgXPG>3RJQNB zRFY&q@44xT>((wRi_$H(ZgKNkEGQ`}V$s^lxcg6y=^LZ+{*a=1Gj=b2+kG=TOjuR! zty9zFA4~$eGmd;%YPxxGr!fDP5TnUfru!G)(x?!cx~IsVvt-M|f3Ixzcel;4XzuvD zK>eM5t6|bT_w+@oNB&>%V(PZr<+)(W!d)o^^%tI$f8>1hul#h^`t09v#R$ zYuI&?H}zq|OqYdkCTJh|(Cl=?YumMSpL1c=_m&Du`|Y~FFc;`i22WQ%mvv4FO#tny B$jATy literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png new file mode 100644 index 0000000000000000000000000000000000000000..38d0ee1af9d11ae541c4170c8f62b44b71e18633 GIT binary patch literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsg?kr5ZBNjDgXcfmy}llBB#v( zK+vtzPF-*Y|nEb zHQrYuLFBb4|A(=D_hNwTfM@YR?-GMQB?W&>3i+B06tP?4?f*Cq$PWFU0(8LZ9Hs3* z_o$Tw`2{mD2q@?pSUY%l281N!6jU@!m@;j_+AUiT9K3Mp!IS5&K79E7_uoI0{S$5h zb=>lFaSW-rRXXveSd#&d%VnlVO;rY_`xU@|oQka zCpEcgJ}OQ6Z#!fy&Q&f@eqLNFwQBM16U(dItup7ae%Z3z@0QK_ez!RiL2^7UpH&T9 z&$O-BR>phLeoJBFPk~s?3AZoQ8a*?sGuyxC?(F-@*}rO4yS;X6)Sk`y^}07@^(T#u wF{izs1Szc$Xy)e<(Eb(jNO4n*aa+ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png new file mode 100644 index 0000000000000000000000000000000000000000..8d745ea5ecc806c4c04388b0435b531e61ae3318 GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llB2^zl zr_BLChUElTAnAQM5=c644RF~R2xQnT^YJ_z4rD-(+u;xh!}DCY$B8iCJ24)o!XP64 z4`coA#Q@m>&*FpLB?f;=3jUZB@--PKVqE9o|2PiF4*i}2v|4?0uqx0cDkVXF!3+!n z3TnCr)(##4fguTrIRy<9rYtyc@WhFe7alx+_2I+szyJQ-oANIXsQ!+pi(^Q|t#+nM^yH%R6Xp2_#s3~Lxy1Lk_EBZBiTe@V)(&^!h5LJ^T@wo~Da`Y~XdhB^ zAgk*gheqlP!^nc<{oVEpe%#l6{p+L2+XbH#Pkl5|-qw3u`eVrNr7E@$eM54)7*_LC mi9}Q%3f*dF_SNj;3iHNx$=jxx6-R*nWbkzLb6Mw<&;$T0lEBme literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png new file mode 100644 index 0000000000000000000000000000000000000000..7e0b2eda3b9c6dbc62f64778e632420629efdd26 GIT binary patch literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mg^)dvK zj_drKHU|J1mJ?ior1#}WqbhqK!+C3f%g#U`1CHDdhXC2Wx1xch=ecm78&N=p_mxQh z$8o-QVt{O)o6+9aqWtcG*a6Sty)H%sy-N)Klob3i2`FM*=MeZhA>?av==T($8)R~q z9|Sr{y(Gvln1Mk+L_t&6z}msXGaxV|p`c;Hlm%N496WpB(v>?89z1#V;lt10fB*IT z%S#7px$o)X7*cVobmDWNCI_CD;tmC~FB=+pxsMgS|6h})tQ>U5{>;a-c~X+eFO==O zrk$Lkz#QmVvpYTQ`5oi=5B|E&OPv}l`PjT?4e|ef1>^r z7b%;t#zV$6jE=>MhJQ13-z?aB(Ao50yN7n^9xJXTaeDDH)7^MeFP&6BpTDS4&25sq z?#UX?-kIUKC8c@ZAMLj|CfsskWxCjQUB!9J#k+s}`YziRy;qv9DZX{ZV%GceRROnS z@_W{Xxz7w|neK5~XyF{AH327TCN0;jH3&8PI>X%2S^Dk0IDx4^uQGVL`njxgN@xNA DL0P}$ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png new file mode 100644 index 0000000000000000000000000000000000000000..9943eeb5dd15a0e0278ed3a1c5fdea45a04b0d92 GIT binary patch literal 481 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}llBKwuT zj_drKHU|LNmJ?ior1#}WAnCj{z-4D3kl}eY97wwE4F-}x}uceyG=pu8}IkV*2R;j zozd!Xk+PY{&}do1=cqVY^l!%bH(I+7I?EnxKhg0#BKFCLJ#+ToQC~1G=w0~po3$p7 zkC;}@ac3^vKSL}^-8n>}c+pk+5{Cpc7k`!{-qrrkG|cw>5j@==HZLqFQfPbZmTKw8 z!Z~~IcQ1{9{w?s(v>aSz&SAQgA<$lok%Gl&4V`H#n$q%4!89ZJ6 KT-G@yGywnhJfLui@9p78gAzzb2zo!5laB{0qEYLk_ zB|(0{3=9GaY8tu*)(!z7aS1sE4HKp;SbE^#*$bDhJb3c_#miS8KK%asPpRa=b)b$r zo-U3d6}L(!zT|5%5NMgcC?v$i=A+xesxN=FZI}>fc|9gboFyt=akR{09E_P>Hq)$ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png new file mode 100644 index 0000000000000000000000000000000000000000..b6dcbdb4327f3e255b5b9b0ba482e8b02e019e39 GIT binary patch literal 468 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsm1`G5ZBNjDgXcfmy}llBKwuT zPMZTDY?B5@Aj4&6AdvJt9j0Ke4dU(!g2=iZ4uLQ{&xL!O2=l!Y<8vblDB^u3637NJ z{2#{p-2*WKp2P*cOAP*$6#Ov>$W^q`@qZi#WQTlB4*i}2wEbpx(q5orR7!&Uf*BYD z6f|@V9DD;p64EmYiW??OS#aRsi4$ipT)Ok%`Ku3~e*gW)Rh<+FRDaLY#WAGfR_Vl> z)0!LvoV&vgQ;4OiN>@=+Qgr-@S@8yKp%(GhXCCe|{T9Lb^noYmkH|yfMSeXxPkpVE z>wB8rtD5+2*Ty{jrBM^%TYcLuTk=KD@4cVYoVO$-$7I^qn9!NTG4RF~R2xP#K+u;x(+xJ#9kn}tk?t3Q&$nd@r2_$`PMtfh2 z^1BBT33wLoempejU1IR3q~MQ9KoN}~W1y~(ugRg`Q-JpFZ;-MDIzzc6$S;_IK|n!U z*TCArGaxV|p`c;Hlm%N496WL1(t`(2UVZrREaktajSIV>uF5} zJS~@%I=G_Nzo?45Tk-O@{ldu;o0(q!QG9A`>>E^g?xgq8nwifzz0)Vn5^1mQ2@EZ7 z+~WAsF35}3U&GB!ylMaEEcdWfr;ZKm|L$*J=}?#TWd?_-!THVxgBj;H|6`0i-R`wv zrl{~|5ASE2PcXmpynW*Fz6SkgH@)10&$NG=VE$%NOK0&JWm`qV>ApSB?V?uprT*oe zDpv7wF;`=b@>`+vX8X*~Pptp@Us-(nYQ0+-2lrGZrmdKI;Vst04D>&TmS$7 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png new file mode 100644 index 0000000000000000000000000000000000000000..9d74037d9a09ba96305d458a83fe4a5d59e4b8b9 GIT binary patch literal 473 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mAuC)h} zjvM`*HU|J1mJ?ioBoMjm476S74P` zuS9~#Yf=6WWBu;M0NDZ0;sc(<1-(lQ{*)B_F$pN5Xr-f@X$545d`%Aho&t2k_h~I_ zflg8@3GxeOU=Wa2(AG7ub_fWGOGqy$ZkRA-!R7-8Pn^AQ>A{oduReVE^!x9>1?RqL z0Cn8bNJ5jIaXm@8a&pZ;;r{$KyB$UGP^Vv6r-RBchZb*Kgc<`qElv8%yT#^++ zOoy2E37+3LY3HBJ)o+eiR4iH8ZS>_z(aubz|BFq(uKw(D>5}8~)t~m5Nf{env3cpC z?va}Gc3F4cxwk3z+P`s}+4Z8&;fg`-qs4CZKj%&K?mKfNR5*4fd-U(h>t1~}g_?!F zkDl+2;+nQ*HBXkvjqsf!3cnIo<=zyqKQ8{DUi#<$!sbgrZ!&ng`njxgN@xNAN*c+I literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png new file mode 100644 index 0000000000000000000000000000000000000000..4b8abe16349c9c41a2ef0dcf5521760daad1c1cb GIT binary patch literal 458 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspEQ;90Fu}oCpIcI2R6N_}qvBvVHHw z07?IcvDULZ{O*C+0Z-zB-X#WqN&>QVGp$0tCWn4c0oohn^|l%443&}~zhDLi0R;_h zT?2=Jkc9M{g7St56Q?X#d*I-Svls3>c>dzmhflx%x*yVB15|&@)5S5Q;#TRzmqJYr zJk75canEbeh|`eMe)HRYVWxzu!)Nwo_f;n6r7jT)6>yC-6Yg?mI&=Au<@3Gky)N+^ ztz2vqdW&tPx{Z*?&%H|3M_wi02z{=tJmIePT5(>ZSDvB)%R=tY_|JJFX5SWOYhyp# zOA~)>Q9mWRtUIT7){mt5Q@@lPs`HJz;BKtuc4Bhl21m8v=bsdJRqvnr)4t@wfyV;2 z0w<)F`Rioq)K9;^_|N;PzDK9d-0^ae^z}u#ANH8+?Vq$ICHAq#Hp{e*;5J>Z`TM$D h1#1nqdg%$X>QCG&<@>TsUkB(*22WQ%mvv4FO#pHHxKIE9 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png new file mode 100644 index 0000000000000000000000000000000000000000..b9896bf982f8de94c18cf4b67b7137f3362dd544 GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png new file mode 100644 index 0000000000000000000000000000000000000000..b9896bf982f8de94c18cf4b67b7137f3362dd544 GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png new file mode 100644 index 0000000000000000000000000000000000000000..d0777a130427a4498121e2ce215c748b927ea096 GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspZhelFbOL>NTe`&yL$ z!&tw2F+gs>v-qHQiNT+ef@8= z$K|q82Y1I&)&P->S^xiY&uys^33%Q*r8;fak|k3X`FYMeUU*=t(B6O``#HteZk;j^ zRdKoQm*VQiEn>2tBV%@~>YAPoR-@dH{{vf&{OftCsc^ByHl2})_xF}7?h|ZlHCUsI zH?pi;ss61nrMLQ!-K8DpA9Po8NogFeanXL_+paO;h|K2;9v(fJuVaketh2?p{BI57 z+tU|#vXS%F4BpOXX8%n6+kdaU6z@Jw(yVsNM4SB^OLoLvzUjR`bA9P|jjq=(QgXkz m3C(tE+9MfdkWBYH{(<>pkq`^g8YIR z7z7kFbPcQ>JUjzJ64DDACQMndW$W$(2QOTD@Z|Zc4RFn7=T_Kedu zi?7;G$#{_4!Nqw*VzQl~@5a}U=lzqByXyUUvys)4*eS~EHx^#nA9wu*_nj~9r@sZB r2s^XQb=uBN6{$b2T)A~cm?b~pu#}lm@)LESM;Sa_{an^LB{Ts5qrgFht&e@qJbnhX?iI}{T5IswQI{hk7}U9(d65705H zB|(0{3=9Ga8oCD74jus^3F$cncfZMfB*d}75dN))bPmD z#WAGfR_Vmt;wA%umi`3IwHi`cxq*&rHqQG0pLzH0_zBbXjGw5TfAdUlBa2aHWN78Q zRX!U3w%)&!+@sjCSY1@8(05AHler5nJX88``E7qvsT#|ss)YZA6~@23FPWKUI-dX8 zx^%(v^DFHdYi>^F+OW2YIWtG$mqh2YcX=H$=RQXmp5NXZl4Q5p^i$k%jnJgl${Zz? zXWs;q&0T*B+e7Sh#_6yHR_Gz1>9OEzknF#bPgQu&X%Q~loCID}e B%s2o5 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/skin.xml b/res/skins/ShadeDark1024x600-Netbook/skin.xml new file mode 100644 index 00000000000..c8ee92ab3d2 --- /dev/null +++ b/res/skins/ShadeDark1024x600-Netbook/skin.xml @@ -0,0 +1,6057 @@ + + + + + + + + + + + + background1024x600.png + #000000 + + + + + 5,266 + + 1014,285 + vertical + + + + + + 1014,100 + horizontal + + + + + 0,0 + 254,100 + + + + + Track title Displays the artist and title of the loaded track. Informations are extracted from the tracks tags. + + [Sampler1] + 2,3 + 165,21 + + + + + + Tempo Displays the tempo of the loaded track in BPM (beats per minute) + + [Sampler1] + 164,3 + 55,21 + + right + + [Sampler1],visual_bpm + + + + + + + + Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM + + 1 + + 0 + btn_tap_sampler_over.png + btn_tap_sampler.png + + 175,5 + + [Sampler1],bpm_tap + true + + + + + + Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. + + [Sampler1] + 5,25 + 170,35 + #E4E4E4 + #00FF00 + #E4E4E4 + 80 + + [Sampler1],playposition + false + + + + + + Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 184,66 + + [Sampler1],pregain + + + + + + Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). + + btn_clipping_sampler_over.png + btn_clipping_sampler.png + 219,5 + + [Sampler1],PeakIndicator + + + + + + Channel volume meter Shows the current channel volume + + btn_volume_display_sampler_over.png + btn_volume_display_sampler.png + 219,24 + false + 5 + 500 + 50 + 2 + + [Sampler1],VuMeter + + + + + + Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value + + knob_pitch_sampler.png + slider_pitch_sampler.png + 228,8 + false + + [Sampler1],rate + false + + + + + + Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Jumps to the beginning of the track. + + 2 + + 0 + btn_play_sampler.png + btn_play_sampler.png + + + 1 + btn_play_sampler_over.png + btn_play_sampler_over.png + + 11,68 + + [Sampler1],play + true + LeftButton + + + [Sampler1],start + true + RightButton + + + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_1_over.png + btn_hotcue1_1.png + + + 1 + btn_hotcue1_1.png + btn_hotcue1_1_over.png + + 47,68 + + [Sampler1],hotcue_1_activate + true + LeftButton + false + + + [Sampler1],hotcue_1_clear + true + RightButton + false + + + [Sampler1],hotcue_1_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_2_over.png + btn_hotcue1_2.png + + + 1 + btn_hotcue1_2.png + btn_hotcue1_2_over.png + + 68,68 + + [Sampler1],hotcue_2_activate + true + LeftButton + false + + + [Sampler1],hotcue_2_clear + true + RightButton + false + + + [Sampler1],hotcue_2_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_3_over.png + btn_hotcue1_3.png + + + 1 + btn_hotcue1_3.png + btn_hotcue1_3_over.png + + 89,68 + + [Sampler1],hotcue_3_activate + true + LeftButton + false + + + [Sampler1],hotcue_3_clear + true + RightButton + false + + + [Sampler1],hotcue_3_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_4_over.png + btn_hotcue1_4.png + + + 1 + btn_hotcue1_4.png + btn_hotcue1_4_over.png + + 110,68 + + [Sampler1],hotcue_4_activate + true + LeftButton + false + + + [Sampler1],hotcue_4_clear + true + RightButton + false + + + [Sampler1],hotcue_4_enabled + false + + + + + + Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. + + 2 + + 0 + btn_pfl_sampler.png + btn_pfl_sampler.png + + + 1 + btn_pfl_sampler_over.png + btn_pfl_sampler_over.png + + 146,68 + + [Sampler1],pfl + + + + + + Eject Eject currently loaded track from deck. + + 1 + + 0 + btn_eject_sampler_over.png + btn_eject_sampler.png + + 198,23 + + [Sampler1],eject + true + LeftButton + false + + + + + + Repeat When active the track will repeat if you go past the end or reverse before the start. + + 2 + + 0 + btn_repeat_sampler.png + btn_repeat_sampler.png + + + 1 + btn_repeat_sampler_over.png + btn_repeat_sampler_over.png + + 176,23 + + [Sampler1],repeat + + + + + + Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) + + 3 + + 0 + btn_orientation_sampler_left_over.png + btn_orientation_sampler_left_over.png + + + 1 + btn_orientation_sampler_master.png + btn_orientation_sampler_master.png + + + 2 + btn_orientation_sampler_right_over.png + btn_orientation_sampler_right_over.png + + 176,41 + + [Sampler1],orientation + true + LeftButton + + + + + + Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. + + 2 + + 0 + btn_keylock_sampler.png + btn_keylock_sampler.png + + + 1 + btn_keylock_sampler_over.png + btn_keylock_sampler_over.png + + 197,41 + + [Sampler1],keylock + + + + + + 0,0 + 254,100 + + + + + Track title Displays the artist and title of the loaded track. Informations are extracted from the tracks tags. + + [Sampler2] + 2,3 + 165,21 + + + + + + Tempo Displays the tempo of the loaded track in BPM (beats per minute) + + [Sampler2] + 164,3 + 55,21 + + right + + [Sampler2],visual_bpm + + + + + + + + Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM + + 1 + + 0 + btn_tap_sampler_over.png + btn_tap_sampler.png + + 175,5 + + [Sampler2],bpm_tap + true + + + + + + Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. + + [Sampler2] + 5,25 + 170,35 + #E4E4E4 + #00FF00 + #E4E4E4 + 80 + + [Sampler2],playposition + false + + + + + + Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 184,66 + + [Sampler2],pregain + + + + + + Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). + + btn_clipping_sampler_over.png + btn_clipping_sampler.png + 219,5 + + [Sampler2],PeakIndicator + + + + + + Channel volume meter Shows the current channel volume + + btn_volume_display_sampler_over.png + btn_volume_display_sampler.png + 219,24 + false + 5 + 500 + 50 + 2 + + [Sampler2],VuMeter + + + + + + Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value + + knob_pitch_sampler.png + slider_pitch_sampler.png + 228,8 + false + + [Sampler2],rate + false + + + + + + Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Jumps to the beginning of the track. + + 2 + + 0 + btn_play_sampler.png + btn_play_sampler.png + + + 1 + btn_play_sampler_over.png + btn_play_sampler_over.png + + 11,68 + + [Sampler2],play + true + LeftButton + + + [Sampler2],start + true + RightButton + + + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_1_over.png + btn_hotcue1_1.png + + + 1 + btn_hotcue1_1.png + btn_hotcue1_1_over.png + + 47,68 + + [Sampler2],hotcue_1_activate + true + LeftButton + false + + + [Sampler2],hotcue_1_clear + true + RightButton + false + + + [Sampler2],hotcue_1_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_2_over.png + btn_hotcue1_2.png + + + 1 + btn_hotcue1_2.png + btn_hotcue1_2_over.png + + 68,68 + + [Sampler2],hotcue_2_activate + true + LeftButton + false + + + [Sampler2],hotcue_2_clear + true + RightButton + false + + + [Sampler2],hotcue_2_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_3_over.png + btn_hotcue1_3.png + + + 1 + btn_hotcue1_3.png + btn_hotcue1_3_over.png + + 89,68 + + [Sampler2],hotcue_3_activate + true + LeftButton + false + + + [Sampler2],hotcue_3_clear + true + RightButton + false + + + [Sampler2],hotcue_3_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_4_over.png + btn_hotcue1_4.png + + + 1 + btn_hotcue1_4.png + btn_hotcue1_4_over.png + + 110,68 + + [Sampler2],hotcue_4_activate + true + LeftButton + false + + + [Sampler2],hotcue_4_clear + true + RightButton + false + + + [Sampler2],hotcue_4_enabled + false + + + + + + Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. + + 2 + + 0 + btn_pfl_sampler.png + btn_pfl_sampler.png + + + 1 + btn_pfl_sampler_over.png + btn_pfl_sampler_over.png + + 146,68 + + [Sampler2],pfl + + + + + + Eject Eject currently loaded track from deck. + + 1 + + 0 + btn_eject_sampler_over.png + btn_eject_sampler.png + + 198,23 + + [Sampler2],eject + true + LeftButton + false + + + + + + Repeat When active the track will repeat if you go past the end or reverse before the start. + + 2 + + 0 + btn_repeat_sampler.png + btn_repeat_sampler.png + + + 1 + btn_repeat_sampler_over.png + btn_repeat_sampler_over.png + + 176,23 + + [Sampler2],repeat + + + + + + Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) + + 3 + + 0 + btn_orientation_sampler_left_over.png + btn_orientation_sampler_left_over.png + + + 1 + btn_orientation_sampler_master.png + btn_orientation_sampler_master.png + + + 2 + btn_orientation_sampler_right_over.png + btn_orientation_sampler_right_over.png + + 176,41 + + [Sampler2],orientation + true + LeftButton + + + + + + Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. + + 2 + + 0 + btn_keylock_sampler.png + btn_keylock_sampler.png + + + 1 + btn_keylock_sampler_over.png + btn_keylock_sampler_over.png + + 197,41 + + [Sampler2],keylock + + + + + + 0,0 + 254,100 + + + + + Track title Displays the artist and title of the loaded track. Informations are extracted from the tracks tags. + + [Sampler3] + 2,3 + 165,21 + + + + + + Tempo Displays the tempo of the loaded track in BPM (beats per minute) + + [Sampler3] + 164,3 + 55,21 + + right + + [Sampler3],visual_bpm + + + + + + + + Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM + + 1 + + 0 + btn_tap_sampler_over.png + btn_tap_sampler.png + + 175,5 + + [Sampler3],bpm_tap + true + + + + + + Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. + + [Sampler3] + 5,25 + 170,35 + #E4E4E4 + #00FF00 + #E4E4E4 + 80 + + [Sampler3],playposition + false + + + + + + Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 184,66 + + [Sampler3],pregain + + + + + + Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). + + btn_clipping_sampler_over.png + btn_clipping_sampler.png + 219,5 + + [Sampler3],PeakIndicator + + + + + + Channel volume meter Shows the current channel volume + + btn_volume_display_sampler_over.png + btn_volume_display_sampler.png + 219,24 + false + 5 + 500 + 50 + 2 + + [Sampler3],VuMeter + + + + + + Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value + + knob_pitch_sampler.png + slider_pitch_sampler.png + 228,8 + false + + [Sampler3],rate + false + + + + + + Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Jumps to the beginning of the track. + + 2 + + 0 + btn_play_sampler.png + btn_play_sampler.png + + + 1 + btn_play_sampler_over.png + btn_play_sampler_over.png + + 11,68 + + [Sampler3],play + true + LeftButton + + + [Sampler3],start + true + RightButton + + + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_1_over.png + btn_hotcue1_1.png + + + 1 + btn_hotcue1_1.png + btn_hotcue1_1_over.png + + 47,68 + + [Sampler3],hotcue_1_activate + true + LeftButton + false + + + [Sampler3],hotcue_1_clear + true + RightButton + false + + + [Sampler3],hotcue_1_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_2_over.png + btn_hotcue1_2.png + + + 1 + btn_hotcue1_2.png + btn_hotcue1_2_over.png + + 68,68 + + [Sampler3],hotcue_2_activate + true + LeftButton + false + + + [Sampler3],hotcue_2_clear + true + RightButton + false + + + [Sampler3],hotcue_2_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_3_over.png + btn_hotcue1_3.png + + + 1 + btn_hotcue1_3.png + btn_hotcue1_3_over.png + + 89,68 + + [Sampler3],hotcue_3_activate + true + LeftButton + false + + + [Sampler3],hotcue_3_clear + true + RightButton + false + + + [Sampler3],hotcue_3_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_4_over.png + btn_hotcue1_4.png + + + 1 + btn_hotcue1_4.png + btn_hotcue1_4_over.png + + 110,68 + + [Sampler3],hotcue_4_activate + true + LeftButton + false + + + [Sampler3],hotcue_4_clear + true + RightButton + false + + + [Sampler3],hotcue_4_enabled + false + + + + + + Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. + + 2 + + 0 + btn_pfl_sampler.png + btn_pfl_sampler.png + + + 1 + btn_pfl_sampler_over.png + btn_pfl_sampler_over.png + + 146,68 + + [Sampler3],pfl + + + + + + Eject Eject currently loaded track from deck. + + 1 + + 0 + btn_eject_sampler_over.png + btn_eject_sampler.png + + 198,23 + + [Sampler3],eject + true + LeftButton + false + + + + + + Repeat When active the track will repeat if you go past the end or reverse before the start. + + 2 + + 0 + btn_repeat_sampler.png + btn_repeat_sampler.png + + + 1 + btn_repeat_sampler_over.png + btn_repeat_sampler_over.png + + 176,23 + + [Sampler3],repeat + + + + + + Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) + + 3 + + 0 + btn_orientation_sampler_left_over.png + btn_orientation_sampler_left_over.png + + + 1 + btn_orientation_sampler_master.png + btn_orientation_sampler_master.png + + + 2 + btn_orientation_sampler_right_over.png + btn_orientation_sampler_right_over.png + + 176,41 + + [Sampler3],orientation + true + LeftButton + + + + + + Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. + + 2 + + 0 + btn_keylock_sampler.png + btn_keylock_sampler.png + + + 1 + btn_keylock_sampler_over.png + btn_keylock_sampler_over.png + + 197,41 + + [Sampler3],keylock + + + + + + 0,0 + 252,100 + + + + + Track title Displays the artist and title of the loaded track. Informations are extracted from the tracks tags. + + [Sampler4] + 2,3 + 165,21 + + + + + + Tempo Displays the tempo of the loaded track in BPM (beats per minute) + + [Sampler4] + 164,3 + 55,21 + + right + + [Sampler4],visual_bpm + + + + + + + + Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM + + 1 + + 0 + btn_tap_sampler_over.png + btn_tap_sampler.png + + 175,5 + + [Sampler4],bpm_tap + true + + + + + + Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. + + [Sampler4] + 5,25 + 170,35 + #E4E4E4 + #00FF00 + #E4E4E4 + 80 + + [Sampler4],playposition + false + + + + + + Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 184,66 + + [Sampler4],pregain + + + + + + Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). + + btn_clipping_sampler_over.png + btn_clipping_sampler.png + 219,5 + + [Sampler4],PeakIndicator + + + + + + Channel volume meter Shows the current channel volume + + btn_volume_display_sampler_over.png + btn_volume_display_sampler.png + 219,24 + false + 5 + 500 + 50 + 2 + + [Sampler4],VuMeter + + + + + + Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value + + knob_pitch_sampler.png + slider_pitch_sampler.png + 228,8 + false + + [Sampler4],rate + false + + + + + + Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Jumps to the beginning of the track. + + 2 + + 0 + btn_play_sampler.png + btn_play_sampler.png + + + 1 + btn_play_sampler_over.png + btn_play_sampler_over.png + + 11,68 + + [Sampler4],play + true + LeftButton + + + [Sampler4],start + true + RightButton + + + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_1_over.png + btn_hotcue1_1.png + + + 1 + btn_hotcue1_1.png + btn_hotcue1_1_over.png + + 47,68 + + [Sampler4],hotcue_1_activate + true + LeftButton + false + + + [Sampler4],hotcue_1_clear + true + RightButton + false + + + [Sampler4],hotcue_1_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_2_over.png + btn_hotcue1_2.png + + + 1 + btn_hotcue1_2.png + btn_hotcue1_2_over.png + + 68,68 + + [Sampler4],hotcue_2_activate + true + LeftButton + false + + + [Sampler4],hotcue_2_clear + true + RightButton + false + + + [Sampler4],hotcue_2_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_3_over.png + btn_hotcue1_3.png + + + 1 + btn_hotcue1_3.png + btn_hotcue1_3_over.png + + 89,68 + + [Sampler4],hotcue_3_activate + true + LeftButton + false + + + [Sampler4],hotcue_3_clear + true + RightButton + false + + + [Sampler4],hotcue_3_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_4_over.png + btn_hotcue1_4.png + + + 1 + btn_hotcue1_4.png + btn_hotcue1_4_over.png + + 110,68 + + [Sampler4],hotcue_4_activate + true + LeftButton + false + + + [Sampler4],hotcue_4_clear + true + RightButton + false + + + [Sampler4],hotcue_4_enabled + false + + + + + + Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. + + 2 + + 0 + btn_pfl_sampler.png + btn_pfl_sampler.png + + + 1 + btn_pfl_sampler_over.png + btn_pfl_sampler_over.png + + 146,68 + + [Sampler4],pfl + + + + + + Eject Eject currently loaded track from deck. + + 1 + + 0 + btn_eject_sampler_over.png + btn_eject_sampler.png + + 198,23 + + [Sampler4],eject + true + LeftButton + false + + + + + + Repeat When active the track will repeat if you go past the end or reverse before the start. + + 2 + + 0 + btn_repeat_sampler.png + btn_repeat_sampler.png + + + 1 + btn_repeat_sampler_over.png + btn_repeat_sampler_over.png + + 176,23 + + [Sampler4],repeat + + + + + + Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) + + 3 + + 0 + btn_orientation_sampler_left_over.png + btn_orientation_sampler_left_over.png + + + 1 + btn_orientation_sampler_master.png + btn_orientation_sampler_master.png + + + 2 + btn_orientation_sampler_right_over.png + btn_orientation_sampler_right_over.png + + 176,41 + + [Sampler4],orientation + true + LeftButton + + + + + + Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. + + 2 + + 0 + btn_keylock_sampler.png + btn_keylock_sampler.png + + + 1 + btn_keylock_sampler_over.png + btn_keylock_sampler_over.png + + 197,41 + + [Sampler4],keylock + + + + + + + [Samplers],show_samplers + visible + + + + + + 0,0 + 1012,i + horizontal + + + + + + + + + + + + + + + 384,6 + 255,255 + + + + + + 0,0 + 250,250 + + + + + Microphone Show/hide the microphone widget. + + 2 + + 0 + tab_microphone.png + tab_microphone.png + + + 1 + tab_microphone_over.png + tab_microphone_over.png + + 68,77 + + [Microphone],show_microphone + + + + + + Sampler Show/hide the Sampler widget. + + 2 + + 0 + tab_sampler.png + tab_sampler.png + + + 1 + tab_sampler_over.png + tab_sampler_over.png + + 105,77 + + [Samplers],show_samplers + + + + + + + Vinyl Control Show/hide the Vinyl Control widget. Activate Vinyl Control from the Menu→Options + + 2 + + 0 + tab_vinylcontrol.png + tab_vinylcontrol.png + + + 1 + tab_vinylcontrol_over.png + tab_vinylcontrol_over.png + + 160,77 + + [Vinylcontrol],show_vinylcontrol + + + + + + Volume control Adjust the volume of the selected channel. Right-click: Reset to default value + + knob_volume1.png + slider_volume1.png + 72,91 + false + + [Channel1],volume + false + + + + Volume control Adjust the volume of the selected channel. Right-click: Reset to default value + + knob_volume2.png + slider_volume2.png + 168,91 + false + + [Channel2],volume + false + + + + + + Crossfader Fade between the channels and define what you hear through the master output. Right-click: Reset to default value Change the crossfader curve in Preferences→Crossfader + + knob_crossfader.png + slider_crossfader.png + 72,230 + true + + [Master],crossfader + false + + + + + + Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Places a Cue-point at the current position on the waveform. + + 2 + true + + 0 + btn_play1.png + btn_play1.png + + + 1 + btn_play1_over.png + btn_play1_over.png + + 11,226 + + [Channel1],play + true + LeftButton + + + [Channel1],cue_set + true + RightButton + false + + + + Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Places a Cue-point at the current position on the waveform. + + 2 + true + + 0 + btn_play2.png + btn_play2.png + + + 1 + btn_play2_over.png + btn_play2_over.png + + 203,226 + + [Channel2],play + true + LeftButton + + + [Channel2],cue_set + true + RightButton + false + + + + + + Cue Left-click (while playing): The track will seek to the cue-point and stop (=CDJ) OR play (=simple). Change the default cue behaviour in Preferences→Interface. Left-click (while stopped): Places a cue-point at the current position on the waveform. Right-click: The track will seek to the cue-point and stop. + + 1 + + 0 + btn_cue1_over.png + btn_cue1.png + + 11,205 + + [Channel1],cue_default + true + LeftButton + + + [Channel1],cue_gotoandstop + true + RightButton + + + + Cue Left-click (while playing): The track will seek to the cue-point and stop (=CDJ) OR play (=simple). Change the default cue behaviour in Preferences→Interface. Left-click (while stopped): Places a cue-point at the current position on the waveform. Right-click: The track will seek to the cue-point and stop. + + 1 + + 0 + btn_cue2_over.png + btn_cue2.png + + 203,205 + + [Channel2],cue_default + true + LeftButton + + + [Channel2],cue_gotoandstop + true + RightButton + + + + + + Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. + + 2 + + 0 + btn_pfl1.png + btn_pfl1.png + + + 1 + btn_pfl1_over.png + btn_pfl1_over.png + + 64,197 + + [Channel1],pfl + + + + Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. + + 2 + + 0 + btn_pfl2.png + btn_pfl2.png + + + 1 + btn_pfl2_over.png + btn_pfl2_over.png + + 160,197 + + [Channel2],pfl + + + + + + Flanger Toggles the flange effect. Use the depth/delay/lfo knobs to adjust + + 2 + + 0 + btn_fx1.png + btn_fx1.png + + + 1 + btn_fx1_over.png + btn_fx1_over.png + + 11,49 + + [Channel1],flanger + + + + Flanger Toggles the flange effect. Use the depth/delay/lfo knobs to adjust + + 2 + + 0 + btn_fx2.png + btn_fx1.png + + + 1 + btn_fx2_over.png + btn_fx2_over.png + + 214,49 + + [Channel2],flanger + + + + + + High EQ kill Holds the gain of the high EQ to zero while active. + + 2 + + 0 + btn_kill_over.png + btn_kill.png + + + 1 + btn_kill.png + btn_kill_over.png + + 11,120 + + [Channel1],filterHighKill + true + LeftButton + + + + Mid EQ kill Holds the gain of the mid EQ to zero while active. + + 2 + + 0 + btn_kill_over.png + btn_kill.png + + + 1 + btn_kill.png + btn_kill_over.png + + 11,149 + + [Channel1],filterMidKill + true + LeftButton + + + + Low EQ kill Holds the gain of the low EQ to zero while active. + + 2 + + 0 + btn_kill_over.png + btn_kill.png + + + 1 + btn_kill.png + btn_kill_over.png + + 11,178 + + [Channel1],filterLowKill + true + LeftButton + + + + + High EQ kill Holds the gain of the high EQ to zero while active. + + 2 + + 0 + btn_kill_over.png + btn_kill.png + + + 1 + btn_kill.png + btn_kill_over.png + + 229,120 + + [Channel2],filterHighKill + true + LeftButton + + + + Mid EQ kill Holds the gain of the mid EQ to zero while active. + + 2 + + 0 + btn_kill_over.png + btn_kill.png + + + 1 + btn_kill.png + btn_kill_over.png + + 229,149 + + [Channel2],filterMidKill + true + LeftButton + + + + Low EQ kill Holds the gain of the low EQ to zero while active. + + 2 + + 0 + btn_kill_over.png + btn_kill.png + + + 1 + btn_kill.png + btn_kill_over.png + + 229,178 + + [Channel2],filterLowKill + true + LeftButton + + + + + + Master volume Adjusts the Master output volume. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 208,15 + + [Master],volume + + + + Balance Adjusts the left/right channel balance on the Master output. Right-click: Reset to default value + + 63 + knobs/knob_rotary_s%1.png + 145,15 + + [Master],balance + + + + + + Headphone volume Adjusts the headphone output volume. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 20,15 + + [Master],headVolume + + + + Headphone mix Controls what you hear on the headphone output. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 82,15 + + [Master],headMix + + + + + + Flanger delay Adjusts the phase delay of the flange effect (when active). Right-click: Reset to default value + + 63 + knobs/knob_rotary_s%1.png + 171,51 + + [Flanger],lfoDelay + + + + Flanger depth Adjusts the intensity of the flange effect (when active). Right-click: Reset to default value + + 63 + knobs/knob_rotary_s%1.png + 114,51 + + [Flanger],lfoDepth + + + + Flanger LFO period Adjusts the wavelength of the flange effect (when active). Right-click: Reset to default value + + 63 + knobs/knob_rotary_s%1.png + 57,51 + + [Flanger],lfoPeriod + + + + + + Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 32,83 + + [Channel1],pregain + + + + High EQ Adjusts the gain of the high EQ filter. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 32,112 + + [Channel1],filterHigh + + + + Mid EQ Adjusts the gain of the mid EQ filter. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 32,141 + + [Channel1],filterMid + + + + Low EQ Adjusts the gain of the low EQ filter. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 32,170 + + [Channel1],filterLow + + + + + Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 196,83 + + [Channel2],pregain + + + + High EQ Adjusts the gain of the high EQ filter. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 196,112 + + [Channel2],filterHigh + + + + Mid EQ Adjusts the gain of the mid EQ filter. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 196,141 + + [Channel2],filterMid + + + + Low EQ Adjusts the gain of the low EQ filter. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 196,170 + + [Channel2],filterLow + + + + + + Channel volume meter Shows the current channel volume + + btn_volume_display1_over.png + btn_volume_display1.png + 107,112 + false + 5 + 500 + 50 + 2 + + [Channel1],VuMeter + + + + Channel volume meter Shows the current channel volume + + btn_volume_display2_over.png + btn_volume_display2.png + 143,112 + false + 5 + 500 + 50 + 2 + + [Channel2],VuMeter + + + + + Master channel volume meter Outputs the current instantaneous master volume for the left channel. + + btn_volume_display_master1_over.png + btn_volume_display_master1.png + 122,112 + 5 + 500 + 50 + 2 + + [Master],VuMeterL + + + + Master channel volume meter Outputs the current instantaneous master volume for the right channel. + + btn_volume_display_master2_over.png + btn_volume_display_master2.png + 128,112 + 5 + 500 + 50 + 2 + + [Master],VuMeterR + + + + + + Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). + + btn_clipping1_over.png + btn_clipping1.png + 107,93 + + [Channel1],PeakIndicator + + + + Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). + + btn_clipping2_over.png + btn_clipping2.png + 143,93 + + [Channel2],PeakIndicator + + + + Master Peak Indicator Indicates when the signal on the Master output is clipping, (too loud for the hardware and is being distorted). + + btn_clipping_master_over.png + btn_clipping_master.png + 122,93 + + [Master],PeakIndicator + + + + + + + + + + + 8,8 + 370,250 + + + + + 370,250 + + + + + Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected + + 3 + btn_vinylcontrol_indicator_horizontal1.png + btn_vinylcontrol_indicator_horizontal2.png + btn_vinylcontrol_indicator_horizontal3.png + -1,0 + + [Channel1],vinylcontrol_status + + + + Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected + + 3 + btn_vinylcontrol_indicator_horizontal1.png + btn_vinylcontrol_indicator_horizontal2.png + btn_vinylcontrol_indicator_horizontal3.png + -1,248 + + [Channel1],vinylcontrol_status + + + + Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected + + 3 + btn_vinylcontrol_indicator_vertical1.png + btn_vinylcontrol_indicator_vertical2.png + btn_vinylcontrol_indicator_vertical3.png + 0,0 + + [Channel1],vinylcontrol_status + + + + Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected + + 3 + btn_vinylcontrol_indicator_vertical1.png + btn_vinylcontrol_indicator_vertical2.png + btn_vinylcontrol_indicator_vertical3.png + 367,0 + + [Channel1],vinylcontrol_status + + + + + + 2,43 + 318,118 + horizontal + + + + + + + Waveform display Shows the loaded tracks' waveforms near the playback position. Left-click: Use the mouse to scratch, halt, spin back and push forward a track. Right-click: Drag with mouse to make temporary pitch adjustments. Drop tracks from library or external file manager here. + + 1 + 0,0 + + + #3F4249 + + #E4E4E4 + #E4E4E4 + #565E6B + #565E6B + #00FF00 + #FFFF00 + + loop_start_position + loop_end_position + loop_enabled + #00FF00 + #BCDBFB + + + loop_start_position + LOOP IN + top + #00FF00 + #FFFFFF + + + loop_end_position + LOOP OUT + top + #00FF00 + #FFFFFF + + + + hotcue_1_position + marker_hotcue1_1.png + HOTCUE 1 + bottom + #3FC7FA + #FFFFFF + + + hotcue_2_position + marker_hotcue1_2.png + HOTCUE 2 + bottom + #3FC7FA + #FFFFFF + + + hotcue_3_position + marker_hotcue1_3.png + HOTCUE 3 + bottom + #3FC7FA + #FFFFFF + + + hotcue_4_position + marker_hotcue1_4.png + HOTCUE 4 + bottom + #3FC7FA + #FFFFFF + + + hotcue_5_position + HOTCUE 5 + center + #AE5CFF + #FFFFFF + + + hotcue_6_position + HOTCUE 6 + center + #AE5CFF + #FFFFFF + + + hotcue_7_position + HOTCUE 7 + center + #AE5CFF + #FFFFFF + + + hotcue_8_position + HOTCUE 8 + center + #AE5CFF + #FFFFFF + + + hotcue_9_position + HOTCUE 9 + center + #AE5CFF + #FFFFFF + + + hotcue_10_position + HOTCUE 10 + center + #AE5CFF + #FFFFFF + + + hotcue_11_position + HOTCUE 11 + center + #AE5CFF + #FFFFFF + + + hotcue_12_position + HOTCUE 12 + center + #AE5CFF + #FFFFFF + + + hotcue_13_position + HOTCUE 13 + center + #AE5CFF + #FFFFFF + + + hotcue_14_position + HOTCUE 14 + center + #AE5CFF + #FFFFFF + + + hotcue_15_position + HOTCUE 15 + center + #AE5CFF + #FFFFFF + + + hotcue_16_position + HOTCUE 16 + center + #AE5CFF + #FFFFFF + + + hotcue_17_position + HOTCUE 17 + center + #AE5CFF + #FFFFFF + + + hotcue_18_position + HOTCUE 18 + center + #AE5CFF + #FFFFFF + + + hotcue_19_position + HOTCUE 19 + center + #AE5CFF + #FFFFFF + + + hotcue_20_position + HOTCUE 20 + center + #AE5CFF + #FFFFFF + + + hotcue_21_position + HOTCUE 21 + center + #AE5CFF + #FFFFFF + + + hotcue_22_position + HOTCUE 22 + center + #AE5CFF + #FFFFFF + + + hotcue_23_position + HOTCUE 23 + center + #AE5CFF + #FFFFFF + + + hotcue_24_position + HOTCUE 24 + center + #AE5CFF + #FFFFFF + + + hotcue_25_position + HOTCUE 25 + center + #AE5CFF + #FFFFFF + + + hotcue_26_position + HOTCUE 26 + center + #AE5CFF + #FFFFFF + + + hotcue_27_position + HOTCUE 27 + center + #AE5CFF + #FFFFFF + + + hotcue_28_position + HOTCUE 28 + center + #AE5CFF + #FFFFFF + + + hotcue_29_position + HOTCUE 29 + center + #AE5CFF + #FFFFFF + + + hotcue_30_position + HOTCUE 30 + center + #AE5CFF + #FFFFFF + + + hotcue_31_position + HOTCUE 31 + center + #AE5CFF + #FFFFFF + + + hotcue_32_position + HOTCUE 32 + center + #AE5CFF + #FFFFFF + + + hotcue_33_position + HOTCUE 33 + center + #AE5CFF + #FFFFFF + + + hotcue_34_position + HOTCUE 34 + center + #AE5CFF + #FFFFFF + + + hotcue_35_position + HOTCUE 35 + center + #AE5CFF + #FFFFFF + + + hotcue_36_position + HOTCUE 36 + center + #AE5CFF + #FFFFFF + + + cue_point + marker_cue1.png + CUE + bottom + #FF001C + #FFFFFF + + + + + + + vertical + + + + + + 88,82 + + + + Spinning vinyl Rotates during playback and shows the position of a track. Use the mouse to scratch, halt, spin back and push forward a track. Drop tracks from library or external file manager here. If Vinyl control is enabled, it can display the timecoded vinyls signal quality (see Preferences→Vinyl Control). + + 1 + 0,0 + i,i + vinyl_spinny1_background.png + vinyl_spinny1_foreground.png + vinyl_spinny1_foreground_ghost.png + + + + [Spinny1],show_spinny + visible + + + + + 0,0 + 88,35 + + + + + + + + + Vinyl Control Mode Absolute mode - track position equals needle position and speed Relative mode - track speed equals needle speed regardless of needle position Constant mode - track speed equals last known-steady speed regardless of needle input + + 3 + + 0 + btn_vinylcontrol_mode_abs1.png + btn_vinylcontrol_mode_abs1.png + + + 1 + btn_vinylcontrol_mode_rel1.png + btn_vinylcontrol_mode_rel1.png + + + 2 + btn_vinylcontrol_mode_const1.png + btn_vinylcontrol_mode_const1.png + + -1,2 + + [Channel1],vinylcontrol_mode + + + + + Vinyl Cueing Mode Determines how cue points are treated in vinyl control Relative mode Off - Cue points ignored One Cue - If needle is dropped after the cue point, track will seek to that cue point Hot Cue - Track will seek to nearest previous hot cue point + + 3 + + 0 + btn_vinylcontrol_cueing_off1.png + btn_vinylcontrol_cueing_off1.png + + + 1 + btn_vinylcontrol_cueing_one1.png + btn_vinylcontrol_cueing_one1.png + + + 2 + btn_vinylcontrol_cueing_hot1.png + btn_vinylcontrol_cueing_hot1.png + + 44,2 + + [Channel1],vinylcontrol_cueing + + + + + [Vinylcontrol],show_vinylcontrol + visible + + + + + + + + + + + Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. + + 1 + 2,161 + 250,37 + + #E4E4E4 + #00FF00 + #E4E4E4 + 80 + + [Channel1],playposition + false + + + + + + Track title Displays the title of the loaded track. Informations are extracted from the tracks tags. + + title + 1 + 0,4 + 230,18 + + + + + Track Artist Displays the artist of the loaded track. Informations are extracted from the tracks tags. + + artist + 1 + 0,24 + 210,18 + + + + + Tempo Displays the tempo of the loaded track in BPM (beats per minute) + + 1 + 245,4 + 70,18 + + right + + [Channel1],visual_bpm + + + + + + Time Displays the elapsed or remaining time of the track loaded. Click to toggle between time elapsed/remaining time. + + 1 + 215,24 + 100,18 + + right + + [Channel1],playposition + + + + + + + + Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM + + 1 + + 0 + btn_tap1_over.png + btn_tap1.png + + 232,2 + + [Channel1],bpm_tap + true + + + + + + Pitch rate Displays the current pitch of the track based on the original tempo. + + 1 + 316,28 + 50,14 + + center + + + + + Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value + + knob_pitch1.png + slider_pitch1.png + 335,45 + false + + [Channel1],rate + false + + + + + + Syncronize Left-click: Syncs the tempo (BPM) and phase to that of the other track, if BPM is detected on both Right-click: Syncs the tempo (BPM) to that of the other track, if BPM is detected on both + + 1 + + 0 + btn_sync1_over.png + btn_sync1.png + + 321,7 + + [Channel1],beatsync + true + LeftButton + + + [Channel1],beatsync_tempo + true + RightButton + + + + + + Raise pitch Left-click: Sets the pitch higher Right-click: Sets the pitch higher in small steps. Change the the amount of the steps in Preferences→Interface menu. + + 1 + + 0 + btn_pitch_up1_over.png + btn_pitch_up1.png + + 342,153 + + [Channel1],rate_perm_up + true + LeftButton + + + [Channel1],rate_perm_up_small + true + RightButton + + + + Lower pitch Left-click: Sets the pitch lower Right-click: Sets the pitch lower in small steps. Change the the amount of the steps in Preferences→Interface menu. + + 1 + + 0 + btn_pitch_down1_over.png + btn_pitch_down1.png + + 321,153 + + [Channel1],rate_perm_down + true + LeftButton + + + [Channel1],rate_perm_down_small + true + RightButton + + + + + + Raise pitch temporary (nudge) Left-click: Holds the pitch higher while active Right-click: Holds the pitch higher (small amount) while active Change the amount of the steps in Preferences→Interface + + 1 + + 0 + btn_nudge_up1_over.png + btn_nudge_up1.png + + 342,174 + + [Channel1],rate_temp_up + true + LeftButton + + + [Channel1],rate_temp_up_small + true + RightButton + + + + Lower pitch temporary (nudge) Left-click: Holds the pitch lower while active Right-click: Holds the pitch lower (small amount) while active Change the amount of the steps in Preferences→Interface. + + 1 + + 0 + btn_nudge_down1_over.png + btn_nudge_down1.png + + 321,174 + + [Channel1],rate_temp_down + true + LeftButton + + + [Channel1],rate_temp_down_small + true + RightButton + + + + + + Spinning vinyl Show/hide the spinning vinyl widget + + 2 + + 0 + btn_spinny1.png + btn_spinny1.png + + + 1 + btn_spinny1_over.png + btn_spinny1_over.png + + 253,161 + + [Spinny1],show_spinny + true + LeftButton + + + + + + Adjust beatgrid Adjust beatgrid so closest beat is aligned with the current playposition. + + 1 + + 0 + btn_beatgrid1_over.png + btn_beatgrid1.png + + 253,180 + + [Channel1],beats_translate_curpos + true + LeftButton + + + + + + Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. + + 2 + + 0 + btn_keylock1.png + btn_keylock1.png + + + 1 + btn_keylock1_over.png + btn_keylock1_over.png + + 295,180 + + [Channel1],keylock + + + + + + Quantize Toggles quantization in loops and cues + + 2 + + 0 + btn_quantize1.png + btn_quantize1.png + + + 1 + btn_quantize1_over.png + btn_quantize1_over.png + + 274,180 + + [Channel1],quantize + true + LeftButton + + + + + + Repeat When active the track will repeat if you go past the end or reverse before the start. + + 2 + + 0 + btn_repeat1.png + btn_repeat1.png + + + 1 + btn_repeat1_over.png + btn_repeat1_over.png + + 274,161 + + [Channel1],repeat + + + + + + Eject Eject currently loaded track from deck. + + 1 + + 0 + btn_eject1_over.png + btn_eject1.png + + 295,161 + + [Channel1],eject + true + LeftButton + false + + + + + + 86,204 + 277,43 + + + + + Fast Forward Left-click: Fast forward through the track. Right-click: Jumps to the end of the track. + + 1 + + 0 + btn_forward1_over.png + btn_forward1.png + + 21,0 + + [Channel1],fwd + true + LeftButton + + + [Channel1],end + true + RightButton + + + + Fast Rewind Left-click: Fast rewind through the track. Right-click: Jumps to the beginning of the track. + + 1 + + 0 + btn_rewind1_over.png + btn_rewind1.png + + 0,0 + + [Channel1],back + true + LeftButton + + + [Channel1],start + true + RightButton + + + + + + Reverse Toggles reverse playback when pressed during regular playback + + 1 + + 0 + btn_reverse1_over.png + btn_reverse1.png + + 0,21 + + [Channel1],reverse + true + LeftButton + + + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop1_0125.png + btn_beatloop1_0125.png + + + 1 + btn_beatloop1_0125_over.png + btn_beatloop1_0125_over.png + + 72,0 + + [Channel1],beatloop_0.125 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop1_0250.png + btn_beatloop1_0250.png + + + 1 + btn_beatloop1_0250_over.png + btn_beatloop1_0250_over.png + + 93,0 + + [Channel1],beatloop_0.25 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop1_0500.png + btn_beatloop1_0500.png + + + 1 + btn_beatloop1_0500_over.png + btn_beatloop1_0500_over.png + + 114,0 + + [Channel1],beatloop_0.5 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop1_1.png + btn_beatloop1_1.png + + + 1 + btn_beatloop1_1_over.png + btn_beatloop1_1_over.png + + 135,0 + + [Channel1],beatloop_1 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop1_2.png + btn_beatloop1_2.png + + + 1 + btn_beatloop1_2_over.png + btn_beatloop1_2_over.png + + 72,21 + + [Channel1],beatloop_2 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop1_4.png + btn_beatloop1_4.png + + + 1 + btn_beatloop1_4_over.png + btn_beatloop1_4_over.png + + 93,21 + + [Channel1],beatloop_4 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop1_8_over.png + btn_beatloop1_8.png + + + 1 + btn_beatloop1_8.png + btn_beatloop1_8_over.png + + 114,21 + + [Channel1],beatloop_8 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop1_16.png + btn_beatloop1_16.png + + + 1 + btn_beatloop1_16_over.png + btn_beatloop1_16_over.png + + 135,21 + + [Channel1],beatloop_16 + true + LeftButton + + + + + + Loop halve Halves the current loop's length by moving the end marker. Deck immediately loops if past the new endpoint. + + 1 + + 0 + btn_beatloop1_halve_over.png + btn_beatloop1_halve.png + + 50,0 + + [Channel1],loop_halve + true + LeftButton + + + + Loop double Doubles the current loop's length by moving the end marker. + + 1 + + 0 + btn_beatloop1_double_over.png + btn_beatloop1_double.png + + 157,0 + + [Channel1],loop_double + true + LeftButton + + + + + + Loop-In marker Sets the deck loop-in position to the current play position. + + 1 + + 0 + btn_loop_in1_over.png + btn_loop_in1.png + + 186,0 + + [Channel1],loop_in + true + LeftButton + + + + Loop-Out marker Sets the deck loop-out position to the current play position. + + 1 + + 0 + btn_loop_out1_over.png + btn_loop_out1.png + + 207,0 + + [Channel1],loop_out + true + LeftButton + + + + Reloop/Exit Toggles the current loop on or off. Works only if Loop-In and Loop-Out marker are set. + + 2 + true + + 0 + btn_reloop1.png + btn_reloop1.png + + + 1 + btn_reloop1_over.png + btn_reloop1_over.png + + 186,21 + + [Channel1],reloop_exit + true + LeftButton + false + + + [Channel1],loop_enabled + false + + + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_1_over.png + btn_hotcue1_1.png + + + 1 + btn_hotcue1_1.png + btn_hotcue1_1_over.png + + 236,0 + + [Channel1],hotcue_1_activate + true + LeftButton + false + + + [Channel1],hotcue_1_clear + true + RightButton + false + + + [Channel1],hotcue_1_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_2_over.png + btn_hotcue1_2.png + + + 1 + btn_hotcue1_2.png + btn_hotcue1_2_over.png + + 257,0 + + [Channel1],hotcue_2_activate + true + LeftButton + false + + + [Channel1],hotcue_2_clear + true + RightButton + false + + + [Channel1],hotcue_2_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_3_over.png + btn_hotcue1_3.png + + + 1 + btn_hotcue1_3.png + btn_hotcue1_3_over.png + + 236,21 + + [Channel1],hotcue_3_activate + true + LeftButton + false + + + [Channel1],hotcue_3_clear + true + RightButton + false + + + [Channel1],hotcue_3_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue1_4_over.png + btn_hotcue1_4.png + + + 1 + btn_hotcue1_4.png + btn_hotcue1_4_over.png + + 257,21 + + [Channel1],hotcue_4_activate + true + LeftButton + false + + + [Channel1],hotcue_4_clear + true + RightButton + false + + + [Channel1],hotcue_4_enabled + false + + + + + + + + + + + + + + + 646,8 + 370,250 + + + + + 370,250 + + + + + Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected + + 3 + btn_vinylcontrol_indicator_horizontal1.png + btn_vinylcontrol_indicator_horizontal2.png + btn_vinylcontrol_indicator_horizontal3.png + -1,0 + + [Channel2],vinylcontrol_status + + + + Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected + + 3 + btn_vinylcontrol_indicator_horizontal1.png + btn_vinylcontrol_indicator_horizontal2.png + btn_vinylcontrol_indicator_horizontal3.png + -1,248 + + [Channel2],vinylcontrol_status + + + + Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected + + 3 + btn_vinylcontrol_indicator_vertical1.png + btn_vinylcontrol_indicator_vertical2.png + btn_vinylcontrol_indicator_vertical3.png + 0,0 + + [Channel2],vinylcontrol_status + + + + Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected + + 3 + btn_vinylcontrol_indicator_vertical1.png + btn_vinylcontrol_indicator_vertical2.png + btn_vinylcontrol_indicator_vertical3.png + 367,0 + + [Channel2],vinylcontrol_status + + + + + + 2,43 + 318,118 + horizontal + + + + + + + Waveform display Shows the loaded tracks' waveforms near the playback position. Left-click: Use the mouse to scratch, halt, spin back and push forward a track. Right-click: Drag with mouse to make temporary pitch adjustments. Drop tracks from library or external file manager here. + + 2 + 0,0 + + + #3F4249 + + #E4E4E4 + #E4E4E4 + #565E6B + #565E6B + #00FF00 + #FFFF00 + + loop_start_position + loop_end_position + loop_enabled + #00FF00 + #BCDBFB + + + loop_start_position + LOOP IN + top + #00FF00 + #FFFFFF + + + loop_end_position + LOOP OUT + top + #00FF00 + #FFFFFF + + + + hotcue_1_position + marker_hotcue1_2.png + HOTCUE 1 + bottom + #3FC7FA + #FFFFFF + + + hotcue_2_position + marker_hotcue1_2.png + HOTCUE 2 + bottom + #3FC7FA + #FFFFFF + + + hotcue_3_position + marker_hotcue1_3.png + HOTCUE 3 + bottom + #3FC7FA + #FFFFFF + + + hotcue_4_position + marker_hotcue1_4.png + HOTCUE 4 + bottom + #3FC7FA + #FFFFFF + + + hotcue_5_position + HOTCUE 5 + center + #AE5CFF + #FFFFFF + + + hotcue_6_position + HOTCUE 6 + center + #AE5CFF + #FFFFFF + + + hotcue_7_position + HOTCUE 7 + center + #AE5CFF + #FFFFFF + + + hotcue_8_position + HOTCUE 8 + center + #AE5CFF + #FFFFFF + + + hotcue_9_position + HOTCUE 9 + center + #AE5CFF + #FFFFFF + + + hotcue_10_position + HOTCUE 10 + center + #AE5CFF + #FFFFFF + + + hotcue_11_position + HOTCUE 11 + center + #AE5CFF + #FFFFFF + + + hotcue_12_position + HOTCUE 12 + center + #AE5CFF + #FFFFFF + + + hotcue_13_position + HOTCUE 13 + center + #AE5CFF + #FFFFFF + + + hotcue_14_position + HOTCUE 14 + center + #AE5CFF + #FFFFFF + + + hotcue_15_position + HOTCUE 15 + center + #AE5CFF + #FFFFFF + + + hotcue_16_position + HOTCUE 16 + center + #AE5CFF + #FFFFFF + + + hotcue_17_position + HOTCUE 17 + center + #AE5CFF + #FFFFFF + + + hotcue_18_position + HOTCUE 18 + center + #AE5CFF + #FFFFFF + + + hotcue_19_position + HOTCUE 19 + center + #AE5CFF + #FFFFFF + + + hotcue_20_position + HOTCUE 20 + center + #AE5CFF + #FFFFFF + + + hotcue_21_position + HOTCUE 21 + center + #AE5CFF + #FFFFFF + + + hotcue_22_position + HOTCUE 22 + center + #AE5CFF + #FFFFFF + + + hotcue_23_position + HOTCUE 23 + center + #AE5CFF + #FFFFFF + + + hotcue_24_position + HOTCUE 24 + center + #AE5CFF + #FFFFFF + + + hotcue_25_position + HOTCUE 25 + center + #AE5CFF + #FFFFFF + + + hotcue_26_position + HOTCUE 26 + center + #AE5CFF + #FFFFFF + + + hotcue_27_position + HOTCUE 27 + center + #AE5CFF + #FFFFFF + + + hotcue_28_position + HOTCUE 28 + center + #AE5CFF + #FFFFFF + + + hotcue_29_position + HOTCUE 29 + center + #AE5CFF + #FFFFFF + + + hotcue_30_position + HOTCUE 30 + center + #AE5CFF + #FFFFFF + + + hotcue_31_position + HOTCUE 31 + center + #AE5CFF + #FFFFFF + + + hotcue_32_position + HOTCUE 32 + center + #AE5CFF + #FFFFFF + + + hotcue_33_position + HOTCUE 33 + center + #AE5CFF + #FFFFFF + + + hotcue_34_position + HOTCUE 34 + center + #AE5CFF + #FFFFFF + + + hotcue_35_position + HOTCUE 35 + center + #AE5CFF + #FFFFFF + + + hotcue_36_position + HOTCUE 36 + center + #AE5CFF + #FFFFFF + + + cue_point + marker_cue2.png + CUE + bottom + #FF001C + #FFFFFF + + + + + + + vertical + + + + + + 88,82 + + + + Spinning vinyl Rotates during playback and shows the position of a track. Use the mouse to scratch, halt, spin back and push forward a track. Drop tracks from library or external file manager here. If Vinyl control is enabled, it can display the timecoded vinyls signal quality (see Preferences→Vinyl Control). + + 2 + 0,0 + i,i + vinyl_spinny2_background.png + vinyl_spinny2_foreground.png + vinyl_spinny2_foreground_ghost.png + + + + [Spinny2],show_spinny + visible + + + + + 0,0 + 88,35 + + + + + + + + + Vinyl Control Mode Absolute mode - track position equals needle position and speed Relative mode - track speed equals needle speed regardless of needle position Constant mode - track speed equals last known-steady speed regardless of needle input + + 3 + + 0 + btn_vinylcontrol_mode_abs2.png + btn_vinylcontrol_mode_abs2.png + + + 1 + btn_vinylcontrol_mode_rel2.png + btn_vinylcontrol_mode_rel2.png + + + 2 + btn_vinylcontrol_mode_const2.png + btn_vinylcontrol_mode_const2.png + + -1,2 + + [Channel2],vinylcontrol_mode + + + + + Vinyl Cueing Mode Determines how cue points are treated in vinyl control Relative mode Off - Cue points ignored One Cue - If needle is dropped after the cue point, track will seek to that cue point Hot Cue - Track will seek to nearest previous hot cue point + + 3 + + 0 + btn_vinylcontrol_cueing_off2.png + btn_vinylcontrol_cueing_off2.png + + + 1 + btn_vinylcontrol_cueing_one2.png + btn_vinylcontrol_cueing_one2.png + + + 2 + btn_vinylcontrol_cueing_hot2.png + btn_vinylcontrol_cueing_hot2.png + + 44,2 + + [Channel2],vinylcontrol_cueing + + + + + [Vinylcontrol],show_vinylcontrol + visible + + + + + + + + + + + Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. + + 2 + 2,161 + 250,37 + + #E4E4E4 + #00FF00 + #E4E4E4 + 80 + + [Channel2],playposition + false + + + + + + Track title Displays the title of the loaded track. Informations are extracted from the tracks tags. + + title + 2 + 0,4 + 230,18 + + + + + Track Artist Displays the artist of the loaded track. Informations are extracted from the tracks tags. + + artist + 2 + 0,24 + 210,18 + + + + + Tempo Displays the tempo of the loaded track in BPM (beats per minute) + + 2 + 245,4 + 70,18 + + right + + [Channel2],visual_bpm + + + + + + Time Displays the elapsed or remaining time of the track loaded. Click to toggle between time elapsed/remaining time. + + 2 + 215,24 + 100,18 + + right + + [Channel2],playposition + + + + + + + + Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM + + 1 + + 0 + btn_tap2_over.png + btn_tap2.png + + 232,2 + + [Channel2],bpm_tap + true + + + + + + Pitch rate Displays the current pitch of the track based on the original tempo. + + 2 + 316,28 + 50,14 + + center + + + + + Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value + + knob_pitch2.png + slider_pitch2.png + 335,45 + false + + [Channel2],rate + false + + + + + + Syncronize Left-click: Syncs the tempo (BPM) and phase to that of the other track, if BPM is detected on both Right-click: Syncs the tempo (BPM) to that of the other track, if BPM is detected on both + + 1 + + 0 + btn_sync2_over.png + btn_sync2.png + + 321,7 + + [Channel2],beatsync + true + LeftButton + + + [Channel2],beatsync_tempo + true + RightButton + + + + + + Raise pitch Left-click: Sets the pitch higher Right-click: Sets the pitch higher in small steps. Change the the amount of the steps in Preferences→Interface menu. + + 1 + + 0 + btn_pitch_up2_over.png + btn_pitch_up2.png + + 342,153 + + [Channel2],rate_perm_up + true + LeftButton + + + [Channel2],rate_perm_up_small + true + RightButton + + + + Lower pitch Left-click: Sets the pitch lower Right-click: Sets the pitch lower in small steps. Change the the amount of the steps in Preferences→Interface menu. + + 1 + + 0 + btn_pitch_down2_over.png + btn_pitch_down2.png + + 321,153 + + [Channel2],rate_perm_down + true + LeftButton + + + [Channel2],rate_perm_down_small + true + RightButton + + + + + + Raise pitch temporary (nudge) Left-click: Holds the pitch higher while active Right-click: Holds the pitch higher (small amount) while active Change the amount of the steps in Preferences→Interface + + 1 + + 0 + btn_nudge_up2_over.png + btn_nudge_up2.png + + 342,174 + + [Channel2],rate_temp_up + true + LeftButton + + + [Channel2],rate_temp_up_small + true + RightButton + + + + Lower pitch temporary (nudge) Left-click: Holds the pitch lower while active Right-click: Holds the pitch lower (small amount) while active Change the amount of the steps in Preferences→Interface. + + 1 + + 0 + btn_nudge_down2_over.png + btn_nudge_down2.png + + 321,174 + + [Channel2],rate_temp_down + true + LeftButton + + + [Channel2],rate_temp_down_small + true + RightButton + + + + + + Spinning vinyl Show/hide the spinning vinyl widget + + 2 + + 0 + btn_spinny2.png + btn_spinny2.png + + + 1 + btn_spinny2_over.png + btn_spinny2_over.png + + 253,161 + + [Spinny2],show_spinny + true + LeftButton + + + + + + Adjust beatgrid Adjust beatgrid so closest beat is aligned with the current playposition. + + 1 + + 0 + btn_beatgrid2_over.png + btn_beatgrid2.png + + 253,180 + + [Channel2],beats_translate_curpos + true + LeftButton + + + + + + Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. + + 2 + + 0 + btn_keylock2.png + btn_keylock2.png + + + 1 + btn_keylock2_over.png + btn_keylock2_over.png + + 295,180 + + [Channel2],keylock + + + + + + Quantize Toggles quantization in loops and cues + + 2 + + 0 + btn_quantize2.png + btn_quantize2.png + + + 1 + btn_quantize2_over.png + btn_quantize2_over.png + + 274,180 + + [Channel2],quantize + true + LeftButton + + + + + + Repeat When active the track will repeat if you go past the end or reverse before the start. + + 2 + + 0 + btn_repeat2.png + btn_repeat2.png + + + 1 + btn_repeat2_over.png + btn_repeat2_over.png + + 274,161 + + [Channel2],repeat + + + + + + Eject Eject currently loaded track from deck. + + 1 + + 0 + btn_eject2_over.png + btn_eject2.png + + 295,161 + + [Channel2],eject + true + LeftButton + false + + + + + + 86,204 + 277,43 + + + + + Fast Forward Left-click: Fast forward through the track. Right-click: Jumps to the end of the track. + + 1 + + 0 + btn_forward2_over.png + btn_forward2.png + + 21,0 + + [Channel2],fwd + true + LeftButton + + + [Channel2],end + true + RightButton + + + + Fast Rewind Left-click: Fast rewind through the track. Right-click: Jumps to the beginning of the track. + + 1 + + 0 + btn_rewind2_over.png + btn_rewind2.png + + 0,0 + + [Channel2],back + true + LeftButton + + + [Channel2],start + true + RightButton + + + + + + Reverse Toggles reverse playback when pressed during regular playback + + 1 + + 0 + btn_reverse2_over.png + btn_reverse2.png + + 0,21 + + [Channel2],reverse + true + LeftButton + + + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop2_0125.png + btn_beatloop2_0125.png + + + 1 + btn_beatloop2_0125_over.png + btn_beatloop2_0125_over.png + + 72,0 + + [Channel2],beatloop_0.125 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop2_0250.png + btn_beatloop2_0250.png + + + 1 + btn_beatloop2_0250_over.png + btn_beatloop2_0250_over.png + + 93,0 + + [Channel2],beatloop_0.25 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop2_0500.png + btn_beatloop2_0500.png + + + 1 + btn_beatloop2_0500_over.png + btn_beatloop2_0500_over.png + + 114,0 + + [Channel2],beatloop_0.5 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop2_1.png + btn_beatloop2_1.png + + + 1 + btn_beatloop2_1_over.png + btn_beatloop2_1_over.png + + 135,0 + + [Channel2],beatloop_1 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop2_2.png + btn_beatloop2_2.png + + + 1 + btn_beatloop2_2_over.png + btn_beatloop2_2_over.png + + 72,21 + + [Channel2],beatloop_2 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop2_4.png + btn_beatloop2_4.png + + + 1 + btn_beatloop2_4_over.png + btn_beatloop2_4_over.png + + 93,21 + + [Channel2],beatloop_4 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop2_8_over.png + btn_beatloop2_8.png + + + 1 + btn_beatloop2_8.png + btn_beatloop2_8_over.png + + 114,21 + + [Channel2],beatloop_8 + true + LeftButton + + + + Beatloop Setup a loop over X beats + + 2 + + 0 + btn_beatloop2_16.png + btn_beatloop2_16.png + + + 1 + btn_beatloop2_16_over.png + btn_beatloop2_16_over.png + + 135,21 + + [Channel2],beatloop_16 + true + LeftButton + + + + + + Loop halve Halves the current loop's length by moving the end marker. Deck immediately loops if past the new endpoint. + + 1 + + 0 + btn_beatloop2_halve_over.png + btn_beatloop2_halve.png + + 50,0 + + [Channel2],loop_halve + true + LeftButton + + + + Loop double Doubles the current loop's length by moving the end marker. + + 1 + + 0 + btn_beatloop2_double_over.png + btn_beatloop2_double.png + + 157,0 + + [Channel2],loop_double + true + LeftButton + + + + + + Loop-In marker Sets the deck loop-in position to the current play position. + + 1 + + 0 + btn_loop_in2_over.png + btn_loop_in2.png + + 186,0 + + [Channel2],loop_in + true + LeftButton + + + + Loop-Out marker Sets the deck loop-out position to the current play position. + + 1 + + 0 + btn_loop_out2_over.png + btn_loop_out2.png + + 207,0 + + [Channel2],loop_out + true + LeftButton + + + + Reloop/Exit Toggles the current loop on or off. Works only if Loop-In and Loop-Out marker are set. + + 2 + true + + 0 + btn_reloop2.png + btn_reloop2.png + + + 1 + btn_reloop2_over.png + btn_reloop2_over.png + + 186,21 + + [Channel2],reloop_exit + true + LeftButton + false + + + [Channel2],loop_enabled + false + + + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue2_1_over.png + btn_hotcue2_1.png + + + 1 + btn_hotcue2_1.png + btn_hotcue2_1_over.png + + 236,0 + + [Channel2],hotcue_1_activate + true + LeftButton + false + + + [Channel2],hotcue_1_clear + true + RightButton + false + + + [Channel2],hotcue_1_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue2_2_over.png + btn_hotcue2_2.png + + + 1 + btn_hotcue2_2.png + btn_hotcue2_2_over.png + + 257,0 + + [Channel2],hotcue_2_activate + true + LeftButton + false + + + [Channel2],hotcue_2_clear + true + RightButton + false + + + [Channel2],hotcue_2_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue2_3_over.png + btn_hotcue2_3.png + + + 1 + btn_hotcue2_3.png + btn_hotcue2_3_over.png + + 236,21 + + [Channel2],hotcue_3_activate + true + LeftButton + false + + + [Channel2],hotcue_3_clear + true + RightButton + false + + + [Channel2],hotcue_3_enabled + false + + + + Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). + + 2 + true + true + + 0 + btn_hotcue2_4_over.png + btn_hotcue2_4.png + + + 1 + btn_hotcue2_4.png + btn_hotcue2_4_over.png + + 257,21 + + [Channel2],hotcue_4_activate + true + LeftButton + false + + + [Channel2],hotcue_4_clear + true + RightButton + false + + + [Channel2],hotcue_4_enabled + false + + + + + + + + + + + + + + + 10,206 + 80,50 + + + + + + Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). + + btn_clipping_microphone_over.png + btn_clipping_microphone.png + 64,11 + + [Microphone],PeakIndicator + + + + + + Microphone volume meter Outputs the current instantaneous microphone volume + + btn_volume_display_microphone_over.png + btn_volume_display_microphone.png + 24,11 + true + 5 + 500 + 50 + 2 + + [Microphone],VuMeter + + + + + + Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) + + 3 + + 0 + btn_orientation_microphone_left_over.png + btn_orientation_microphone_left_over.png + + + 1 + btn_orientation_microphone_master.png + btn_orientation_microphone_master.png + + + 2 + btn_orientation_microphone_right_over.png + btn_orientation_microphone_right_over.png + + 4,4 + + [Microphone],orientation + true + LeftButton + + + + + + Microphone volume Adjusts the microphone volume. Right-click: Reset to default value + + 64 + knobs/knob_rotary_s%1.png + 49,24 + + [Microphone],volume + + + + + + Talkover Hold-to-talk and mix microphone input into the master output. + + 1 + + 0 + btn_microphone_talkover_over.png + btn_microphone_talkover.png + + 4,27 + + [Microphone],talkover + true + LeftButton + + + + + [Microphone],show_microphone + visible + + + + diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_crossfader.png b/res/skins/ShadeDark1024x600-Netbook/slider_crossfader.png new file mode 100644 index 0000000000000000000000000000000000000000..1cfec86233249b3b77ab6c432d307523a23f5c8f GIT binary patch literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^`9RFa!3-qDHg8%Bq}T#{LR=-~RqUNS*QqVp0Tfa5 zba4!+xb^nZL0$$1j%J5D^_l4}mK(B(?cnz@x818U@40@}^u4U#99d0IZ+dYJsDr`N L)z4*}Q$iB}8RsFT literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_pitch1.png b/res/skins/ShadeDark1024x600-Netbook/slider_pitch1.png new file mode 100644 index 0000000000000000000000000000000000000000..cb1cc25e12cac81d331993c01f897833f966fb21 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^d_bJR!3-q7SsN|^QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2Rb)5S5Q;?~>a8+jQRIG7cF|GzvhAe~vdc;bpxue!^))2A_mdKI;Vst09zU*Q2+n{ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_pitch2.png b/res/skins/ShadeDark1024x600-Netbook/slider_pitch2.png new file mode 100644 index 0000000000000000000000000000000000000000..cb1cc25e12cac81d331993c01f897833f966fb21 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^d_bJR!3-q7SsN|^QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2Rb)5S5Q;?~>a8+jQRIG7cF|GzvhAe~vdc;bpxue!^))2A_mdKI;Vst09zU*Q2+n{ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_pitch_sampler.png b/res/skins/ShadeDark1024x600-Netbook/slider_pitch_sampler.png new file mode 100644 index 0000000000000000000000000000000000000000..ded167cec3b38f2430962ad67b938225f4498010 GIT binary patch literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^d_Ww^!3-q*?yFu0QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2RL)5S5Q;?~>aih@8Mli`basZK@4@5Pr>g1uhGYA?!WSYG~f1wT-Z N!PC{xWt~$(69Dg*BANgI literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_volume1.png b/res/skins/ShadeDark1024x600-Netbook/slider_volume1.png new file mode 100644 index 0000000000000000000000000000000000000000..cb1cc25e12cac81d331993c01f897833f966fb21 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^d_bJR!3-q7SsN|^QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2Rb)5S5Q;?~>a8+jQRIG7cF|GzvhAe~vdc;bpxue!^))2A_mdKI;Vst09zU*Q2+n{ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_volume2.png b/res/skins/ShadeDark1024x600-Netbook/slider_volume2.png new file mode 100644 index 0000000000000000000000000000000000000000..cb1cc25e12cac81d331993c01f897833f966fb21 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^d_bJR!3-q7SsN|^QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2Rb)5S5Q;?~>a8+jQRIG7cF|GzvhAe~vdc;bpxue!^))2A_mdKI;Vst09zU*Q2+n{ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png new file mode 100644 index 0000000000000000000000000000000000000000..d39f1544686eb0e7d9646125799c7b7cec89ad22 GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^K0s{3!3-qDE%demDb4_&5LbIA&;S4bhs9;D<9X}= z6l5w1@(X5QD4TrN0?5<%ba4!+xb^nTMn(n!0hSH+zxmf*T=`*cN`px3q+O;}YjkE$ lOg5VNX7L(#MurbLOiN8!wJW+0odRlQ@O1TaS?83{1OOjEE8qYC literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png new file mode 100644 index 0000000000000000000000000000000000000000..374f20eec6f925ca115226be5742e8d7a98c1162 GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^zkoP}gBeK9h`Vb8q}T#{LR{^gJj3F$nRvStfg&?K zT^vIyZoR#;kc&Zq$HDRYfBRe^!ERTH+j|o>$j(Z!Sd{zX#f!Nw7AI}IW?g~`zSMl# zG}ZcR#XT2yRPcXZxRkh@EHbdQy{mI}BBvIr30QcWP5$+`xx3%@ea|5vDybN?4(Lb* MPgg&ebxsLQ0LgGs3;+NC literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png new file mode 100644 index 0000000000000000000000000000000000000000..962f91fbccb6e784a5002aa9efa6815e7d68fae7 GIT binary patch literal 15109 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 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png b/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..17ac682340e02bf53e85a7101cce4dc43f299b35 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^96-#;!3HGxgLCzORG_DeV@SoV)(abX8w_|D4hC#y zZcdT7mN?gAo@#bWn}{rl0G+92289?P_yFV mI+GcM(vHfQ^{WzxHrCu@fjdmuxx$(dHlQtL6RZPlZ0b0o5>FVdQ I&MBb@08?%}rvLx| literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..c75ef514746d6540cf0d194e0e33f9b3dbf7cdc5 GIT binary patch literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OG6Q@t<7|2UN4KM)l%yh>pB zlECmOk>P(T!^Z@MA4v?ql0j@B14#Z)V|W|S@HUR&dlJJJpqey>4+$XluM`mXb0Wjv zREB3U46kBAYJgHe5r`~M9H{tI%V&@aq)LMPf*CkDIVB{lta^c9{f2{w4j(>o`ufeA z_y7J)h_yZiR2Amw;uunK%lDMKP=f-4^Tmh_-@BAL?);yS7Nb!A)kt`Ug2j(N9=pED zZ=HSed2yYLW#<0NY1$LaX9Z6AF4b%@PbDjDR*Uc)I$ztaD0e0sv0+dDH*^ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png new file mode 100644 index 0000000000000000000000000000000000000000..bea88dec076ede1dc0129ccffdfc28423e76c738 GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1O*aCb)T>t<7zj^a!{-~ZRpa@e* zkY6x^1A~9h3LsC$)5S5Q;?~jQhP*)Dp$%vEXWBI}{}N>nTHnLK$HJh?`9=9AP=>+N L)z4*}Q$iB}#T_8a literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..1d6afaf116119d481fb7397e4d95f64844dc50b3 GIT binary patch literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c1!3HEbvi3>O=$B>F!ThA>N1&XxF!ThA>N1d25{m=~vu q9a(ktvcpdohMTt=tg0HjFEhE?O^W(e}LdVL_m722WQ%mvv4FO#sppEW-c* literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_microphone_over.png b/res/skins/ShadeDark1024x600-Netbook/tab_microphone_over.png new file mode 100644 index 0000000000000000000000000000000000000000..ac716d02b0edbb9759e59959213578e8de929b9c GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^8bHj)!3-p`uC6NqQk(%kA+A9B|Ns9%KT^6}wnqaw zOeH~n!3+##lh0ZJc}AWtjv*Ddu1+`zG>OIe<9~VI?nz30%XU^w3s7EH#l)qf_FlH5 lQm3bL1@Da7T_4{b77aIJv^}qVSP-b2!PC{xWt~$(69Bm|EPnt1 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_sampler.png b/res/skins/ShadeDark1024x600-Netbook/tab_sampler.png new file mode 100644 index 0000000000000000000000000000000000000000..034630a1b92d5d962ce4ae267b6b75c202b62464 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^IzY_F!3-pm?fCeB6kC8#h%1nuzTsf#TyGU1i?Jlg zFPOpM*^M+H$J*1yF{I+w(|$)T1_hqOoBr2FiLA|tQ+O7@_V&~<_KFa@^YuG={;`)z4*}Q$iB}doC<_ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/tab_sampler_over.png new file mode 100644 index 0000000000000000000000000000000000000000..67dc813cdde6de4a7609a5a07d614dd399cb9bd1 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^IzY_F!3-pm?fCeB6kC8#h%1l|`jHazwD&2H#aI&L z7tG-B>_!@pW9{kU7*cWTX}=>Eg96XtP5gTe~DWM4f*kdhm literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol.png b/res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol.png new file mode 100644 index 0000000000000000000000000000000000000000..8d45f3cdb4baef6194163666f75164d8db0bf122 GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^8bHj)!3-p`uC6NqQk(%kA+A9B|NsBfHymuXDVzo5 zFqH)P1v4;|O+IS@S3 rt$j+i$HGG54A)Lvv0EhK`v>l;ISj{BpD$kqG=ag>)z4*}Q$iB}%Nj8@ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol_over.png b/res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol_over.png new file mode 100644 index 0000000000000000000000000000000000000000..a2104e61acb6ac17b514ec8e5375730c4b7cf38a GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^8bHj)!3-p`uC6NqQk(%kA+A9B|Ns9%KT^6}wnqaw zOeH~n!3+##lh0ZJd1jt2jv*Ddu1+xIVld!1vgUt%38$91$g0&7om+VpmCi}&e9_W$ rYoC(sv9ORh!?hDv>=udm{(<{y4#V-(=gXG?ObP0l+XkKu_rL{ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_background.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_background.png new file mode 100644 index 0000000000000000000000000000000000000000..3158d8dbea3b41762b772c088bfa11ee0a09da5a GIT binary patch literal 1568 zcmW;Mi9gc~9Ki7<9WT=3rKjBH7&CS-#x^_17E4C161B=AJG3DQ7F39^V{=zy}z&T_b>P+JJanH^ZiFf&A=VMsF=(%cAu!WaWgF>n(L zxTz%^ZGkYgM4+)qDIC%ahmyjh%<(7;&cqyVg29_&tV}TkQwsuGiiozfLSqSLmP9j4 zk~tPY#sF{tYYPA#U}Fie0vyHy2mo6w(FRAb#Sv}sL<*h+pyJ84crwL`M71IVjt~IW z06U_!9m&R?WNSx0Oe0h59h_b1E*>r(o~|dnPIxg+dNaHj=g%_zeEcq53b=YXFyu;5 zXh?X(waDnm=$Po(xR|)aTM0>ViOF$^x8sv;C#9q%r`}1u!%EM%pP8L=KRY)ohm)O~ zm(9t`&Ew{A3-Y*y`2|G=E=nX4#pVPN2(5bQR3~Wj@O!NvBg65VsY>{@C|O>n8i?=W8nIM`GVoM zlN;9tChPLeFBhtYkMH*%dsw#knfq>fa}dRO2JvbRIS(lwQgl*HP4hKQh+bc~6v|Ob z=jTvLeBw)ldj#)sj_ks)!0^94w(NORZowc{60f9jF}~7Cfi_gl@-UxG_ccYvaOxq- zv+VAGVli3?QTZ9_>3NTT!wc=OZC|Ij+lh8+7c@;{*Io9>933W++8Oc6vmWN}3;E>A z3@{rO3dGe#avx~CXJ5n(T?O#W;M_@U+I;PY}MLT`Z(-rpav^petY_Tz>2?h z@egdpj$xm_?w6#Q-J4h-y%1oJv|nwy@bq0l0mVs=V_y3^zq17ex-4VKN(FcB-mw?I zjObpW42_#2^2(zWWK>qatajVWBKli!ch5fPWqyw+h`v_f6sAiivffItovX)D+e_Nf z*qDNVJmJx0+-J{*rcJ_iB_sJfxC`z72k!hx$TB`kUzlFdB zl6_+^aCi=<5O-`@b9g9LaVc_ZtIuDxD`m7_Lb?c+xqM6Qlqa!-AE&MgVzrEGmZ`Z{Sb%YFVlZ!Va%IQVtyRll!# z_r_?J$_L!ExOT<^+(H+cG3m@hZi9}G6eWFkOj~M}dIaWZ4Z`?!P{O!QO4Z@BvHt=3M$s++ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a4dc80c452ec1ab8b7fb6b549962dbd4ec941804 GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?$y1o(uw{{R2~2`z!&)Nl)+T?!>Z ze!&d9YMMqif#H#fRg0Ew*m30a*~_=@zIgZf%hzw;fBpIUk3EKABT#LWr;B4q#jUqz zj*B)ZFt|7#nN!U6a=&-K*@p9%=QAC9P+l0j@_63dqv_jr?!G?lr`(?fy4}h@0%PV) yfr9v*_I@$TPH#26Wu&$In$*Xc5g=bPG~D2Bn$PR9t3QMn#PxLbb6Mw<&;$T*JgM*i literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground_ghost.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground_ghost.png new file mode 100644 index 0000000000000000000000000000000000000000..8f9be4de36f314d1bca93d2dffdaf56851efe0ec GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?$S2Ka=y{{R1f`i6tlME0lN0}Yoh z3GxeOP&2fTC@8Gx?wLG$#p<26AH00~;oJ8gKY#uG*R*(rB~V$Yr;B4q#jUqzt}`|$ zh_E;wkTF8)=(Zhx_d3ttvId&Sz_2B*e?7O-Glk%jAfBhIpUXO@geCxv^N0BW literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_background.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_background.png new file mode 100644 index 0000000000000000000000000000000000000000..3158d8dbea3b41762b772c088bfa11ee0a09da5a GIT binary patch literal 1568 zcmW;Mi9gc~9Ki7<9WT=3rKjBH7&CS-#x^_17E4C161B=AJG3DQ7F39^V{=zy}z&T_b>P+JJanH^ZiFf&A=VMsF=(%cAu!WaWgF>n(L zxTz%^ZGkYgM4+)qDIC%ahmyjh%<(7;&cqyVg29_&tV}TkQwsuGiiozfLSqSLmP9j4 zk~tPY#sF{tYYPA#U}Fie0vyHy2mo6w(FRAb#Sv}sL<*h+pyJ84crwL`M71IVjt~IW z06U_!9m&R?WNSx0Oe0h59h_b1E*>r(o~|dnPIxg+dNaHj=g%_zeEcq53b=YXFyu;5 zXh?X(waDnm=$Po(xR|)aTM0>ViOF$^x8sv;C#9q%r`}1u!%EM%pP8L=KRY)ohm)O~ zm(9t`&Ew{A3-Y*y`2|G=E=nX4#pVPN2(5bQR3~Wj@O!NvBg65VsY>{@C|O>n8i?=W8nIM`GVoM zlN;9tChPLeFBhtYkMH*%dsw#knfq>fa}dRO2JvbRIS(lwQgl*HP4hKQh+bc~6v|Ob z=jTvLeBw)ldj#)sj_ks)!0^94w(NORZowc{60f9jF}~7Cfi_gl@-UxG_ccYvaOxq- zv+VAGVli3?QTZ9_>3NTT!wc=OZC|Ij+lh8+7c@;{*Io9>933W++8Oc6vmWN}3;E>A z3@{rO3dGe#avx~CXJ5n(T?O#W;M_@U+I;PY}MLT`Z(-rpav^petY_Tz>2?h z@egdpj$xm_?w6#Q-J4h-y%1oJv|nwy@bq0l0mVs=V_y3^zq17ex-4VKN(FcB-mw?I zjObpW42_#2^2(zWWK>qatajVWBKli!ch5fPWqyw+h`v_f6sAiivffItovX)D+e_Nf z*qDNVJmJx0+-J{*rcJ_iB_sJfxC`z72k!hx$TB`kUzlFdB zl6_+^aCi=<5O-`@b9g9LaVc_ZtIuDxD`m7_Lb?c+xqM6Qlqa!-AE&MgVzrEGmZ`Z{Sb%YFVlZ!Va%IQVtyRll!# z_r_?J$_L!ExOT<^+(H+cG3m@hZi9}G6eWFkOj~M}dIaWZ4Z`?!P{O!QO4Z@BvHt=3M$s++ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a4dc80c452ec1ab8b7fb6b549962dbd4ec941804 GIT binary patch literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?$y1o(uw{{R2~2`z!&)Nl)+T?!>Z ze!&d9YMMqif#H#fRg0Ew*m30a*~_=@zIgZf%hzw;fBpIUk3EKABT#LWr;B4q#jUqz zj*B)ZFt|7#nN!U6a=&-K*@p9%=QAC9P+l0j@_63dqv_jr?!G?lr`(?fy4}h@0%PV) yfr9v*_I@$TPH#26Wu&$In$*Xc5g=bPG~D2Bn$PR9t3QMn#PxLbb6Mw<&;$T*JgM*i literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground_ghost.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground_ghost.png new file mode 100644 index 0000000000000000000000000000000000000000..8f9be4de36f314d1bca93d2dffdaf56851efe0ec GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?$S2Ka=y{{R1f`i6tlME0lN0}Yoh z3GxeOP&2fTC@8Gx?wLG$#p<26AH00~;oJ8gKY#uG*R*(rB~V$Yr;B4q#jUqzt}`|$ zh_E;wkTF8)=(Zhx_d3ttvId&Sz_2B*e?7O-Glk%jAfBhIpUXO@geCxv^N0BW literal 0 HcmV?d00001 From 4f04246517f7785a37df436fc22f1c4701aaad5b Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Fri, 26 Dec 2014 01:54:17 +0100 Subject: [PATCH 004/481] cleaning --- .../background1024x600.png | Bin 2852 -> 0 bytes .../btn_beatgrid1.png | Bin 128 -> 0 bytes .../btn_beatgrid1_over.png | Bin 136 -> 0 bytes .../btn_beatgrid2.png | Bin 128 -> 0 bytes .../btn_beatgrid2_over.png | Bin 136 -> 0 bytes .../btn_beatloop1_0125.png | Bin 132 -> 0 bytes .../btn_beatloop1_0125_over.png | Bin 132 -> 0 bytes .../btn_beatloop1_0250.png | Bin 134 -> 0 bytes .../btn_beatloop1_0250_over.png | Bin 134 -> 0 bytes .../btn_beatloop1_0500.png | Bin 138 -> 0 bytes .../btn_beatloop1_0500_over.png | Bin 138 -> 0 bytes .../btn_beatloop1_1.png | Bin 115 -> 0 bytes .../btn_beatloop1_16.png | Bin 122 -> 0 bytes .../btn_beatloop1_16_over.png | Bin 122 -> 0 bytes .../btn_beatloop1_1_over.png | Bin 115 -> 0 bytes .../btn_beatloop1_2.png | Bin 121 -> 0 bytes .../btn_beatloop1_2_over.png | Bin 121 -> 0 bytes .../btn_beatloop1_4.png | Bin 120 -> 0 bytes .../btn_beatloop1_4_over.png | Bin 120 -> 0 bytes .../btn_beatloop1_8.png | Bin 117 -> 0 bytes .../btn_beatloop1_8_over.png | Bin 117 -> 0 bytes .../btn_beatloop1_double.png | Bin 138 -> 0 bytes .../btn_beatloop1_double_over.png | Bin 138 -> 0 bytes .../btn_beatloop1_halve.png | Bin 130 -> 0 bytes .../btn_beatloop1_halve_over.png | Bin 130 -> 0 bytes .../btn_beatloop2_0125.png | Bin 132 -> 0 bytes .../btn_beatloop2_0125_over.png | Bin 132 -> 0 bytes .../btn_beatloop2_0250.png | Bin 134 -> 0 bytes .../btn_beatloop2_0250_over.png | Bin 134 -> 0 bytes .../btn_beatloop2_0500.png | Bin 138 -> 0 bytes .../btn_beatloop2_0500_over.png | Bin 138 -> 0 bytes .../btn_beatloop2_1.png | Bin 115 -> 0 bytes .../btn_beatloop2_16.png | Bin 122 -> 0 bytes .../btn_beatloop2_16_over.png | Bin 122 -> 0 bytes .../btn_beatloop2_1_over.png | Bin 115 -> 0 bytes .../btn_beatloop2_2.png | Bin 121 -> 0 bytes .../btn_beatloop2_2_over.png | Bin 121 -> 0 bytes .../btn_beatloop2_4.png | Bin 120 -> 0 bytes .../btn_beatloop2_4_over.png | Bin 120 -> 0 bytes .../btn_beatloop2_8.png | Bin 117 -> 0 bytes .../btn_beatloop2_8_over.png | Bin 117 -> 0 bytes .../btn_beatloop2_double.png | Bin 138 -> 0 bytes .../btn_beatloop2_double_over.png | Bin 138 -> 0 bytes .../btn_beatloop2_halve.png | Bin 130 -> 0 bytes .../btn_beatloop2_halve_over.png | Bin 130 -> 0 bytes .../btn_clipping1.png | Bin 89 -> 0 bytes .../btn_clipping1_over.png | Bin 85 -> 0 bytes .../btn_clipping2.png | Bin 89 -> 0 bytes .../btn_clipping2_over.png | Bin 85 -> 0 bytes .../btn_clipping_master.png | Bin 101 -> 0 bytes .../btn_clipping_master_over.png | Bin 100 -> 0 bytes .../btn_clipping_microphone.png | Bin 87 -> 0 bytes .../btn_clipping_microphone_over.png | Bin 84 -> 0 bytes .../btn_clipping_sampler.png | Bin 89 -> 0 bytes .../btn_clipping_sampler_over.png | Bin 85 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_cue1.png | Bin 134 -> 0 bytes .../btn_cue1_over.png | Bin 134 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_cue2.png | Bin 134 -> 0 bytes .../btn_cue2_over.png | Bin 134 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_eject1.png | Bin 188 -> 0 bytes .../btn_eject1_over.png | Bin 188 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_eject2.png | Bin 188 -> 0 bytes .../btn_eject2_over.png | Bin 188 -> 0 bytes .../btn_eject_sampler.png | Bin 190 -> 0 bytes .../btn_eject_sampler_over.png | Bin 190 -> 0 bytes .../btn_forward1.png | Bin 164 -> 0 bytes .../btn_forward1_over.png | Bin 164 -> 0 bytes .../btn_forward2.png | Bin 164 -> 0 bytes .../btn_forward2_over.png | Bin 164 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_fx1.png | Bin 128 -> 0 bytes .../btn_fx1_over.png | Bin 128 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_fx2.png | Bin 128 -> 0 bytes .../btn_fx2_over.png | Bin 128 -> 0 bytes .../btn_hotcue1_1.png | Bin 115 -> 0 bytes .../btn_hotcue1_1_over.png | Bin 115 -> 0 bytes .../btn_hotcue1_2.png | Bin 121 -> 0 bytes .../btn_hotcue1_2_over.png | Bin 121 -> 0 bytes .../btn_hotcue1_3.png | Bin 118 -> 0 bytes .../btn_hotcue1_3_over.png | Bin 118 -> 0 bytes .../btn_hotcue1_4.png | Bin 120 -> 0 bytes .../btn_hotcue1_4_over.png | Bin 120 -> 0 bytes .../btn_hotcue2_1.png | Bin 115 -> 0 bytes .../btn_hotcue2_1_over.png | Bin 115 -> 0 bytes .../btn_hotcue2_2.png | Bin 121 -> 0 bytes .../btn_hotcue2_2_over.png | Bin 121 -> 0 bytes .../btn_hotcue2_3.png | Bin 118 -> 0 bytes .../btn_hotcue2_3_over.png | Bin 118 -> 0 bytes .../btn_hotcue2_4.png | Bin 120 -> 0 bytes .../btn_hotcue2_4_over.png | Bin 120 -> 0 bytes .../btn_keylock1.png | Bin 146 -> 0 bytes .../btn_keylock1_over.png | Bin 146 -> 0 bytes .../btn_keylock2.png | Bin 146 -> 0 bytes .../btn_keylock2_over.png | Bin 146 -> 0 bytes .../btn_keylock_sampler.png | Bin 156 -> 0 bytes .../btn_keylock_sampler_over.png | Bin 156 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_kill.png | Bin 102 -> 0 bytes .../btn_kill_over.png | Bin 102 -> 0 bytes .../btn_loop_in1.png | Bin 116 -> 0 bytes .../btn_loop_in1_over.png | Bin 116 -> 0 bytes .../btn_loop_in2.png | Bin 116 -> 0 bytes .../btn_loop_in2_over.png | Bin 116 -> 0 bytes .../btn_loop_out1.png | Bin 115 -> 0 bytes .../btn_loop_out1_over.png | Bin 115 -> 0 bytes .../btn_loop_out2.png | Bin 115 -> 0 bytes .../btn_loop_out2_over.png | Bin 115 -> 0 bytes .../btn_microphone_talkover.png | Bin 136 -> 0 bytes .../btn_microphone_talkover_over.png | Bin 136 -> 0 bytes .../btn_nudge_down1.png | Bin 165 -> 0 bytes .../btn_nudge_down1_over.png | Bin 165 -> 0 bytes .../btn_nudge_down2.png | Bin 165 -> 0 bytes .../btn_nudge_down2_over.png | Bin 165 -> 0 bytes .../btn_nudge_up1.png | Bin 167 -> 0 bytes .../btn_nudge_up1_over.png | Bin 167 -> 0 bytes .../btn_nudge_up2.png | Bin 167 -> 0 bytes .../btn_nudge_up2_over.png | Bin 167 -> 0 bytes .../btn_orientation_microphone_left_over.png | Bin 144 -> 0 bytes .../btn_orientation_microphone_master.png | Bin 184 -> 0 bytes .../btn_orientation_microphone_right_over.png | Bin 185 -> 0 bytes .../btn_orientation_sampler_left_over.png | Bin 144 -> 0 bytes .../btn_orientation_sampler_master.png | Bin 192 -> 0 bytes .../btn_orientation_sampler_right_over.png | Bin 185 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_pfl1.png | Bin 235 -> 0 bytes .../btn_pfl1_over.png | Bin 235 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_pfl2.png | Bin 235 -> 0 bytes .../btn_pfl2_over.png | Bin 235 -> 0 bytes .../btn_pfl_sampler.png | Bin 235 -> 0 bytes .../btn_pfl_sampler_over.png | Bin 235 -> 0 bytes .../btn_pitch_down1.png | Bin 126 -> 0 bytes .../btn_pitch_down1_over.png | Bin 126 -> 0 bytes .../btn_pitch_down2.png | Bin 126 -> 0 bytes .../btn_pitch_down2_over.png | Bin 126 -> 0 bytes .../btn_pitch_up1.png | Bin 135 -> 0 bytes .../btn_pitch_up1_over.png | Bin 135 -> 0 bytes .../btn_pitch_up2.png | Bin 135 -> 0 bytes .../btn_pitch_up2_over.png | Bin 135 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_play1.png | Bin 183 -> 0 bytes .../btn_play1_over.png | Bin 183 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_play2.png | Bin 183 -> 0 bytes .../btn_play2_over.png | Bin 183 -> 0 bytes .../btn_play_sampler.png | Bin 157 -> 0 bytes .../btn_play_sampler_over.png | Bin 157 -> 0 bytes .../btn_quantize1.png | Bin 174 -> 0 bytes .../btn_quantize1_over.png | Bin 174 -> 0 bytes .../btn_quantize2.png | Bin 174 -> 0 bytes .../btn_quantize2_over.png | Bin 174 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_reloop1.png | Bin 129 -> 0 bytes .../btn_reloop1_over.png | Bin 129 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_reloop2.png | Bin 129 -> 0 bytes .../btn_reloop2_over.png | Bin 129 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_repeat1.png | Bin 214 -> 0 bytes .../btn_repeat1_over.png | Bin 214 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_repeat2.png | Bin 214 -> 0 bytes .../btn_repeat2_over.png | Bin 214 -> 0 bytes .../btn_repeat_sampler.png | Bin 212 -> 0 bytes .../btn_repeat_sampler_over.png | Bin 212 -> 0 bytes .../btn_reverse1.png | Bin 138 -> 0 bytes .../btn_reverse1_over.png | Bin 138 -> 0 bytes .../btn_reverse2.png | Bin 138 -> 0 bytes .../btn_reverse2_over.png | Bin 138 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_rewind1.png | Bin 182 -> 0 bytes .../btn_rewind1_over.png | Bin 182 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_rewind2.png | Bin 182 -> 0 bytes .../btn_rewind2_over.png | Bin 182 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_spinny1.png | Bin 218 -> 0 bytes .../btn_spinny1_over.png | Bin 288 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_spinny2.png | Bin 218 -> 0 bytes .../btn_spinny2_over.png | Bin 288 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_sync1.png | Bin 133 -> 0 bytes .../btn_sync1_over.png | Bin 133 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_sync2.png | Bin 133 -> 0 bytes .../btn_sync2_over.png | Bin 133 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_tap1.png | Bin 109 -> 0 bytes .../btn_tap1_over.png | Bin 187 -> 0 bytes .../ShadeDark1024x600-Netbook/btn_tap2.png | Bin 109 -> 0 bytes .../btn_tap2_over.png | Bin 187 -> 0 bytes .../btn_tap_sampler.png | Bin 108 -> 0 bytes .../btn_tap_sampler_over.png | Bin 130 -> 0 bytes .../btn_vinylcontrol_cueing_hot1.png | Bin 198 -> 0 bytes .../btn_vinylcontrol_cueing_hot2.png | Bin 198 -> 0 bytes .../btn_vinylcontrol_cueing_off1.png | Bin 204 -> 0 bytes .../btn_vinylcontrol_cueing_off2.png | Bin 204 -> 0 bytes .../btn_vinylcontrol_cueing_one1.png | Bin 204 -> 0 bytes .../btn_vinylcontrol_cueing_one2.png | Bin 204 -> 0 bytes ...btn_vinylcontrol_indicator_horizontal1.png | Bin 95 -> 0 bytes ...btn_vinylcontrol_indicator_horizontal2.png | Bin 95 -> 0 bytes ...btn_vinylcontrol_indicator_horizontal3.png | Bin 95 -> 0 bytes .../btn_vinylcontrol_indicator_vertical1.png | Bin 94 -> 0 bytes .../btn_vinylcontrol_indicator_vertical2.png | Bin 94 -> 0 bytes .../btn_vinylcontrol_indicator_vertical3.png | Bin 94 -> 0 bytes .../btn_vinylcontrol_mode_abs1.png | Bin 217 -> 0 bytes .../btn_vinylcontrol_mode_abs2.png | Bin 217 -> 0 bytes .../btn_vinylcontrol_mode_const1.png | Bin 214 -> 0 bytes .../btn_vinylcontrol_mode_const2.png | Bin 214 -> 0 bytes .../btn_vinylcontrol_mode_rel1.png | Bin 219 -> 0 bytes .../btn_vinylcontrol_mode_rel2.png | Bin 219 -> 0 bytes .../btn_volume_display1.png | Bin 94 -> 0 bytes .../btn_volume_display1_over.png | Bin 107 -> 0 bytes .../btn_volume_display2.png | Bin 94 -> 0 bytes .../btn_volume_display2_over.png | Bin 107 -> 0 bytes .../btn_volume_display_master1.png | Bin 94 -> 0 bytes .../btn_volume_display_master1_over.png | Bin 107 -> 0 bytes .../btn_volume_display_master2.png | Bin 94 -> 0 bytes .../btn_volume_display_master2_over.png | Bin 107 -> 0 bytes .../btn_volume_display_microphone.png | Bin 93 -> 0 bytes .../btn_volume_display_microphone_over.png | Bin 101 -> 0 bytes .../btn_volume_display_sampler.png | Bin 109 -> 0 bytes .../btn_volume_display_sampler_over.png | Bin 107 -> 0 bytes .../knob_crossfader.png | Bin 214 -> 0 bytes .../ShadeDark1024x600-Netbook/knob_pitch1.png | Bin 183 -> 0 bytes .../ShadeDark1024x600-Netbook/knob_pitch2.png | Bin 183 -> 0 bytes .../knob_pitch_sampler.png | Bin 183 -> 0 bytes .../knob_volume1.png | Bin 183 -> 0 bytes .../knob_volume2.png | Bin 183 -> 0 bytes .../knobs/knob_rotary_s0.png | Bin 393 -> 0 bytes .../knobs/knob_rotary_s1.png | Bin 418 -> 0 bytes .../knobs/knob_rotary_s10.png | Bin 457 -> 0 bytes .../knobs/knob_rotary_s11.png | Bin 448 -> 0 bytes .../knobs/knob_rotary_s12.png | Bin 455 -> 0 bytes .../knobs/knob_rotary_s13.png | Bin 445 -> 0 bytes .../knobs/knob_rotary_s14.png | Bin 453 -> 0 bytes .../knobs/knob_rotary_s15.png | Bin 435 -> 0 bytes .../knobs/knob_rotary_s16.png | Bin 420 -> 0 bytes .../knobs/knob_rotary_s17.png | Bin 423 -> 0 bytes .../knobs/knob_rotary_s18.png | Bin 416 -> 0 bytes .../knobs/knob_rotary_s19.png | Bin 410 -> 0 bytes .../knobs/knob_rotary_s2.png | Bin 454 -> 0 bytes .../knobs/knob_rotary_s20.png | Bin 410 -> 0 bytes .../knobs/knob_rotary_s21.png | Bin 396 -> 0 bytes .../knobs/knob_rotary_s22.png | Bin 403 -> 0 bytes .../knobs/knob_rotary_s23.png | Bin 390 -> 0 bytes .../knobs/knob_rotary_s24.png | Bin 399 -> 0 bytes .../knobs/knob_rotary_s25.png | Bin 390 -> 0 bytes .../knobs/knob_rotary_s26.png | Bin 395 -> 0 bytes .../knobs/knob_rotary_s27.png | Bin 382 -> 0 bytes .../knobs/knob_rotary_s28.png | Bin 377 -> 0 bytes .../knobs/knob_rotary_s29.png | Bin 367 -> 0 bytes .../knobs/knob_rotary_s3.png | Bin 457 -> 0 bytes .../knobs/knob_rotary_s30.png | Bin 362 -> 0 bytes .../knobs/knob_rotary_s31.png | Bin 328 -> 0 bytes .../knobs/knob_rotary_s32.png | Bin 344 -> 0 bytes .../knobs/knob_rotary_s33.png | Bin 369 -> 0 bytes .../knobs/knob_rotary_s34.png | Bin 378 -> 0 bytes .../knobs/knob_rotary_s35.png | Bin 384 -> 0 bytes .../knobs/knob_rotary_s36.png | Bin 401 -> 0 bytes .../knobs/knob_rotary_s37.png | Bin 409 -> 0 bytes .../knobs/knob_rotary_s38.png | Bin 403 -> 0 bytes .../knobs/knob_rotary_s39.png | Bin 409 -> 0 bytes .../knobs/knob_rotary_s4.png | Bin 451 -> 0 bytes .../knobs/knob_rotary_s40.png | Bin 433 -> 0 bytes .../knobs/knob_rotary_s41.png | Bin 425 -> 0 bytes .../knobs/knob_rotary_s42.png | Bin 432 -> 0 bytes .../knobs/knob_rotary_s43.png | Bin 428 -> 0 bytes .../knobs/knob_rotary_s44.png | Bin 442 -> 0 bytes .../knobs/knob_rotary_s45.png | Bin 422 -> 0 bytes .../knobs/knob_rotary_s46.png | Bin 416 -> 0 bytes .../knobs/knob_rotary_s47.png | Bin 428 -> 0 bytes .../knobs/knob_rotary_s48.png | Bin 434 -> 0 bytes .../knobs/knob_rotary_s49.png | Bin 435 -> 0 bytes .../knobs/knob_rotary_s5.png | Bin 463 -> 0 bytes .../knobs/knob_rotary_s50.png | Bin 435 -> 0 bytes .../knobs/knob_rotary_s51.png | Bin 439 -> 0 bytes .../knobs/knob_rotary_s52.png | Bin 446 -> 0 bytes .../knobs/knob_rotary_s53.png | Bin 476 -> 0 bytes .../knobs/knob_rotary_s54.png | Bin 469 -> 0 bytes .../knobs/knob_rotary_s55.png | Bin 463 -> 0 bytes .../knobs/knob_rotary_s56.png | Bin 478 -> 0 bytes .../knobs/knob_rotary_s57.png | Bin 481 -> 0 bytes .../knobs/knob_rotary_s58.png | Bin 471 -> 0 bytes .../knobs/knob_rotary_s59.png | Bin 468 -> 0 bytes .../knobs/knob_rotary_s6.png | Bin 460 -> 0 bytes .../knobs/knob_rotary_s60.png | Bin 473 -> 0 bytes .../knobs/knob_rotary_s61.png | Bin 458 -> 0 bytes .../knobs/knob_rotary_s62.png | Bin 467 -> 0 bytes .../knobs/knob_rotary_s63.png | Bin 467 -> 0 bytes .../knobs/knob_rotary_s7.png | Bin 464 -> 0 bytes .../knobs/knob_rotary_s8.png | Bin 468 -> 0 bytes .../knobs/knob_rotary_s9.png | Bin 473 -> 0 bytes res/skins/ShadeDark1024x600-Netbook/skin.xml | 6057 ----------------- .../slider_crossfader.png | Bin 113 -> 0 bytes .../slider_pitch1.png | Bin 119 -> 0 bytes .../slider_pitch2.png | Bin 119 -> 0 bytes .../slider_pitch_sampler.png | Bin 118 -> 0 bytes .../slider_volume1.png | Bin 119 -> 0 bytes .../slider_volume2.png | Bin 119 -> 0 bytes .../style/style_bg_microphone.png | Bin 139 -> 0 bytes .../style/style_bg_sampler.png | Bin 228 -> 0 bytes .../style/style_bg_waveform.png | Bin 15109 -> 0 bytes .../style/style_bg_woverview.png | Bin 183 -> 0 bytes .../style/style_branch_closed.png | Bin 138 -> 0 bytes .../style/style_branch_open.png | Bin 158 -> 0 bytes .../style/style_checkbox_checked.png | Bin 298 -> 0 bytes .../style/style_checkbox_unchecked.png | Bin 117 -> 0 bytes .../style/style_handle_checked.png | Bin 92 -> 0 bytes .../style/style_handle_unchecked.png | Bin 93 -> 0 bytes .../tab_microphone.png | Bin 142 -> 0 bytes .../tab_microphone_over.png | Bin 142 -> 0 bytes .../ShadeDark1024x600-Netbook/tab_sampler.png | Bin 147 -> 0 bytes .../tab_sampler_over.png | Bin 147 -> 0 bytes .../tab_vinylcontrol.png | Bin 146 -> 0 bytes .../tab_vinylcontrol_over.png | Bin 146 -> 0 bytes .../vinyl_spinny1_background.png | Bin 1568 -> 0 bytes .../vinyl_spinny1_foreground.png | Bin 299 -> 0 bytes .../vinyl_spinny1_foreground_ghost.png | Bin 274 -> 0 bytes .../vinyl_spinny2_background.png | Bin 1568 -> 0 bytes .../vinyl_spinny2_foreground.png | Bin 299 -> 0 bytes .../vinyl_spinny2_foreground_ghost.png | Bin 274 -> 0 bytes 306 files changed, 6057 deletions(-) delete mode 100644 res/skins/ShadeDark1024x600-Netbook/background1024x600.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatgrid1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatgrid1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatgrid2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatgrid2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0250.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0250_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0500.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0500_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_16.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_16_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_4.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_4_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_8.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_8_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_double.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_double_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_halve.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_halve_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0125.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0125_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0250.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0250_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0500.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0500_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_16.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_16_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_4.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_4_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_8.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_8_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_double.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_double_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_halve.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_halve_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_master.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_master_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_microphone.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_microphone_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_clipping_sampler_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_cue1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_cue1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_cue2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_cue2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_eject_sampler_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_forward1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_forward1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_forward2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_forward2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_fx1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_fx1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_fx2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_fx2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_4.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_4_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_4.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_4_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_keylock_sampler_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_kill.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_kill_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_in1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_in1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_in2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_in2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_out1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_out1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_out2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_loop_out2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_up1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_up1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_up2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_nudge_up2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_left_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_master.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_right_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_left_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_master.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_right_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pfl_sampler_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_down1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_down1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_up1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_up1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_up2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_pitch_up2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_play_sampler_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_quantize1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_quantize1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_quantize2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_quantize2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reloop1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reloop1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reloop2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reloop2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_repeat_sampler_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reverse1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reverse1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reverse2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_reverse2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_rewind1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_rewind1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_rewind2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_rewind2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_spinny1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_spinny1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_spinny2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_spinny2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_sync1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_sync1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_sync2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_sync2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_tap_sampler_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal3.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical3.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_abs1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_abs2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_rel1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_rel2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master1_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master2_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_microphone.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_microphone_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/btn_volume_display_sampler_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_crossfader.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_pitch1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_pitch2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_pitch_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_volume1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knob_volume2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s10.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s11.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s12.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s16.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s19.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s21.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s22.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s28.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s35.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s37.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s38.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s39.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s40.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s47.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s48.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s50.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s53.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s58.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s6.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s8.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s9.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/skin.xml delete mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_crossfader.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_pitch1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_pitch2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_pitch_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_volume1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/slider_volume2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_woverview.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_open.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_unchecked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_microphone.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_microphone_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_sampler_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol_over.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_background.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground_ghost.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_background.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground_ghost.png diff --git a/res/skins/ShadeDark1024x600-Netbook/background1024x600.png b/res/skins/ShadeDark1024x600-Netbook/background1024x600.png deleted file mode 100644 index 78759a7c213e83c958129eb63438b9fad0a3c76c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2852 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 X{w8DR{%CD53xW diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatgrid2.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatgrid2.png deleted file mode 100644 index 2264a2c82179792aa417758945eaebc26ebb4dd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q876lZ`>h-+9}c0@!3khG4N%>)!= zDhcunW@bLJ{k$WPr{d}27*cWT>iLC&3=BMn98Udne!S{Y)M0rozJ-kX7w>&N^1EC} X{w8DR{%CD53xW diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125.png deleted file mode 100644 index 388e1349c8ea5a353aa0a187477a9442d261da02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSkK5lEG|3c z^;$`wkb$R*V@SoVwdV}^7#LWb15f=uuj8TP(`&!4WW}AIulRf27&@cQZZb?%-1z6m cO~VSYOJ7(P39}_!0P1D%boFyt=akR{025UwT>t<8 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0125_over.png deleted file mode 100644 index 5bedf7c49c38ad0b8865a69e054b94143342bb59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSShW|{`jwZ zzUkpWAp=hr$B>F!YtI?-F)*+=2cG(SUdKbnr`LX8$%;EaU-9?4F?2?q-DH@kxbe@A cn}!u)m%gwp5@t)d0MyIi>FVdQ&MBb@0Bva~fdBvi diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0250.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_0250.png deleted file mode 100644 index b0731fd38d2497742045d1e7b316466078e7cdc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSkK5lEG|3c z^;$`wkddd0V@SoVwdV}^7#LVs9Q*$7m+WoR*wC3)#QN&;CHYfQj4Vd0F!YtI?-F)*;OIQISDFWK9sv7s}qi1pRwOY*0r7+H)~$Lpzo;c{c1 e_y3I8jkL$lSayjCNmu}NGkCiCxvXF!YcCk`F*vZe1RlHh|4hV5vt>#3KaE-i?q&XUmR4?X*}FG2@!AAE iW~E&*_jdjlI?H%Rj%8i5+N>;~2@IaDelF{r5}E*P8Z+&Yv-GzwDhKVUCxRwA?=j RDuDVJJYD@<);T3K0RZLPBuxMS diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_16_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_16_over.png deleted file mode 100644 index af0b15f0088748ade7c21e64a1170f542098115b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAD zr;B4q#jUgF4S{+&oG!lk*UmRbY3WV#x||hz!~_K$n!io@IIrHjSLDMo(_d^%X}Nz6 RQ~>occ)I$ztaD0e0sucgC5Zq4 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_1_over.png deleted file mode 100644 index 0b52819af99b98b5b7fe6dd490ce9615bef95148..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jUgFHwrQ^a2#@Y{Lk6A;bqPbBlp8QY@?JEPM7QKT*<^Ug{h7yJarvV1B0il KpUXO@geCwf`XIId diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2.png deleted file mode 100644 index e26ab62a43033219b2d0ab813771f1d8846dc929..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dC4 z)5S5Q;?~)IL!e#`rq%!LtvHla3fD^e3!3=p%=6H9C|fW~`RcR3zFtf$x=h8>CWOcX PwJ~_Q`njxgN@xNA^F1Bf diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_2_over.png deleted file mode 100644 index d7e809610aabeead06174d159cfeb1b4909e43b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAr zr;B4q#jUgbhCsa>OsoIfTX86<6t0!_7c}wHndhPJP_|%}^3`X5eZ81ibeW2$O$d<( PYGd$p^>bP0l+XkKOJg4v diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_4.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_4.png deleted file mode 100644 index 6ff8e699b854fc558ea231bfa7f8d2978f7a466d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB< z)5S5Q;?~*ohI|YREG&-C|M!bUXEK@-ZlIusELo`agf$PCg-nZ8?^#;Tt~rWqc4+R0`rw^#0K# kxRdMc->;Q-?3cw}IKwsd{^GiKKob}|UHx3vIVCg!07Hr^kpKVy diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_double_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_double_over.png deleted file mode 100644 index 5e7f0d27b4c6a91265798a2ccdfdd565035827b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Lk!3-psHvjYiQv3lvA+A80on2U?e3seDYxc)~ z1^nMG2NW{%ba4!+xb^m;As>SR2aDtRumAJ6?c@_O*p_3-8ouF^U&aSPMWrC_MDHI> kf;+k1{{32c$9`Gtg)>}J?=P-<2Q-1f)78&qol`;+00sdq*8l(j diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_halve.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop1_halve.png deleted file mode 100644 index 3867c2ac1f42471b1f5e96fcf753270f05707d52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Lk!3-psHvjYiQbGYfA+A80on2VZ$lfa;HY7SD zEH3+Tc9+Jt<8 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0125_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0125_over.png deleted file mode 100644 index 5bedf7c49c38ad0b8865a69e054b94143342bb59..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 132 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSShW|{`jwZ zzUkpWAp=hr$B>F!YtI?-F)*+=2cG(SUdKbnr`LX8$%;EaU-9?4F?2?q-DH@kxbe@A cn}!u)m%gwp5@t)d0MyIi>FVdQ&MBb@0Bva~fdBvi diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0250.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_0250.png deleted file mode 100644 index b0731fd38d2497742045d1e7b316466078e7cdc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcNO_-fsSkK5lEG|3c z^;$`wkddd0V@SoVwdV}^7#LVs9Q*$7m+WoR*wC3)#QN&;CHYfQj4Vd0F!YtI?-F)*;OIQISDFWK9sv7s}qi1pRwOY*0r7+H)~$Lpzo;c{c1 e_y3I8jkL$lSayjCNmu}NGkCiCxvXF!YcCk`F*vZe1RlHh|4hV5vt>#3KaE-i?q&XUmR4?X*}FG2@!AAE iW~E&*_jdjlI?H%Rj%8i5+N>;~2@IaDelF{r5}E*P8Z+&Yv-GzwDhKVUCxRwA?=j RDuDVJJYD@<);T3K0RZLPBuxMS diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_16_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_16_over.png deleted file mode 100644 index af0b15f0088748ade7c21e64a1170f542098115b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 122 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAD zr;B4q#jUgF4S{+&oG!lk*UmRbY3WV#x||hz!~_K$n!io@IIrHjSLDMo(_d^%X}Nz6 RQ~>occ)I$ztaD0e0sucgC5Zq4 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_1_over.png deleted file mode 100644 index 0b52819af99b98b5b7fe6dd490ce9615bef95148..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jUgFHwrQ^a2#@Y{Lk6A;bqPbBlp8QY@?JEPM7QKT*<^Ug{h7yJarvV1B0il KpUXO@geCwf`XIId diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2.png deleted file mode 100644 index e26ab62a43033219b2d0ab813771f1d8846dc929..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dC4 z)5S5Q;?~)IL!e#`rq%!LtvHla3fD^e3!3=p%=6H9C|fW~`RcR3zFtf$x=h8>CWOcX PwJ~_Q`njxgN@xNA^F1Bf diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_2_over.png deleted file mode 100644 index d7e809610aabeead06174d159cfeb1b4909e43b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAr zr;B4q#jUgbhCsa>OsoIfTX86<6t0!_7c}wHndhPJP_|%}^3`X5eZ81ibeW2$O$d<( PYGd$p^>bP0l+XkKOJg4v diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_4.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_4.png deleted file mode 100644 index 6ff8e699b854fc558ea231bfa7f8d2978f7a466d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB< z)5S5Q;?~*ohI|YREG&-C|M!bUXEK@-ZlIusELo`agf$PCg-nZ8?^#;Tt~rWqc4+R0`rw^#0K# kxRdMc->;Q-?3cw}IKwsd{^GiKKob}|UHx3vIVCg!07Hr^kpKVy diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_double_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_double_over.png deleted file mode 100644 index 5e7f0d27b4c6a91265798a2ccdfdd565035827b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Lk!3-psHvjYiQv3lvA+A80on2U?e3seDYxc)~ z1^nMG2NW{%ba4!+xb^m;As>SR2aDtRumAJ6?c@_O*p_3-8ouF^U&aSPMWrC_MDHI> kf;+k1{{32c$9`Gtg)>}J?=P-<2Q-1f)78&qol`;+00sdq*8l(j diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_halve.png b/res/skins/ShadeDark1024x600-Netbook/btn_beatloop2_halve.png deleted file mode 100644 index 3867c2ac1f42471b1f5e96fcf753270f05707d52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^B0#Lk!3-psHvjYiQbGYfA+A80on2VZ$lfa;HY7SD zEH3+Tc9+Ju6{1-oD!Mvd7(8A5T-G@yGywo2))r&{ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_clipping2.png b/res/skins/ShadeDark1024x600-Netbook/btn_clipping2.png deleted file mode 100644 index b56ed2efa593a310a82f382ea9c0dae92179fc39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^tUxTp!3HEJ?-4l$q!c_|978H@y*;7G$iTqEWRSJM n;LG|Gf0-8u7&AYuIqf7jm#6aos_XhpK(!2>u6{1-oD!Mvd7(8A5T-G@yGywo2))r&{ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_clipping_master.png b/res/skins/ShadeDark1024x600-Netbook/btn_clipping_master.png deleted file mode 100644 index fea64ce9411c5ae2b59d0ce9dae4fa5f5240860e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^+(0bE!3-pKt-j(8q}T#{LR=-~RqUNS*QqVp0Thw) xba4!+xOMlKBPWp8eBkE(EeSn8En+76>vI$^2;c5nJQpa=;OXk;vd$@?2>^*Q8_NIy diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_clipping_master_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_clipping_master_over.png deleted file mode 100644 index f03c9615a756088b95b7fda0af62f7f86801cb73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^+(0bE!3-pKt-j(8q}T#{LR{^gJpVH=X#G(+0u+(- wba4!+xOMl4Auo{Eyy4+}lY_^qN}l91`kY|c`trU)3s9KB)78&qol`;+0HE9*8vpF84@|q2PO@Ai3 gLA=;`Lq!MkQ=a6xrx&LF14=M>y85}Sb4q9e00}u4F#rGn diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_clipping_sampler.png b/res/skins/ShadeDark1024x600-Netbook/btn_clipping_sampler.png deleted file mode 100644 index b56ed2efa593a310a82f382ea9c0dae92179fc39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89 zcmeAS@N?(olHy`uVBq!ia0vp^tUxTp!3HEJ?-4l$q!c_|978H@y*;7G$iTqEWRSJM n;LG|Gf0-8u7&AYuIqf7jm#6aos_XhpK(!2>u6{1-oD!Mvd7(8A5T-G@yGywo2))r&{ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_cue1.png b/res/skins/ShadeDark1024x600-Netbook/btn_cue1.png deleted file mode 100644 index 068cba05607c65ea9cd3be8b50c1e5625869a2d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z;pyTSQgQ3;L_;nH0}kff|NdY6>Lp{vp&4sC!Nnp*hF_=OY;tACf~?g(tc%YYYyLNX f*!NFu*;%$_OIRJB=2Ywln!(`d>gTe~DWM4fqb?|t diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_cue1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_cue1_over.png deleted file mode 100644 index 845bebb43e864ab65d558f44c9ecd420a0b11ee0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zg{O;SNX4zU6Aif-3^OBQk(%kA+A80on1IAF8fDwx*(8a z;pyTSQgQ3;L_;nH0}kff|NdY6>Lp{vp&4sC!Nnp*hF_=OY;tACf~?g(tc%YYYyLNX f*!NFu*;%$_OIRJB=2Ywln!(`d>gTe~DWM4fqb?|t diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_cue2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_cue2_over.png deleted file mode 100644 index 845bebb43e864ab65d558f44c9ecd420a0b11ee0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zg{O;SNX4zU6Aif-3^4PO2y2|a;)chH(Dgu32TvErkcwMd&s*~`C~&wqx(eR> zEgzAr@gZDtdCkorfB$?F$7Rf} U_uUpR2Aaj->FVdQ&MBb@02f$5F#rGn diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_eject1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_eject1_over.png deleted file mode 100644 index 426c05237f2bf4dd19c2e41f5158ae0ccd781907..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESi0l9V|05zILLVlDzD*AO zoD%vaCGNRC70LSOmnvRA-(n8My5^2n);rfkl57E;;h#t_3?DdpP4PO2y2|a;)chH(Dgu32TvErkcwMd&s*~`C~&wqx(eR> zEgzAr@gZDtdCkorfB$?F$7Rf} U_uUpR2Aaj->FVdQ&MBb@02f$5F#rGn diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_eject2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_eject2_over.png deleted file mode 100644 index 426c05237f2bf4dd19c2e41f5158ae0ccd781907..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESi0l9V|05zILLVlDzD*AO zoD%vaCGNRC70LSOmnvRA-(n8My5^2n);rfkl57E;;h#t_3?DdpP}uk1U;Z_Q zA{rZw_7!b9tF}nwSGSfR>jPoywA|xupF04qm zW1ZoQLuM+De7zB7+0v%g^$KQzPT{9);W0(D8 zCCz&~zNtwq(BRVWVij4+&^3wCmDO%r>(9^oER)$Dy2mH);FK)je8CcW>k!Z|22WQ% Jmvv4FO#m5{Gi(3= diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_forward2.png b/res/skins/ShadeDark1024x600-Netbook/btn_forward2.png deleted file mode 100644 index f0ca1f2604cb5b975e523a0ff7263edf4ea53147..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 164 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaasfUeu0WcdU6_YgQc2ZNL)+ZO z(a+g4%qt)^AR;v^E}P|UU^Y;Nqo<2wNX4zK=M9Az1bAEm9b10K--zg4GbQI^m;GcV z&3ik(sYxx+;L`A76F04qm zW1ZoQLuM+De7zB7+0v%g^$KQzPT{9);W0(D8 zCCz&~zNtwq(BRVWVij4+&^3wCmDO%r>(9^oER)$Dy2mH);FK)je8CcW>k!Z|22WQ% Jmvv4FO#m5{Gi(3= diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_fx1.png b/res/skins/ShadeDark1024x600-Netbook/btn_fx1.png deleted file mode 100644 index 877eef4f43d8e33fd0215daa390049cbd2b39f33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)Qk(%kA+A80jZHW#E}L)vToxe5 z$kW9!q~g}y3Fbh(9L!h$+uv$>k;LfEui`XGb%P_bevXODm$h$tOy8c6y`@*h?>0Aa YzBTiCi+wwn0Ch8Xy85}Sb4q9e0Fn|V6951J diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_fx1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_fx1_over.png deleted file mode 100644 index 0aed02cd350b512056c1367a48779175954ab675..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)Qk(%kA+A80jZN78_^&*p|5`wf zk*AAeNX4zY6U>2nIhe2hx4+f&B8kzRU&U#X>IO$<{TvgQFKgfQn7%zBdrPm1-)(N< Yd~4?O7W;NC0qSP(boFyt=akR{0P@WyPXGV_ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_fx2.png b/res/skins/ShadeDark1024x600-Netbook/btn_fx2.png deleted file mode 100644 index 877eef4f43d8e33fd0215daa390049cbd2b39f33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)Qk(%kA+A80jZHW#E}L)vToxe5 z$kW9!q~g}y3Fbh(9L!h$+uv$>k;LfEui`XGb%P_bevXODm$h$tOy8c6y`@*h?>0Aa YzBTiCi+wwn0Ch8Xy85}Sb4q9e0Fn|V6951J diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_fx2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_fx2_over.png deleted file mode 100644 index 0aed02cd350b512056c1367a48779175954ab675..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)Qk(%kA+A80jZN78_^&*p|5`wf zk*AAeNX4zY6U>2nIhe2hx4+f&B8kzRU&U#X>IO$<{TvgQFKgfQn7%zBdrPm1-)(N< Yd~4?O7W;NC0qSP(boFyt=akR{0P@WyPXGV_ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1.png deleted file mode 100644 index 4fc6d791bc9f6c5381787bc39ede8504246c12ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB} z)5S5Q;?~*o8wD8{I1V{H{^xAm@G|Fzk^A8twoyt7r^|JAu4Lkw!c@l;p1KaGfx*+& K&t;ucLK6VlJRf=h diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_1_over.png deleted file mode 100644 index 0b52819af99b98b5b7fe6dd490ce9615bef95148..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jUgFHwrQ^a2#@Y{Lk6A;bqPbBlp8QY@?JEPM7QKT*<^Ug{h7yJarvV1B0il KpUXO@geCwf`XIId diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2.png deleted file mode 100644 index e26ab62a43033219b2d0ab813771f1d8846dc929..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dC4 z)5S5Q;?~)IL!e#`rq%!LtvHla3fD^e3!3=p%=6H9C|fW~`RcR3zFtf$x=h8>CWOcX PwJ~_Q`njxgN@xNA^F1Bf diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_2_over.png deleted file mode 100644 index d7e809610aabeead06174d159cfeb1b4909e43b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAr zr;B4q#jUgbhCsa>OsoIfTX86<6t0!_7c}wHndhPJP_|%}^3`X5eZ81ibeW2$O$d<( PYGd$p^>bP0l+XkKOJg4v diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3.png deleted file mode 100644 index 4747f951b9041e9d0d97fc66423b5d3be192f5a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB% z)5S5Q;?~*zjhqY&9EaEZm)G54b|KKXEg-`%*JfhW0v1WH`+N3x^01y@DvkYGp$62$ N;OXk;vd$@?2>=tgAPoQj diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_3_over.png deleted file mode 100644 index d4d9dac6c7bc01a1cf558dc583dcd2a6bc3ec6a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oA5 zr;B4q#jUgb8#x&mI1aD*FR#19>_VV%TR?_kuFb@#1uT+Y_xJ4Yg N!PC{xWt~$(698x%AxQuL diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_4.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue1_4.png deleted file mode 100644 index 6ff8e699b854fc558ea231bfa7f8d2978f7a466d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB< z)5S5Q;?~*ohI|YREG&-C|M!bUXCWOcX PwJ~_Q`njxgN@xNA^F1Bf diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_2_over.png deleted file mode 100644 index d7e809610aabeead06174d159cfeb1b4909e43b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAr zr;B4q#jUgbhCsa>OsoIfTX86<6t0!_7c}wHndhPJP_|%}^3`X5eZ81ibeW2$O$d<( PYGd$p^>bP0l+XkKOJg4v diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3.png deleted file mode 100644 index 4747f951b9041e9d0d97fc66423b5d3be192f5a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB% z)5S5Q;?~*zjhqY&9EaEZm)G54b|KKXEg-`%*JfhW0v1WH`+N3x^01y@DvkYGp$62$ N;OXk;vd$@?2>=tgAPoQj diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_3_over.png deleted file mode 100644 index d4d9dac6c7bc01a1cf558dc583dcd2a6bc3ec6a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oA5 zr;B4q#jUgb8#x&mI1aD*FR#19>_VV%TR?_kuFb@#1uT+Y_xJ4Yg N!PC{xWt~$(698x%AxQuL diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_4.png b/res/skins/ShadeDark1024x600-Netbook/btn_hotcue2_4.png deleted file mode 100644 index 6ff8e699b854fc558ea231bfa7f8d2978f7a466d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB< z)5S5Q;?~*ohI|YREG&-C|M!bUXR;~yioPn-?N*Yk9745_%a^}ILGL>AY;{eRa-P7z%AWZFr|Jxmc_ px{_4C%nn>6dP!>M?LGhFme$Q@;@$JnLma4+!PC{xWt~$(69Af5El~gf diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_keylock1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_keylock1_over.png deleted file mode 100644 index 19583b137790fa10d4db80e84e9880cdb1fdc63a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q876n}tEi0l9V|05zILO-X3eoqPg zk#hUX6mOsuOG%JlFaxvG#U?*-HXvWm)5S5Q;?~yl-ar#sTm$$2T^~6`aN(0_CnfhV rMSST>QvEVJaFOUGshzj?{Eu5&H=l`j&qoh&piTx)S3j3^P6R;~yioPn-?N*Yk9745_%a^}ILGL>AY;{eRa-P7z%AWZFr|Jxmc_ px{_4C%nn>6dP!>M?LGhFme$Q@;@$JnLma4+!PC{xWt~$(69Af5El~gf diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_keylock2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_keylock2_over.png deleted file mode 100644 index 19583b137790fa10d4db80e84e9880cdb1fdc63a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q876n}tEi0l9V|05zILO-X3eoqPg zk#hUX6mOsuOG%JlFaxvG#U?*-HXvWm)5S5Q;?~yl-ar#sTm$$2T^~6`aN(0_CnfhV rMSST>QvEVJaFOUGshzj?{Eu5&H=l`j&qoh&piTx)S3j3^P6^ zZH)uUvX%t-1v4v=~m1_ch6i>IFb3-42m;c4P=kuUiu w)|9+)Wv%OmCK=|5krHO_-aY&MzC4@#buiPii_%vn0`)U^y85}Sb4q9e0JMTJ_y7O^ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_keylock_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_keylock_sampler_over.png deleted file mode 100644 index 95e0941aa2967cac0d969376eaffdcd1c0c5f785..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q7SoB*E?*Z=?jM}R@-=akUzDWN}7 zUd&Ke2FkLQ1o;IsFta&be11Yo11MnP>Eaktack>&M=k~h4ws9kp8X5&Q;gwh;&G8L y`6$+uym4i%>xL#7=82IKX7AoT`~AK=oBee#)3S@wS0)1WGkCiCxvXzmP@XOOa6-mXfUkOd(yZAD8bgTe~DWM4f`y?6D diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_kill_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_kill_over.png deleted file mode 100644 index 9f7ddde8f915e9d6d34baa6c1464a148546e209c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^{2 z)5S5Q;?~g%hI|YR9ETh}{_i(CW8}EVyeubkuY}+iv6nW>UN2$%c$O)3Ldi=9pbiF4 LS3j3^P6jDzd09^8UJ1c3VlQo$y z)5S5Q;?~g%hI|YR9ETh}{_i(CW8}EVyeubkuY}+iv6nW>UN2$%c$O)3Ldi=9pbiF4 LS3j3^P6jDzd09^8UJ1c3VlQo$yYD5ykZJCR1v~kC{h-8W=oX L{an^LB{Ts52)!a7 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_loop_out1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_loop_out1_over.png deleted file mode 100644 index 3ea4e8aa1042de44813a6b9c17aa5ac5b06b3b8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jT?!778*j@Ekg@?Kl5fSMk<|t}e%RtIzDdr!u!QBZ}$cO{Ua}A2W{vH86O( L`njxgN@xNAS!^R) diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_loop_out2.png b/res/skins/ShadeDark1024x600-Netbook/btn_loop_out2.png deleted file mode 100644 index d555fa1920b61ea6566ba69039a5ca085c6ad2f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNO*kwrn{WSI79dB} z)5S5Q;?~g<3k4Y%cn%%d_M88#t9a`}SC?bE)n|6!Q<>YD5ykZJCR1v~kC{h-8W=oX L{an^LB{Ts52)!a7 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_loop_out2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_loop_out2_over.png deleted file mode 100644 index 3ea4e8aa1042de44813a6b9c17aa5ac5b06b3b8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaoB=)|u0WcNP1yeUuRNpwT0oAf zr;B4q#jT?!778*j@Ekg@?Kl5fSMk<|t}e%RtIzDdr!u!QBZ}$cO{Ua}A2W{vH86O( L`njxgN@xNAS!^R) diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover.png b/res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover.png deleted file mode 100644 index 3b91b1947bfcfc5d468d2a1f635dc174af65c984..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z<>}%WQgQ3;L{BaT0}ke^|Lu1z;R*S$=*z2y>JzTg0=8kAe?D<}yM}F2OI3{x74x6v h@BgkTbN|if43p-tnl6isSPwLX!PC{xWt~$(6970lDdYeE diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_microphone_talkover_over.png deleted file mode 100644 index 3d19e06e73aadaa40c8a695ab360479d5d58cb38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zm8XkiNX4zU6Fs>Y3^6f?GfbMpYPu{kVm;6l22WQ%mvv4FO#m#EDZBsx diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1.png deleted file mode 100644 index 957d9343476ba9349d72c0fe5326badf319fa628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU6_+oR9HkoN!3u# z$lk`$&)G99AR;v+IwLGDTdCc^38={4)5S5Q;?~x)mRt-9JgkAH-KYQZ>vYKb_^b@x zmlkwqM^%Te!!&8j`5dQPS1@f~dt^aX%OsC-<*mvqT)&7OR;xY7Jw@%yc6Ojq44$rj JF6*2UngCTLFZKWc diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down1_over.png deleted file mode 100644 index 9b13fbd252b7267c0f85c5e89522d41080868bc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU6_+cQb5H_$}?Fx zy-l}co#Bi_);nL>9{g;7{1=y-h9OXqy{C&~NX4zKXDzuH6nIzzO}kJ3<=5$u_wiX7 zye}>2&W@@MU59DXmh(AIx2|B?zV^t1s+LI}<;q)?SGax=J*-xHj(dvQm+kC8qZmA0 L{an^LB{Ts51P(Mh diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2.png deleted file mode 100644 index 957d9343476ba9349d72c0fe5326badf319fa628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU6_+oR9HkoN!3u# z$lk`$&)G99AR;v+IwLGDTdCc^38={4)5S5Q;?~x)mRt-9JgkAH-KYQZ>vYKb_^b@x zmlkwqM^%Te!!&8j`5dQPS1@f~dt^aX%OsC-<*mvqT)&7OR;xY7Jw@%yc6Ojq44$rj JF6*2UngCTLFZKWc diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_down2_over.png deleted file mode 100644 index 9b13fbd252b7267c0f85c5e89522d41080868bc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 165 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU6_+cQb5H_$}?Fx zy-l}co#Bi_);nL>9{g;7{1=y-h9OXqy{C&~NX4zKXDzuH6nIzzO}kJ3<=5$u_wiX7 zye}>2&W@@MU59DXmh(AIx2|B?zV^t1s+LI}<;q)?SGax=J*-xHj(dvQm+kC8qZmA0 L{an^LB{Ts51P(Mh diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_up1.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_up1.png deleted file mode 100644 index e37f5168806c987923d0174f0db4ad1ad09e7bd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaDgizru0WcdU6_+ol!sSRSVTce zMngl}T+hhf#?jB&Gb|t?H7qXMzWQzmP@SEpi(^Q|t*z$``4|*9oCCEr7JT2ozFGKI z;Qt>^mjWt2zmnx^d&TpR-7J)0p5Sf;jh{wmv|4yLBj0~nyx$|b@e|J#EtU)dl3mEEaktack>&Lp}xt4(C8^jRoKLuWuH< z75M*$)1`ol&#z?p+FtQIWH$?Cm?yYfLF1><8Lbu`&dB#)7Vr0nZv4b^MfuLg`9PBx NJYD@<);T3K0RRb6HB$fp diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_nudge_up2.png b/res/skins/ShadeDark1024x600-Netbook/btn_nudge_up2.png deleted file mode 100644 index e37f5168806c987923d0174f0db4ad1ad09e7bd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaDgizru0WcdU6_+ol!sSRSVTce zMngl}T+hhf#?jB&Gb|t?H7qXMzWQzmP@SEpi(^Q|t*z$``4|*9oCCEr7JT2ozFGKI z;Qt>^mjWt2zmnx^d&TpR-7J)0p5Sf;jh{wmv|4yLBj0~nyx$|b@e|J#EtU)dl3mEEaktack>&Lp}xt4(C8^jRoKLuWuH< z75M*$)1`ol&#z?p+FtQIWH$?Cm?yYfLF1><8Lbu`&dB#)7Vr0nZv4b^MfuLg`9PBx NJYD@<);T3K0RRb6HB$fp diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_left_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_left_over.png deleted file mode 100644 index 3da9d9ba7064ec5b1dfe4e0553f76b337364a92c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 144 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q7SoB*E?*NBLSurrCFZ<9m6r-c3h zA^)iy`+>5oB|(0{%-q_IH_kXM>;(#Fc)B=-RNUHo!V+jI2eU!{xBu~CM;s4^8oSM7 l^q!Re($ih+b*Ae%mdM2nlROuxL;>|Mc)I$ztaD0e0sxbLD? diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_master.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_master.png deleted file mode 100644 index fa1dca05103d559c339061266a44f09a8ae9c130..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESh-*YdM0iYESZqdETvk|I zHYWPT@;@7>n5!hnFPNEIRJ(EdiW~2K=$bD&0~EFKba4!+xV87hX+8!49+!(oOV9kT z->~V^4bL5!v(8LBKlAH+opT;a99$cfM4$9Ud9Snnb&&7NI>BVu6Pu-U0~lwu>N>s$ Pn#17f>gTe~DWM4fXjVU^ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_right_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_microphone_right_over.png deleted file mode 100644 index 8d86397bb3be0164bd670ede3da9a4e88a94e96a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESh-*YdMCh5s(1%GOZ<9m5 zri6S4qaP_DFr=;%*926|RTAVE%*-vSozpn|#=9Q}SQT7>qPCtcjv*Dd_MS21V^H95 z4&0)2;xE5W;^N)=dgqJ$Jag)uVOOEcCx`x7e8&$7Mm$_FU+HoT`_YuzzFlfhW^#uW WFrNN+j-eW85QC?ypUXO@geCwS8A9Ly diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_left_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_left_over.png deleted file mode 100644 index 3da9d9ba7064ec5b1dfe4e0553f76b337364a92c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 144 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q7SoB*E?*NBLSurrCFZ<9m6r-c3h zA^)iy`+>5oB|(0{%-q_IH_kXM>;(#Fc)B=-RNUHo!V+jI2eU!{xBu~CM;s4^8oSM7 l^q!Re($ih+b*Ae%mdM2nlROuxL;>|Mc)I$ztaD0e0sxbLD? diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_master.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_master.png deleted file mode 100644 index d50ce3b272018f65e57e4fe4a5f4c608ccae088b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87luCe4h-(BGgvX?X#b$)XWrf9M zW1@WvA5H+O=Pn8I3ub2H7S(Q?zT(Ea9~)dY$^nHPJY5_^DsFAPaFCBdfQR|Q!sKiJ z|HnT(@<`>P^W=TWPbYm5JewA-Z9VBkliwT{D<=KUHfI*;C@ABkeU&o_}V#hvkK U?yBRjfo3syy85}Sb4q9e08MH_%m4rY diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_right_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_orientation_sampler_right_over.png deleted file mode 100644 index 8d86397bb3be0164bd670ede3da9a4e88a94e96a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lwyESh-*YdMCh5s(1%GOZ<9m5 zri6S4qaP_DFr=;%*926|RTAVE%*-vSozpn|#=9Q}SQT7>qPCtcjv*Dd_MS21V^H95 z4&0)2;xE5W;^N)=dgqJ$Jag)uVOOEcCx`x7e8&$7Mm$_FU+HoT`_YuzzFlfhW^#uW WFrNN+j-eW85QC?ypUXO@geCwS8A9Ly diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pfl1.png b/res/skins/ShadeDark1024x600-Netbook/btn_pfl1.png deleted file mode 100644 index d564354140f4599770945132dcf876e0298802d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^azHG?!3-qVmezg)QpN#3A+A80jZK)FTS7okPE<@; zN=8#f-9%HzQqRcV*xbd!*2~t(-^nA?-8af7I3X}1H7qXMg6U>4P+PI5i(^Q|t)&+o z`Hm=XI9y~D)xC7^_xv)WmAc#+vzOJURv7C1xMQkUbwuk-(Xpt9^4lhuGM7&Da}X3> zcRShj+nR!$wT8Ht}pWB}`2$r#ZICE0s=0}EPiFw_HycIgu f;ih$^?`PNX+sUv__`Mvnj9s_DC_H=O!skpWD zq9flC1rCRcjH0@i4*s5BX0%e5J7e~;`qT1XJeHiGB`( z!s~7)yM9|!kn{HBN3+?gg?G9NGa2hE-(Bl4p89k9vj)L3whw1cO5FU&kSsB;yO6g+ h$2#1!uJrxvI(|DD)(O8?4P+PI5i(^Q|t)&+o z`Hm=XI9y~D)xC7^_xv)WmAc#+vzOJURv7C1xMQkUbwuk-(Xpt9^4lhuGM7&Da}X3> zcRShj+nR!$wT8Ht}pWB}`2$r#ZICE0s=0}EPiFw_HycIgu f;ih$^?`PNX+sUv__`Mvnj9s_DC_H=O!skpWD zq9flC1rCRcjH0@i4*s5BX0%e5J7e~;`qT1XJeHiGB`( z!s~7)yM9|!kn{HBN3+?gg?G9NGa2hE-(Bl4p89k9vj)L3whw1cO5FU&kSsB;yO6g+ h$2#1!uJrxvI(|DD)(O8?4P+PI5i(^Q|t)&+o z`Hm=XI9y~D)xC7^_xv)WmAc#+vzOJURv7C1xMQkUbwuk-(Xpt9^4lhuGM7&Da}X3> zcRShj+nR!$wT8Ht}pWB}`2$r#ZICE0s=0}EPiFw_HycIgu f;ih$^?`PNX+sUv__`Mvnj9s_DC_H=O!skpWD zq9flC1rCRcjH0@i4*s5BX0%e5J7e~;`qT1XJeHiGB`( z!s~7)yM9|!kn{HBN3+?gg?G9NGa2hE-(Bl4p89k9vj)L3whw1cO5FU&kSsB;yO6g+ h$2#1!uJrxvI(|DD)(O8?7_C2 VzZ0Kan}J#wJYD@<);T3K0RS)(B!2(^ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down1_over.png deleted file mode 100644 index 805434339dc5aa57a944e6d14f9bb690ed0f95fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaLIFM@u0WcdU06B2&1~g0+k>C& zkN-OIHYpD%s_yCH7*cWT>Mz3JkTe8#>HPkl36$@J2g V_1}q4uFXI#44$rjF6*2UngBGqC@}y4 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2.png deleted file mode 100644 index 1addedb816d86e71b442af449c0806cd0fb6ed8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaLIFM@u0WcdU0Bb^-YXzBBswE3 zF8gtIm?%(G-P6S}q~g}u6P7?jIG7F2|F6HfbV0(wt385x)5RtEjC~)T`ewG0>7_C2 VzZ0Kan}J#wJYD@<);T3K0RS)(B!2(^ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_down2_over.png deleted file mode 100644 index 805434339dc5aa57a944e6d14f9bb690ed0f95fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaLIFM@u0WcdU06B2&1~g0+k>C& zkN-OIHYpD%s_yCH7*cWT>Mz3JkTe8#>HPkl36$@J2g V_1}q4uFXI#44$rjF6*2UngBGqC@}y4 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_up1.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_up1.png deleted file mode 100644 index d78dbb4c2321745f1a0fdc1046579479b5284f7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcdUD(vh%_|@_EG~P) zEQaksA!AP$$B>F!ThBUjF&J<#A6)kFfBw7Fe6FS&wtN!)u}i*PIG3@~Id6T6m_xVK fk>&SaYEQGh8q2cwYm?qYpmqjNS3j3^P6xAzo%7bGh&gmy g9a(<=rS>%2tFbI=zc%Sj1ZrpSboFyt=akR{0Kbha(*OVf diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_pitch_up2.png b/res/skins/ShadeDark1024x600-Netbook/btn_pitch_up2.png deleted file mode 100644 index d78dbb4c2321745f1a0fdc1046579479b5284f7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa`~f~8u0WcdUD(vh%_|@_EG~P) zEQaksA!AP$$B>F!ThBUjF&J<#A6)kFfBw7Fe6FS&wtN!)u}i*PIG3@~Id6T6m_xVK fk>&SaYEQGh8q2cwYm?qYpmqjNS3j3^P6xAzo%7bGh&gmy g9a(<=rS>%2tFbI=zc%Sj1ZrpSboFyt=akR{0Kbha(*OVf diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_play1.png b/res/skins/ShadeDark1024x600-Netbook/btn_play1.png deleted file mode 100644 index 827d58c313e32bffb7e71e55dabaa07e025eb00c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQYryHA+A80on4rdQ&d<)K}tqL z&&b}?%FV{n&)GA~Dz>(K+tRjn^lFyotdcNlc0B7OBQYryHA+A80on4rdM^Zq=OvKbn zIlWDze3ovQ-<%vN5rJ^0!F_^&Hz=jDLv{5)M8Ln>}vooLB*z<`Hk?>^I6#EP3exG?~HE)z4*}Q$iB}RJ=jw diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_play2.png b/res/skins/ShadeDark1024x600-Netbook/btn_play2.png deleted file mode 100644 index 827d58c313e32bffb7e71e55dabaa07e025eb00c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQYryHA+A80on4rdQ&d<)K}tqL z&&b}?%FV{n&)GA~Dz>(K+tRjn^lFyotdcNlc0B7OBQYryHA+A80on4rdM^Zq=OvKbn zIlWDze3ovQ-<%vN5rJ^0!F_^&Hz=jDLv{5)M8Ln>}vooLB*z<`Hk?>^I6#EP3exG?~HE)z4*}Q$iB}RJ=jw diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_play_sampler.png b/res/skins/ShadeDark1024x600-Netbook/btn_play_sampler.png deleted file mode 100644 index e5753f4b2fcadc8d17d1f7dc0203bbc68af6ffec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU07H|K}tqLL)%=> z$llb-&Dk?7AR;v+IwLGD`(V+w{Xj+Lo-U3d6}PsYHxyz};9z!iWc~P`|C(L+tEtfw z78XWVwX9}X&bqKt^Ip@1IR>KEcYI>ye_YHz!dKkF-IG+WBnvcx!PC{xWt~$(6996p BEvx_l diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_play_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_play_sampler_over.png deleted file mode 100644 index a1569ce0dc67319b311fc2ccfb46baf5678d5110..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQaiUB?$u0WcdU06WHOvKbnF04p7 zy-lNhmf?&;);nL>9{g;7{1?y8-?>0V=AJH&Ar-f_o;MU?P~c#8bY%VbpZ}U&_^YYW z6BZUmSGBBWSkAhzQuAKZg*gVI)^~hj<$qkvKf+hs!rhZpuOtgJg2B_(&t;ucLK6TD Cc`^?G diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_quantize1.png b/res/skins/ShadeDark1024x600-Netbook/btn_quantize1.png deleted file mode 100644 index 833e896964942e39727ad5fc90b49430cd05cc85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lyrbkh-*YdL|ANQSX>qiWrxK< zk%RjcVW2XOk|4ieW@RI%po0%S>=S#K1QfIKba4!+xV873BNu}Lhs#Az&xJW{ zUdNtir}RjlTW-b6X;&n!WTg1T%;DK`PbO&k9UZ;Ee>KU>t(%#29b(Ei15IJ@boFyt I=akR{0Mk)6wEzGB diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_quantize1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_quantize1_over.png deleted file mode 100644 index 468833d8bfb9fd459889348a452232aec205caef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lyrbkh-*YdMChyJ(2vQXpHo7= zq=bG?3H<>@ZRs7yfXX;Zg8YJ+m5rQ&4nFv>PwZh5P|V8H#WAGf*4}fDTnq{vE*CwG zpZ`-o7v{8i9ebXg(j$FtxfL&`U6Htwk>VFKhiA(@nV{)+bo2uM)g&{wZf4STh$-I; PG=;&_)z4*}Q$iB}HcdPC diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_quantize2.png b/res/skins/ShadeDark1024x600-Netbook/btn_quantize2.png deleted file mode 100644 index 833e896964942e39727ad5fc90b49430cd05cc85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lyrbkh-*YdL|ANQSX>qiWrxK< zk%RjcVW2XOk|4ieW@RI%po0%S>=S#K1QfIKba4!+xV873BNu}Lhs#Az&xJW{ zUdNtir}RjlTW-b6X;&n!WTg1T%;DK`PbO&k9UZ;Ee>KU>t(%#29b(Ei15IJ@boFyt I=akR{0Mk)6wEzGB diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_quantize2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_quantize2_over.png deleted file mode 100644 index 468833d8bfb9fd459889348a452232aec205caef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lyrbkh-*YdMChyJ(2vQXpHo7= zq=bG?3H<>@ZRs7yfXX;Zg8YJ+m5rQ&4nFv>PwZh5P|V8H#WAGf*4}fDTnq{vE*CwG zpZ`-o7v{8i9ebXg(j$FtxfL&`U6Htwk>VFKhiA(@nV{)+bo2uM)g&{wZf4STh$-I; PG=;&_)z4*}Q$iB}HcdPC diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reloop1.png b/res/skins/ShadeDark1024x600-Netbook/btn_reloop1.png deleted file mode 100644 index f554efbf8e9f4335a5e2a81eddb2ba87097cd6f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z?CIhdQgQ3;L`R@r7M73y-Loxoh2AYQneN#ovq3hV>1xPmdKI;Vst0K%0eAOHXW diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reloop1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_reloop1_over.png deleted file mode 100644 index 16daa7ccb64eb951cda7396fd0275f83b9a87122..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zv8Rh;NX4zU6CHtiSy(>)ch9!W6?(VKWV&aU%m&$brmGYcZKJ=gJKz3@{x Y_U3k0ZNozopr0JsPw_y7O^ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reloop2.png b/res/skins/ShadeDark1024x600-Netbook/btn_reloop2.png deleted file mode 100644 index f554efbf8e9f4335a5e2a81eddb2ba87097cd6f8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z?CIhdQgQ3;L`R@r7M73y-Loxoh2AYQneN#ovq3hV>1xPmdKI;Vst0K%0eAOHXW diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reloop2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_reloop2_over.png deleted file mode 100644 index 16daa7ccb64eb951cda7396fd0275f83b9a87122..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on6@e_^%m!toA^T zv8Rh;NX4zU6CHtiSy(>)ch9!W6?(VKWV&aU%m&$brmGYcZKJ=gJKz3@{x Y_U3k0ZNozopr0JsPw_y7O^ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_repeat1.png b/res/skins/ShadeDark1024x600-Netbook/btn_repeat1.png deleted file mode 100644 index 00317532fcaf30f8c1d07c0478675a7318944b34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lzMo$QBh9>3id3-Mt-` r8fSSlFn%se*9f01)wiDiW+~${4Hm{_?#I-DrZafD`njxgN@xNAj}uM# diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_repeat1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_repeat1_over.png deleted file mode 100644 index 504abded9cc94d20a878ea5e81a9cda17ad5a775..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lzMRI zn;iNzCGo$QBh9>3id3-Mt-` r8fSSlFn%se*9f01)wiDiW+~${4Hm{_?#I-DrZafD`njxgN@xNAj}uM# diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_repeat2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_repeat2_over.png deleted file mode 100644 index 504abded9cc94d20a878ea5e81a9cda17ad5a775..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lzMRI zn;iNzCGP~dU7xN_OE z#DD**x!d@}c70jbHaYggPn0=|?e&n&t<$UL^SpvGe4B?7L-b)$JU2&4l lFoRFYDfQ~cMC+eR*;TBVh5vq&+y*q8!PC{xWt~$(69AzMN%jB$ diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_repeat_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_repeat_sampler_over.png deleted file mode 100644 index 18a2d50ee2c9c8c88da6456ed49130262b07c116..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^B0wy}!3-pG`1q87lzMz_;6RjinW|9+F)1~i+&)78&qol`;+0LWBNQvd(} diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reverse1.png b/res/skins/ShadeDark1024x600-Netbook/btn_reverse1.png deleted file mode 100644 index bafa178b5153eff3a671f1b8222e948b49cfabf3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQv3lvA+A80on4rRS27?XH7qXs zygh#zP{_>F#WAGf*3~nLLJSH#M;sRX`G0om#GO_h_fATcY@Rn|>Zw~wjY^GbjW0j1 iXjuh>vzGLy#xc*{%(X{fzhW-X1O`u6KbLh*2~7a9VJ!au diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reverse1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_reverse1_over.png deleted file mode 100644 index 06e14aed300aeaf08a3ccf5005df908fd7da3a42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQv3lvA+A80on4q)M9F&REBoWW z4*bhm1Qas!ba4!+xOMf6q7Z`u&k=_OfBv7HI&r5}$GwwMC7b6>nR@D$QlnC%TI0*l jD_T|o;jAV7sd3D+H*@XL*RPlhG=ag>)z4*}Q$iB}sf;iA diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reverse2.png b/res/skins/ShadeDark1024x600-Netbook/btn_reverse2.png deleted file mode 100644 index bafa178b5153eff3a671f1b8222e948b49cfabf3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQv3lvA+A80on4rRS27?XH7qXs zygh#zP{_>F#WAGf*3~nLLJSH#M;sRX`G0om#GO_h_fATcY@Rn|>Zw~wjY^GbjW0j1 iXjuh>vzGLy#xc*{%(X{fzhW-X1O`u6KbLh*2~7a9VJ!au diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_reverse2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_reverse2_over.png deleted file mode 100644 index 06e14aed300aeaf08a3ccf5005df908fd7da3a42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQv3lvA+A80on4q)M9F&REBoWW z4*bhm1Qas!ba4!+xOMf6q7Z`u&k=_OfBv7HI&r5}$GwwMC7b6>nR@D$QlnC%TI0*l jD_T|o;jAV7sd3D+H*@XL*RPlhG=ag>)z4*}Q$iB}sf;iA diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_rewind1.png b/res/skins/ShadeDark1024x600-Netbook/btn_rewind1.png deleted file mode 100644 index b2075d8a859cc3fab9dc4dddcae8baca1b266264..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa>H$6>u0WcdU6_YgQdmSmN=8FT z)lfs*+{V$**)z;5AT}T(H6%JCEG|2%Sjz^e)XUSwF{I+w)_zN&0}29%lZqI)|JUc| zSn)@zX_+LK+7&%JR&F@WrYwM0Oj{wc=0NS?MQR$|vAZKx7RrjbK9oDEv~!7Z?U!hu aO2#`{ye`k=G-m@%W$<+Mb6Mw<&;$Shr!yx2 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_rewind1_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_rewind1_over.png deleted file mode 100644 index fd5842795d0e495d4e825e8cbda63d29d4647dfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa>H$6>u0WcdU6@-$NkGL+#MDd5 zGg&UINVj91;fzCOE3aAad}Vv^v;Fa3bzKjNfl9qRT^vIyZf)(i6gr?Fa5$-mf%|`b zevTD?w3?Pla;aU>vt#9k(`?EDc*V38B5MxR9$uuT(H*-xQe~m6nCnBiqe?rM7}tJ@ b_NipNqs8m;Oipt)&{PIbS3j3^P6H$6>u0WcdU6_YgQdmSmN=8FT z)lfs*+{V$**)z;5AT}T(H6%JCEG|2%Sjz^e)XUSwF{I+w)_zN&0}29%lZqI)|JUc| zSn)@zX_+LK+7&%JR&F@WrYwM0Oj{wc=0NS?MQR$|vAZKx7RrjbK9oDEv~!7Z?U!hu aO2#`{ye`k=G-m@%W$<+Mb6Mw<&;$Shr!yx2 diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_rewind2_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_rewind2_over.png deleted file mode 100644 index fd5842795d0e495d4e825e8cbda63d29d4647dfb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc3?z4jzqJQa>H$6>u0WcdU6@-$NkGL+#MDd5 zGg&UINVj91;fzCOE3aAad}Vv^v;Fa3bzKjNfl9qRT^vIyZf)(i6gr?Fa5$-mf%|`b zevTD?w3?Pla;aU>vt#9k(`?EDc*V38B5MxR9$uuT(H*-xQe~m6nCnBiqe?rM7}tJ@ b_NipNqs8m;Oipt)&{PIbS3j3^P6sNxHCaFlumf8#We=)z$1G_J*^>tBMJ{;%YFLLX zK+UHSzgU&TLUd6>@CbpB#Dqd3r8uyaSct$(cJoj8?R1`4h`=oh1D__6cnv~FM|cN9 zMG+o!MqbIwP$kO9c5sNxHCaFlumf8#We=)z$1G_J*^>tBMJ{;%YFLLX zK+UHSzgU&TLUd6>@CbpB#Dqd3r8uyaSct$(cJoj8?R1`4h`=oh1D__6cnv~FM|cN9 zMG+o!MqbIwP$kO9c5OBQk(%kA+A80on1IAF8fDwx*(8a z?&;zfQgQ3;L_OBQk(%kA+A80on6@e_^%m!toA^T zxu=U`NX4zU6Ak$o7+9D;{&!#8%%_xC&OIqxyy!sLo|zgQ6SHF%%=D39E>>?jsbcbe e?t}e1k27|Au^O5bBv$~9VDNPHb6Mw<&;$TXu_lWE diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_sync2.png b/res/skins/ShadeDark1024x600-Netbook/btn_sync2.png deleted file mode 100644 index f4b5ed9fdb15ed6e30b2960a1ad4b6b131e2fe2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133 zcmeAS@N?(olHy`uVBq!ia0vp^nm{bV!3-qTqn>OBQk(%kA+A80on1IAF8fDwx*(8a z?&;zfQgQ3;L_OBQk(%kA+A80on6@e_^%m!toA^T zxu=U`NX4zU6Ak$o7+9D;{&!#8%%_xC&OIqxyy!sLo|zgQ6SHF%%=D39E>>?jsbcbe e?t}e1k27|Au^O5bBv$~9VDNPHb6Mw<&;$TXu_lWE diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_tap1.png b/res/skins/ShadeDark1024x600-Netbook/btn_tap1.png deleted file mode 100644 index 6a5d1bbe0b9e3ab7d12ebef873b7e7beedccf58b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^AS}Yc3?wx_&$t<7zx;^w2_ToTB*-tA z!Qt7BG$2R9)5S5Q;?~=PhKxYoA%)+|G8)<^Qv<02u+EE{-7;x87de&Bq|Xn6`@ z$&0u0m~bst%=_dv6+!n+%9@RnTqZ9y?~T56vF@iNv!mYWMccn@NZEJV-&4@yt<7zx;^w2_ToTB*-tA z!Qt7BG$2R9)5S5Q;?~=PhKxYoA%)+|G8)<^Qv<02u+EE{-7;x87de&Bq|Xn6`@ z$&0u0m~bst%=_dv6+!n+%9@RnTqZ9y?~T56vF@iNv!mYWMccn@NZEJV-&4@yp=fS?83{1OTbO B9!3BF diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_tap_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/btn_tap_sampler_over.png deleted file mode 100644 index d26e6043ad4998f41c5d9b0fa5bb04819fb9e8fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 130 zcmeAS@N?(olHy`uVBq!ia0vp^IzTMM!3-qDwa&Z)QfvV}A+C~=lA%9RY$vH+1&Ww^ zx;TbZ+Rs d{9jt1y||0%(kFS{_dp{UJYD@<);T3K0RW!PDB=JB diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot1.png deleted file mode 100644 index 980b5fc2b19e28ddfaf64229410a8f3033c38316..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRh?c)B=-RNT5c(UI$b0uRgOCI9P-U%y!C6%o`@mTmsZb+c~y*%y3fPtXu; tyPbzu#4P!>QOr@OQ_o55+#4}hy(7I`iv5?BYJm1Jc)I$ztaD0e0sz#QMgaf- diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_hot2.png deleted file mode 100644 index 980b5fc2b19e28ddfaf64229410a8f3033c38316..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRh?c)B=-RNT5c(UI$b0uRgOCI9P-U%y!C6%o`@mTmsZb+c~y*%y3fPtXu; tyPbzu#4P!>QOr@OQ_o55+#4}hy(7I`iv5?BYJm1Jc)I$ztaD0e0sz#QMgaf- diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off1.png deleted file mode 100644 index 17b655ff2b5b4b8af2920faf128aa3e7262990ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhCdAc};RNT5c@gmm&1rFD{cmCIZ4mx*QF33}<>_FZ{ zM=yk~bP0l+XkK_*F^c diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_off2.png deleted file mode 100644 index 17b655ff2b5b4b8af2920faf128aa3e7262990ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhCdAc};RNT5c@gmm&1rFD{cmCIZ4mx*QF33}<>_FZ{ zM=yk~bP0l+XkK_*F^c diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one1.png deleted file mode 100644 index ee32e7217a4338cde70c828680927c5c13e8ea40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhCdAc};RNT5cIgqQtfX8|9*MI3{+>9TSH?HRS;&}3y zT&lgJienCM#I8W0<~zv>MPF8JP?9~myZ4EsiH^u*g+3A0LUr9=f!iBTOw>8q*^+oP zJM(XU-_%FpQ5)CZ+ibuu&Xl{Swznnik@?ztFwla9S`njxgN@xNALbOSK diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_cueing_one2.png deleted file mode 100644 index ee32e7217a4338cde70c828680927c5c13e8ea40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhCdAc};RNT5cIgqQtfX8|9*MI3{+>9TSH?HRS;&}3y zT&lgJienCM#I8W0<~zv>MPF8JP?9~myZ4EsiH^u*g+3A0LUr9=f!iBTOw>8q*^+oP zJM(XU-_%FpQ5)CZ+ibuu&Xl{Swznnik@?ztFwla9S`njxgN@xNALbOSK diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal1.png deleted file mode 100644 index be8ffc82df409d28d2815d93e53b29e2a7d76626..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 95 zcmeAS@N?(olHy`uVBq!ia0y~yU@QW%nK+n%WYPNNen5&jz$e6&;f!(p(<6OAuDGX* sV@SoVw+9({fjkF;zrn8)>$cf2GCgBp+@$P02Pnwk>FVdQ&MBb@0QjC5v;Y7A diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal2.png deleted file mode 100644 index 82587917fedf138231051a358064999f7a8472bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 95 zcmeAS@N?(olHy`uVBq!ia0y~yU@QW%nK+n%WYPNNen5&jz$e7@|9@jsv8lm8uDGX* sV@SoVw+9({fjkF;zrn8)>$cf2GCgBp+@$P02Pnwk>FVdQ&MBb@0PVaOV*mgE diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal3.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_horizontal3.png deleted file mode 100644 index 51bc3a21f758ba7090c5d8dbb7816732da5bef7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 95 zcmeAS@N?(olHy`uVBq!ia0y~yU@QW%nK+n%WYPNNen5&jz$e7@3$cf2GCgBp+@$P02Pnwk>FVdQ&MBb@0NpScDF6Tf diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical1.png deleted file mode 100644 index fd75decec79cbdecd02628c62475e2918189f1aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^OhEjLgBeKfS(%muq?iMILR=Zn7}q~N(g);Q# diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical3.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_indicator_vertical3.png deleted file mode 100644 index d707bd2d6f1559294574f006950d14fe53baf18e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^OhEjLgBeKfS(%muq?iMILR`-cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhyd%8G=RNT5cF_5dlfXDgutH0-Sq%#9A3b6hDaFCUw7T0gX(b&Ur2k97?mcI=VJIP+DR% zWx3-~SCdZFMaO2zzHi;L?aHIY$6xJRAFa}AEa*I0s8u%l&ry9FhEG4Z_%!TnfG%KQ N@O1TaS?83{1OTVJPT~Ln diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_abs2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_abs2.png deleted file mode 100644 index 25f0f41d8c0c9f972309f737290dd3f893e1b99a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhyd%8G=RNT5cF_5dlfXDgutH0-Sq%#9A3b6hDaFCUw7T0gX(b&Ur2k97?mcI=VJIP+DR% zWx3-~SCdZFMaO2zzHi;L?aHIY$6xJRAFa}AEa*I0s8u%l&ry9FhEG4Z_%!TnfG%KQ N@O1TaS?83{1OTVJPT~Ln diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const1.png deleted file mode 100644 index 7bbcb092c30441eb5e0d65915dee8550cd013065..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhyc)B=-RNOi{sgbL}fWt-i@Be(gSWBZz+5%MueJ7pS z%##EjrM3rM`JuAO@X;RIjxw7Cy*>FMLK|z6oo4@7%;mo1Y|Fb1T-q03YrCGZV|6U) zsAy&S=oix~X|zopr0OU$bWdHyG diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const2.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_const2.png deleted file mode 100644 index 7bbcb092c30441eb5e0d65915dee8550cd013065..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|Nnn>cHywN>{qW| z+lZ|%2MRHj1o;IsFqBO`YXRhyc)B=-RNOi{sgbL}fWt-i@Be(gSWBZz+5%MueJ7pS z%##EjrM3rM`JuAO@X;RIjxw7Cy*>FMLK|z6oo4@7%;mo1Y|Fb1T-q03YrCGZV|6U) zsAy&S=oix~X|zopr0OU$bWdHyG diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_rel1.png b/res/skins/ShadeDark1024x600-Netbook/btn_vinylcontrol_mode_rel1.png deleted file mode 100644 index 338eaaf6051c4c84730fe7ed3a7bd780729d5035..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^8bGYX!3-p&c4u1xDgFST5LY1m|NnnUd6lrZ>{qW| z&+2Qx1{7i{3GxeOU?`h>)&j_@^mK6yskpUuvNsolB9F86FB))V;{_!Db7g`;KfU1(L!fgH(8QgR~Cs zoE5TT|32+mEQgZ(SzacjXHB!{b?Wuo9{qW| z&+2Qx1{7i{3GxeOU?`h>)&j_@^mK6yskpUuvNsolB9F86FB))V;{_!Db7g`;KfU1(L!fgH(8QgR~Cs zoE5TT|32+mEQgZ(SzacjXHB!{b?Wuo9#WAGf*4rZ+d4W7egFp3KDh{SxSQs$HxZpF-9#4j>P4^3?0i_r`UHx3vIVCg! E0Hf_7i2wiq diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display2.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display2.png deleted file mode 100644 index 8e0d1d7667a6b6e34ec27bac88914152fdb3b227..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^tUw&d!3-pI*Ud2lQp^E9A+D10Dy~=jvw&PNPZ!6K pid%0FGBN^r4hsLnrJXi1Ffu-4U_2(;C#WAGf*4rZ+d4W7egFp3KDh{SxSQs$HxZpF-9#4j>P4^3?0i_r`UHx3vIVCg! E0Hf_7i2wiq diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master1.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master1.png deleted file mode 100644 index 8e0d1d7667a6b6e34ec27bac88914152fdb3b227..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^tUw&d!3-pI*Ud2lQp^E9A+D10Dy~=jvw&PNPZ!6K pid%0FGBN^r4hsLnrJXi1Ffu-4U_2(;C#WAGf*4rZ+d4W7egFp3KDh{SxSQs$HxZpF-9#4j>P4^3?0i_r`UHx3vIVCg! E0Hf_7i2wiq diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master2.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_master2.png deleted file mode 100644 index 8e0d1d7667a6b6e34ec27bac88914152fdb3b227..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^tUw&d!3-pI*Ud2lQp^E9A+D10Dy~=jvw&PNPZ!6K pid%0FGBN^r4hsLnrJXi1Ffu-4U_2(;C#WAGf*4rZ+d4W7egFp3KDh{SxSQs$HxZpF-9#4j>P4^3?0i_r`UHx3vIVCg! E0Hf_7i2wiq diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_microphone.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_microphone.png deleted file mode 100644 index ae180c850c5c8b1091b16219d8513675499bad34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^>OjoO!3-onO0rr3Ddqs55LZcg71t~NSwOC+r;B4q o#jU#s85x1Rh6(@He^Z{tz;J?r@%>8CJfIMRr>mdKI;Vst07gOOjoO!3-onO0rr3DYgKg5Lcna9{>L{tbgPd0u+(* xba4!+xOMiFA|sG@$lyi%FP9xDRf1OU`Pr=)M46{tQUZ!Ic)I$ztaD0e0swFP8ZQ6< diff --git a/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_sampler.png b/res/skins/ShadeDark1024x600-Netbook/btn_volume_display_sampler.png deleted file mode 100644 index 28ca03b3c78e8c5218f1b76b060499c0954e7afe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^tU&C}!3-pSDAsNOQfvV}A+GG~!jkeTs~qkx1Bxhl zx;TbZ+&0001;NklK4vvAbVnrs zP@7Wfo2+5H3$(*z?o)I;i^Vsj_-d+=qQmPQo#Zt20t;JoyFbZjX3#q_%%}wO#UO(s zD%PVt`ZsiwEaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U diff --git a/res/skins/ShadeDark1024x600-Netbook/knob_pitch2.png b/res/skins/ShadeDark1024x600-Netbook/knob_pitch2.png deleted file mode 100644 index fafd15e2804ba34dc14ea7058d062b39c673fd07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM9!3-oF+x2$?DfIxK5LX#_Wm$O@AdiueQB6}{ zL&r!<*TgR@DIg*xFd{W5Dm^47GxSG_<$)9YKt)U?L4Lve_xlN41es{->Eaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U diff --git a/res/skins/ShadeDark1024x600-Netbook/knob_pitch_sampler.png b/res/skins/ShadeDark1024x600-Netbook/knob_pitch_sampler.png deleted file mode 100644 index fafd15e2804ba34dc14ea7058d062b39c673fd07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM9!3-oF+x2$?DfIxK5LX#_Wm$O@AdiueQB6}{ zL&r!<*TgR@DIg*xFd{W5Dm^47GxSG_<$)9YKt)U?L4Lve_xlN41es{->Eaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U diff --git a/res/skins/ShadeDark1024x600-Netbook/knob_volume1.png b/res/skins/ShadeDark1024x600-Netbook/knob_volume1.png deleted file mode 100644 index fafd15e2804ba34dc14ea7058d062b39c673fd07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM9!3-oF+x2$?DfIxK5LX#_Wm$O@AdiueQB6}{ zL&r!<*TgR@DIg*xFd{W5Dm^47GxSG_<$)9YKt)U?L4Lve_xlN41es{->Eaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U diff --git a/res/skins/ShadeDark1024x600-Netbook/knob_volume2.png b/res/skins/ShadeDark1024x600-Netbook/knob_volume2.png deleted file mode 100644 index fafd15e2804ba34dc14ea7058d062b39c673fd07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^d_XM9!3-oF+x2$?DfIxK5LX#_Wm$O@AdiueQB6}{ zL&r!<*TgR@DIg*xFd{W5Dm^47GxSG_<$)9YKt)U?L4Lve_xlN41es{->Eaktack=Z zLp}xt4i-n2K#~9d;|wQyq%OVfTW-m)WOAo#`5}&!Pg0DTFLd2soSScK;L)>lcUI*4 bmP;&!T3iMCStp(W4Px+g^>bP0l+XkKn4C5U diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png deleted file mode 100644 index 69e64c74b2596c856cce1cf20b0c649057438bca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5LZcg6$oa zWwRG!KhPrSk|4ie1_1?K1BZZ+go5&h2~!rVJ#g^Cr3VjQefaeIFPl_<3Q*YrPZ!6K zid(f4ZwfUl@VGW-+}%*RG*svR|CwqFFN;W4$ItnA#+HR?(x%s4O3f-u@9zD6Zs*M- zA<^dxWfr%XNH^EMO;1byH+ApJ|4SGbXDvG49};}rC?R8o_#Ey@jCDbZ*^%zD9Zt4E zQ8w9_J@-NEpAfya*V)lOjPWwqH8AAH5Sy!jzlsLGunlSLDbUr1k+ z(ewJ@|G1gG>m}~q6wiA3St|0B(vqo59hz6?usq%26<7Qs&@*dcF+*jVaHf@B@duz^ O7(8A5T-G@yGywnz$(h0c diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png deleted file mode 100644 index f06258ae5beb286177a80d9d788350391a630c41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(r03aiAnCd{7$WX=I0VS>z8ndXIv4J7A`Hmzy%Pf@%{pEDAIAFKi-B;1-X#Wq zN&<@OW?F@OO%DB@0<>lNy2-&nOXW&}{DK(-6m$(70zwjU3d$QMOj)q|z`+w2?mT$< z{MFkJzyDfXcLcfhl&6bhNX4zviLb?)6a-qtFYbws+oYrV-4oeuCiW|ByrWgC;>&Fu-H?;Sko0FdV@cr8DOJAItcuJSm qYwy0K_N17n-=>%wSO=MYjWuK6riO!g6>{GH>i{Z`2{mD2q0yCvCg4pIM)*sS{~lI!AIzkZq^` zvo#mg-zfLiiRHhUyt+~7&Gl!xZ5j!WuKivv#o0OWR%N~X;>pF{rGMI!Sj@H?o)S3G zSLLy#=V0H-Cm&+=UYM+HTzKET`q{;{!nXO>w|(o0|FwF{UXgn|1p!GQayuO2vJ=F1+~^M^JV+*CJV)iN5_w4z8CT91GZiZ>S;Tum$9%$(E zXI<|esl9rQ(#3_7tIBUbxV>b-+7_m@4>H3TxMXYE7VMtD{Z~V9+RHN^E;xKhJm>an zwxZFc^CvZb$7C)E`Nm!O+OW&SaJtPb520kM%KtMKO{(;MZaL*b+_%|_xEfz%96OT0 z^LEBR`FV3>jJ1_FN-T2v+|)bykBU1o;84`Ku4V|BAcR7Xnq^_jGX#skoIp>E)zD20YEaUe1lW zu|a*CWKwyb{FN8(jQr5Q?3GuqE&tCb f?;{`m$%b+B83~)5iJ87YuQGVL`njxgN@xNAC4;tF diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png deleted file mode 100644 index e0fb3fa1df5cb35363e78da6d707504baf142afc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFA-p zPMZUO49f|wK+^kiB#?C88sM@s5XjI@F!wwg4rI9Q4F-}x91tE%{xxM(aZ_0|-Xk$k(` z`rzlVc_+2_**4Wa{I|v7ZP5j_&I~qxTV@5G50=-$mEC^^r{(k~@%ERn$Uj@Nw*R$K z`roFx+s=Ddoyy%mWBa?sEuF1TPdZGSaq8K}9Xx?6cO6;sRO6J&*1v5loD{5f*%eAO z%YOd&Q1yo4J|kiN*ph^}Et%Kq?+LsWjsLaU)?T%4S$@c0{>rPzxCLqr{^nf#%=R`{ WeC3rJyDtMh$>8bg=d#Wzp$P!Xq_&0t diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png deleted file mode 100644 index fee0a6942c02e0dfcb4afbcc0de57d1f215ffb96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDgXcfmy}llBFByX zPMZUO49f|wK+^kiB#?C88sM@sP%qmG$o4!N4kTUo1_MbTayuLXVR)VciF;oGsrSAX z<^M3&?_La$?Rz&S=v`v)rz9ZTZi%7Xr6kBNn1Mk+L0i|r z+QB0rFeE%7r=YxH!juIE4qmwQ;K}n>AAbM-`)u0DrZ92V@5NU&R@+LP_|*9Qtw{OwW;tbNLz{L9hs1{Sf?LElE!0mvbZy6y4*~8U z7M`DbrE~WQ&%FxE&um^guW07^PnYbvObQG4&dpHp^vO6D!y98JOL_Bu1^v}uSeDq$ eurrH(B787P!t_G&RY{;P89ZJ6T-G@yGywpoK(_q= diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png deleted file mode 100644 index a9f30d663eca38ecd1cbebda0ffc176294911d24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBB#v( zK+yf=Ro4#S3rvWAIAFKivco% z-X#WqN&+&RwgmV;jtlvk9Qr*4Xi;*_jd?(OO?z%BLRU(g~`_Q2cQrUA^>>s!`DSlhMz1Ui8{| z#yutey<)8L>P|zu8y-T*UNc_nu9_))>fb`1#R>{v9D&L@YiNT+efNYH* z_TdhhDi32|Cm zzT3wCiR#+EuiN+)88ZGjxKG&D$n^AXi&=QnzaZ0;QnN*055GEo<=HxuTdM0#K5t=Z zxmVNodGe&@niT=tQs-2rsC|x1SzUX{vWWLVsm=Yvfl5;ylqWH4Pwf#>dv-^_Tevuq z@v8Z%hN&X|0}>m*ny)gQARFs5N48n^^!;}6_07L4Q`O!*&p5y5V4G51oc)dLeP5z) szYJD+-H_uXd7*IUg7ZF;b3fQHuFw~rf6D!$4A657p00i_>zopr033Izr2qf` diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png deleted file mode 100644 index 7280d9a0b883d3ba32398313f081f96398d9daae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 423 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1k5?Pcv{te1g0&F%#8Bh`M*17cGJSLe;+nn zFE&$i5jov|rq-y9Ua6+X$`kSwF*`j=;}Zr!>KL5(+tN;K2 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png deleted file mode 100644 index 07ea2fd98c27f7e87c1d80370cdb2d9a0de87896..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 z&RYXqb_N0&cFTM`&xXUr-42I9*q-M=g5Fmmfu#S#SigHQKt{l`_@H-*Kr;AKQt-zl zporZP?~t#_q2E)0wydy!9RRdct|Z7Wm_a~6*T5klFeD){r=VfNlm!P4Ubyt&`Ku2f ze*gLVZ|@t~lR%XxJY5_^DsGidd@I$Yz|+cjaFK|q(c5gv@AVUNC(Ai~{v-LJich+0 zN$h2b#Yen;Cvqt=IJ_^4;%Q{nQ5Uwfk+8{+J%8r4zU97;HCEzVrioq*JZkcZheyKd z)7hEgD(WAVyVophJ#=lIlJHNF>(}?5n0Vy3OqkBph!ux^8uXvt`Do!o{-VMuf~pSN z-DDj!--vAzd2#(jvci3h=`Kamx6gilvGW^6JBf-+%vw z2Z`kY6&~|+aSW-rRXXt{Q`;jNdpBnRLBuX=m2(w1lEH`Saea14mMo|%=n zxMs$*RL3Ltj+;n|33lz_OsN+x*uQP>hI4yQ<}LgAa>j=nQ+(WidnKR$z-c(I_SU=( znFrpi8@t%Wd9 z{tsjQ?!^Gv0ng$Cp2P*cOZ0yd7yKzH_+t`KP|->U$O!qG9Qr*4X!~3?g&jc0sFnoz z1v3aJ=o(l%1O$d8B<2*9H%yqaV9V|U2hU!(^x(GBm`k*y=PUNdEDYqY0PxfqbNqF&ATB~?*mkYtABPsnFl zt?4CrV)K(39P13@=c$_;PxgJ3-0^J=d+Y(e$M>bbx>sIbsJhUW>+QU)MLIOA}2JSh{gT;r`kuQ7PK`njxgN@xNAsM)%> diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png deleted file mode 100644 index 571a07dcc19ef12fa48991fbd09bea058ee1416c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(q}?(f&$HohLAS#pK(^O~2q5Ws4kYeuIms>LYjWuK6re5h)MO3*P+O z2TN7;OJ-ev;{Qlpl)-s2Peq)I(5FoXRx_1eGFbP24(N8xIuIy-I`h!7XY7{>ri!s% zSQp4GvgnqRsAyUC6~LhTfK1e8lgnMY}*pMnQB` zPoki1k4zo@S`Z4O@{A^xJ~tb z+Yb56`7dtv@rlh`v7T#nt4t&B)w-813(rkw-(SRHxN^k;foRQ(3Vzupie^svioR`W zy*!eaIs=3jWaq^Hdq4SR;hBpk>I#!~$-Eb^SM{@6j(#EY7|wb-(Ok ZbhssS+NE^U8=!9(JYD@<);T3K0RRxKnj-)J diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png deleted file mode 100644 index f50be60339603fb705844bb266d502373cda6845..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILm*s_6JZdB-@OtW1C6yP z>zo9%MY1HwFPMQrKtb2QAs{57pkcz41qV)Cc<}1Ohu?qyP2kiQ0xCM>>EaktajSOX zZJ}lb9v5aYQCD3L{oAMh|4&m_h}v2fKcnD=FN4sO(DK+wv0}xR6D#DF3otRH8~J+l zDRKq6T~hkm@t{&ZCM7I2=tnrm<*!12d4uiSy}0+}^8{tym~Ir!Fl*lu#w~_@(wE)d zXkU8uM9g!#pY?);bEOU}-IK)qDeWXXUy*~SsDaEyhL9!aCp>egcA3V!bMYPFlgV@V z|NlQV=SF_u{P$UPw(BG9Sl@cx`Woy}>9E#=u}*Vo>g6o&dHntS0%uBn5~YE@VeoYI Kb6Mw<&;$VcSC4f7 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png deleted file mode 100644 index bdaa5a414364a2d8b1c670c67710f64465d40fb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx803t!Rj#_mxQ3eZiVhW@V-k>Ix5PW-YjWuK z6rjlmvmO+IY%U4%3ua&tP|!7S2nb0iXqYf%!PWy8E?v3(;K_&I|5l{g2?JFf_jGX# zskl`;@vT&o0#7S@S6hJR(kn*q{u@ar+b&Cc&VT#f+YhW>jV}sp%zZ57*3B@pJP6jh(!IE zw6rOC_Z~x?DbH7J;;ZWX60j@tWx~?FTix+nm4b41Bx-KUTHyL-`wI7*gYOh)^Bxvs zHF_fZbykMmv3C#u$IX=W_b&N$=j#8eIQ#mp*`2fPS$$kgjEy=b2B{VssU+JhzSrU} V*u0Iw;yBPt44$rjF6*2UngElko+khR diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png deleted file mode 100644 index 393a509891b5db1e753f9ce029442e9583bad86f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#!gXBd2W0r(i2;&+_dsOOyTss+NkE38m5$vK?~t#_q2E)0#?A__ ziU-;vQ4-`A%)lU^VBp{p5Ry>PFk#Aq1E(+CdGPAP?|&1k@;3n09QJf^45_$PJMkt{ zlY)S2yRP|}jy1O*{r{hKZiV2ix#ALh5Jw0NK2xiW_#bP0l+XkK1~!@> diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png deleted file mode 100644 index 6a7e0e737b97f5bd8a28e84d055a2ee3e95017f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx7~$g$RhC_mxN>!~0qkkOZm>dY1@fC|c>b9SRBgnjHE)1!!#3 zUyD0HTO>+?{DK)61Qc`)90EcT3K}L%S+MlLg-Z{feE9nN`h{D6foe{8x;TbZ+^U`U znyE>Fr78VRMp2Z4!A3dx^#ad+TX24!CaGM!^jn!7!*AfVn67A01${{6K&R?cg_=lpi2X>OUun@qg2LO*TPynSTxy;rXUqU>iK ROa}Ui!PC{xWt~$(697?7ndkrj diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png deleted file mode 100644 index a0c644070588c549167b0da9a0a8533b5248cc35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#B)BUGBItQK9Kr~Cmk4AiTIskQ3JLj|3^dK;i-|VSaIumgzhDLi z0R;mGkARSbf`$oG7VJ52;m(5(zZs8RJ_%HD%+tj&q~ccX#J5sS3Oubp&j^@sMxXo} zpK-gR{lc3+jUV61v~y@K)!5}XdF!n0lR_r$%ePEt@MzRfR?|A5Eu6%9bi%|=hVyf_ zAD=YCH8{yIXX%35tzM==v4&GtM>u;)o(T8qk5=FJ+c*R!Dz{*4-UF zNy`Oqd(V64XuIXkh3>^`Bp9YCM=`KHJU3zSfeIHv=AYeJ{vy4Fc7N_`7eBhQc;`LS zKfm@awC&12vfW)Ujn#lxvO(1Q=f{aFZ=O(oDjmL$-|isChFd@%F?hQAxvX0H|LeGegC_U@y&D2 zFsncCcSd1iriRiBqw|kfY?KYS+rigg8avaHb)kcTmcf1&M@L4cnGsy)zt+YJMVK5~ zAuw0`vD|fUya6FH&ue}-dYmzQ&|Ggv5bB`>4Ju9R?WYzN1k22Jc&&is1 zZ*i1N;R5kRZnI`gSB{#YzA62LSAxx=2&RwjTh+O8=a~Kfe#&Fd*B5OzH|lKiGn_y6 t{xB0Unt9VCWya=%QyvNjZ|+Q&u=CO2Yc9~^bO(Bf!PC{xWt~$(6990XkDCAh diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png deleted file mode 100644 index 5b3f01b10d6a92616515403b8c8e20656f58c42b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDUSf35ZBNjDgXcfmy}n5Aj=7^ za5kLbb~psihA@KOB?1|COT2w=MTdM%1{%WiHtZqLOyQCszhDLi0R;nVhk%5Fh6z&^ z9Juh{!|#gbm)n5Shdo^!Ln?07PP{GDtia=PcqzxCrlnW^|G%u}E!)+1SKL5-HBNhZCg0uY2d9ko&HOw irij)gWtn}{_{!KT%xh&GG7ab(1_n=8KbLh*2~7ZYA&s^G diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png deleted file mode 100644 index 72782ab6a738a7b190593ec722609d79c696e74d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 457 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspXHth)a z^3LL93p*ax96ovKEYW)&i{$>7Te6FEcV2VWQ1?%Do}n;b^S#Hp4~qpN7galdYM6P# zed3Ln^UpgTRx|ClIL|BARcO~>`;685$*UtG&l{&2Tx{jE7c z1-BiX-V!Li>u1gGN~xWt{an^LB{Ts5)H1yl diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png deleted file mode 100644 index c48b7e5e7dc4f8b198cbe10b6af25d107706aac2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDc1m>5ZBNjDgXcfmy}n*h1?E@ zph>yx3=Ddg2owx_oe=sx1!#iLvzPA^4Aof*aSJutzV>KI zzv`?|(YU^2-=F*Hs~;OJO0CYetdGmR)i$YKCEt0XV!6_P=Xon$Uu&D0xIE_T^B)YX XRXn!~^#2qCJ;UJX>gTe~DWM4f*;a>W diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png deleted file mode 100644 index 4d50a6d306da9c27d3f3b145dd357bec922f273d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDdPa25ZBNjDgXcfmy}l_9(~J< zQ3h%kC<*cl22u(J4gm=T4HFg|xbWb^-p?K;K)EfRE{-7;w`wQe7HU!8ao)}PL`mWQ z|E;&pPHR}@#Z8!}*08j{D@3nDX6Bk7%Q8nh6Yir|_A8gKRH<;gTe~DWM4f*{yOt diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png deleted file mode 100644 index 95f9935432605e11cbe875e8adb6b63bfbbf771a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDW?FR5ZBNjDU$LkK&0wp2w|X+ zZihqAM0{^W2fa%K3I=~n0&2Hn+?5P8O}He;FPK3v!*k zgA0Jtn><|{Ln?07PQ1<6tRUc0u6e?h_14P&|5u;gTVG@M*`}c_P$g;_N zyc6eL-1wnm$BTDNELN%vuC>p4N-mykGh9`)h2eKkS%PAH>mH8JJ)1aq8SI4F64vq8 z|395{$8C%0{AmdK II;Vst02hgV;s5{u diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png deleted file mode 100644 index 8df606d5406dd440388088dd23566d4dd5207a7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDZc=p5ZBNjDgXcfmy}mAuC)h} zKxEPD0%0JNZihpVxj=T%yF?)Av?aj*aa_pPAA8D0bsL~h7(8A5T-G@yGywp!2ah)Z diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png deleted file mode 100644 index a3a4fe1cb26877adc174763146d154416c4a68f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bd-e) zK(@=yK)48y;dVF#NJ6CG;z92cgFht!xt5dMd~ZdEd`$+LmY3eG0yJE#B*-tAfk8mQ zz`->jB%z>T!juI|4_tWg;rGAu{f!HNDh_$NIEGZ*s-1XS=#T=B%i*(L2cx>&p8mHF zaWZP&68yb&!sOTt#-$D^b9Ucf7qePRAn8$C$I>b3CQgzsJ5-$VkF1as+O)DUx3=cg zYO#||CsJOW&;4c;Tre$Q?uK@cibuICmbT0Pyv3p0XJgP&({|mr-y`29cDiKQtk)LL zXUa}owzx4+=4pacOn-*!oCEU}{Tp9)2(e!3tnxRBef;OsfBTt2(*<>P`*V-qm;SxD u#m}i;WP{|zvJ^q%+@tDh8%_IvF?~J7&vD_<#=k({FnGH9xvXV@SoV+KCsXniP0i*;yo; zq};SC|9|iC{mF6m9Pftu=N2q24Zf#lB<}baHi0Xs&h5yQOP_Y+h|KWlUo+*ao4%yc zrN)Ccw$rD?bvTMIQMy|FZtp!!3$28cZ!{HEZPvI?-!8TFVdQ&MBb@ E0K%b_PXGV_ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png deleted file mode 100644 index b04d702333736cb3437d686ca4746b27b9d545a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}mgFxLi> zPMZUOBm_Bc4RF~R2I9A;hS2&Dx6p$8+@0O|UBaci`Rj=M7B9Bm#B{ted_$hnG9;QIgw{ zpi|H5f-ID!14AO-XRtB}&u4k^@u%XKLr)wPcXa-qdqyswUrUW$>9ibQpsa;W=FQ97 zTIIK1dGR#wO=Ihg=Ot&$I&-dNMp;FyTwHjzID5(21I*r;Aq{MWcNPh5Y%Js7Dl=o7 zM&N=u8P|_|GX19i|NCUs!iYtVpRR2CyLY3VSG;GuJnNs97uugXgr}x`+qm-9k>q_J XIR)QdJ12hz=qUzIS3j3^P6P0|$?Qz>tK3h6z&^95{dB(t}qYe*a_2D%cHFb%sX*l%fx3V zFSQ-!vI{9(c=6^DX5-HDw{Jd{NzC8=b4B99S?i6l(GWrJE0G?j!gW(Efn4uvQ9v^2U1IRZBp}0SOF+oi zWT45PdITkb7D<-``2{mD2q>uN8aM=mBos7En6hB&feV*z-+l1p!|#7(71+)w}dZ@#-=xg_l~`;C6du<1P=3gIHtKXXPmziQu6 zmhw#Ev5h7x(?(rq!C8C_4s{F?zmE&Qc{g>UfcTcUXd&74#WjTmERkQgFf^E3^1Zxq zV%sA9(4dS9v#T-`b5AWT-Qt~cyd-+stguy2t4q_qvpjO$ttOWH#_a$1$rEqr2DaPWsQYuzjDP9LB`2K~fu!5v5D44jL>PqOcP|FW2zVAB^e!>@Q&RB9Bp}!KRwEp_X^Q;|k3ZwzbpPCrT_GJDmr`$*s@sUI zsO!wLHEtKUm%PM5VO6Sz!(t{DBfbOt+u3jYoiU-scjG+`Az8im0`n%TdARy>GgQdqF$) g@;;k$zK9%UUo9Y%Ait>KE6`&Mp00i_>zopr0Dt$LM*si- diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png deleted file mode 100644 index 8cc83bcf3f0e90228dd17a9fbbb109f0e3026ef0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 451 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsp$aOmu67n@U^m_`>-s!z7*n!SaDGBlmW?&Fd z&^54j2nb0?%qeJ?FlE8g0|!r>y>RK)gC|d)zxwdu_uqdaSW-rRXg#v zP?G_Vi?imU3eB#rMNJ7u{{0u9wN6$EC@Kw_9!r&HAZe?`qxR{-+E47aP3^!zL4E#WSK7QxU>ATTAIJT$|JKG zXH!?SpWb<@d(PtDUMDNv)@?f9$+jkSzyJN56;3A0Hu3jPI5vat+O_1VhUZ^eEENjB z(G#lID&frEdq`sP;s3Fcz0ddETI8?scJkzh&rNq+4zGFHclP`8fDl9Xcu~XfyNgc= ezDhd5l7Cu2GW7FDh7CaPF?hQAxvX~Up-Y?h0P$<&`)ryEY%d%nzD zSLmwV-QTH(90!>=S0;pZOj;4WsBeWx7O$qnEL9_eBjTxEDz_r?|NWo%@W!zfyy;h8 zz7IX(ny7Ks$AD9gtIbB>sqv}KwmbJ_uPn^aGZy-&|36K1V$Qzv-+`WE@O1TaS?83{ F1OVpOth@jK diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png deleted file mode 100644 index 61674b87aa15b34a956ebfa0d1567a3891a5b618..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 425 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4ac-5U&HxE&5rw9Yq~E<5AQ|*7G5Awb@W&(| zTO-IA$O!qG9Qr*4XiH?rt?NKbWlMtmf*BYD6m$(70s=!43d$QMOqsFZz`+w2?ml?+ z;rHKv^SPhT2P(Ye>EaktajSOHL$M|Wo>orQVByP3Tcc*a|3Bev-($m%H~uQ_$luMp z+{%H=Yt7o%G24WuvOij{ zr_EP3-m&l)r@#&4U9H!gTe~DWM4fA=IY0 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png deleted file mode 100644 index 1fbb754f79f21f9e53bcedfbb1fd1c1af49e9851..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 432 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsr&$+5ZBNjDgXcfmy}nrnBWQ| zoi+ymNeFV;83<(9E%Wg_9R?S4I~)RJdtHbClAhrLY=Mmf!&Hla<|?# z-f=s{c;03f`!AE=JPFUhmAVbD#6u4p+N3$faKk@WQ|6zp=4yS%XW0F@udQ8Z?GpX; zqS?Q_k}1D*ZaThx_v_2geb&oOwq`njxg HN@xNAi6*QB diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png deleted file mode 100644 index ebe7a5011267837056fa5e2a354e8a5d88504a9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBHIOC zj_drKHU~hsK+@P^ZpeiZ#PwgOc8WX`Cf|Sqo;_O1@pDR&e#N=Qj(^bZr-i@H$%sq-`sx4tHL~;195HfH{hTqo zHlJzMKHrmSN^@6i&0m(uXspP*CFWOJSLUY5^V0WziRPa?w_6tIHwI5vKbLh*2~7Z> C1E`b$ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png deleted file mode 100644 index f91a7a1452e06f180d3cd41dbb9852d3813b274f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFByX zPMZUO46{yWh=|M1Kp?|znUCk$a3Dj$TpJ?pb~psW@H__+_r4MdB>f-8`rV5GG6J5( z2fa%S{*)B_F$u_3w9;`q6cX|^IrMu9(7KPeez^c`S11Yc3ua&tP|(&juy6u|dURD0di#WAGfR_VmsLQMudF3Ex{flInpJW%cW z^Iw0K?+nMTcix$PWw(W*99Ud4cimrqvgHwjQhnO9eeZW4(+~)I9(i^3tQ!JTu5g{Z z`gY-)eTao@l@ zd*{8F_FY6IL;dYE@zxVvno>@RT1$I&&RX1Mx%iL#DZz;IfxQlMUN}7}=)FH(zhL`( z|FTQ(+RbH_y;xAWp>@A`x-qY_rhAj;m2+(~x12fIA@nS4yP3}nx7k%MnECFB**#TH RZv=Xg!PC{xWt~$(697~Tv`GK} diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png deleted file mode 100644 index 8e798dee8b1186839a99ca4e929fa9b32f33c4d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk8u}5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1kaQz>~`rV5GvV-0w27gKd zGVGRkhkQ*A{hk7}La4G*0BECZNswPK1A~Bqu7N{9U`RqvL3zW3DGLr9Ja*#3gXgb4 z{QhgU#(Nb|;U!NO$B>F!r4wI^H5u?UziwFA;TJG-LEyK4`kH6&1O|Lw9=(6^o1n;* zE>k0`EpnbLT{-jDIc5$;n_s?8QwkR_U45RS;_Ck3_Uq<1+q^ZTx5P`wK2Klz*i31$ zocn%q!dHtzzCYb8!vNK2P>EdAB4#rtMoAbZ5TjA=(cNZIShGynTOx%WA{;l`PZ)%IT1-M^j5H&df} v;UyI-;r2)#hmUTo1_jdXlD;S1`fV5^N<`eAsInXb`i{ZV)z4*}Q$iB}nS-Zt diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png deleted file mode 100644 index 36daa7aaabe5fe6b9258a9cee059be4fddb0384b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspJ5k5ZBNjDgXcfmy}llBB#v( zK+>$!8Nzkh83BP5UO$q`of~G#7wN2KVJ^nj?(_KO1i_iEsd_VJsNh?WH!))2Q z^QXF3d+&YKdBW11cV5*3?Isx$6>TqFhx>MF8=j{+e$@EtblUD^%f8wbV*acQ79IKr z_oYl)Egw7cu7JAS)`>qK1xu{^6Fes8+!9WxwZ$x4-x@Y0<+y6BJIZ=vi@h z@idL#pY~I{7u;1o-6-MQ?vlLi@I&r1WhJS1A2&|8krUW#bEE0`S)v1>2}Ev(Lm&*#b0Bf=D@?3he85hCxm=W4*i}2wCLMrj#WT=?yJD|#oo-U3d6}L(!UKDFm5O4|CF*X%8HCe0s`2YWnu@0VF zDjy!OH9o`SB4}|mGwQnfjHl;Q`#YjuXPyZ6@G!aLc34nM^;E;YTAw!C_1-s5JYULi zX=n1UojXjvc`)T!Z#bV8=EC*kV(*Q+M>evDCCPuEQqmu>__2#==F&*yd5Fa9u!A@J{aj8qd4m+{O0RD){AGTQM_z z>ch`s9%}zJr>g%EJ{3E$r=!!I^U;au=gE_|N1hkw&=qY`|M$lQ=s5;YS3j3^P6AURzSAr*>E7~x;Gd|0+HL{5D3Hb97x>z3P`>8wJ86Gv3~br zfb5`miT;mbgFht!*=~nIyv|32d`%Aho&vPaOqFpr&~}BAAirP+0R>$HYX^^jz>x5S zoPzR(2~!pvIC$aGgD1~lefa(7ZzH_ju%?AhGcA!TC2BG{ZhmQ)9VK}{eCEY z?el*phRnN0wQpmks<$vK|8i)b=4O>e58U08(xs2faU8LGxALWo;G&WRv%V~?^2~mCTk(V)3l64>JM1#^CAd K=d#Wzp$P!dSgWT1 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png deleted file mode 100644 index 80991f557d771f79021d8326243ebd2cbca1977f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#U`shwc%c{Uu#aNQdWB!S57a0rCqc`n@JL>Q3meFdc6X`{dY!&tw2 zF+g_EyTss6Ng-d8fo$VC2e(5ZKz8W&6rgqGrrEqe+Z9TJ{DK(-6m$)&9XtX;5>j&t z${Qw3S#aRsi3^t=Jb(V`!|%WUUY(f30aSaz)5S5Q;#TRzhfGZhJS~nVFBt`%)>*6Z z`EUG2=>V1+|5nu8%Qc^|TIKYss-ug%Qr1tI^}JH5h`*q-xH-W2RmTgH7q?$8xtaUl zRQlzfCtvp23D&+{Bb}+zu(E_a)=73^iry&CKFl;f^TdNfDO(gF_+(@V>*PCRk4=NYg3buoV~W{0UjzcF~a L`njxgN@xNA=}WPn diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png deleted file mode 100644 index a99fefcc2a34e2a834cd0e3e9eaeb21d736c9c69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llBB#v( zK+yf=fXWsg!$fyaa`x;b0Z2U z=zRsG(f?tr-@OfAt7IrL%*j0t*)rn_6NE|r6kBN zn1Mk+LD#_A!6P6rBq67uykWwW1qTkEIC1*yg-Z{fzxwdu_uqd$Wea};)!*@SaSW-r zRXQXJI7!pSsU;^-DF9j)xO zhR@bSdnfHV#N~FO&m(Yl(2>XcY`ahY4Qn;s7e3SdK;-7VyHqOXXu5Pvt~($PRj!82l*-$S|&R2>F^E`aK0`omS-9nLyhWOM?7@83Yt`4XhnJ0zwiJ za|((ZCQO;TVCjK_7cO0U@Z|Zc55NEV^ocD7Dn9S&;uunKD|g~sDJKVk*7S>EUSUFl zQn!BZKbqEdOYHf1z4yiEYIH1Eqwa4~UL!I?N^tGg{ie6#v{ z$m-vGjeYO6m%aZy=a}4K2a#=WW*Y@78UA_c_eQhu4Ws{?$9B9|1MRq0#(ArsoqWJa z^sM<)R_`s7U3@Dy2&twOIOtqkvHcs@N3|-R!>!DxO|=BKZdo|Zr7-{YS@kQoznxe$ zjny;h-Qqvit0UK$KU=%Y-Bb4AbdBdif$b8OB8p{yEPvm4k~8@^^A{bl|9VqmL6O1W M>FVdQ&MBb@0GyYs`~Uy| diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png deleted file mode 100644 index 4fded0ba158a603a1dd2aaf41ac08b43ac9d53b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsnP(S5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#W(Y%3tc^K3Yfbln>aB!S57aEQy!ARxnWqd$=JJQwb9A`Hmzy%Pf@ zy{|?2KaBOe2Vw`kOAP*$6!JA0$TqHX05U?qrvPpAY@MzRaz{y!UoeA!g06wJgGWF} zctTD=dBcP$a~CW-aPY*1I}e^bfA!(_-+$2x3ugjVpZ9ce45_$PI`OSolY)SY<4Ly`Kl8~&mWaaUHe4=}WlKj^z6@6bO9x>xx zXW_3Vw(CJna@-adZ9g9af0jc>oN^de36x%wbN$6XSK~sgTe~DWM4f2Z^u~ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png deleted file mode 100644 index d979b1b4f9723b2b709532b99d88bcc761013dc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDU$LkK;*PJKsVD0 z$grH?3K4PM8sM@s5XiP$<^vG{l5U4XTy_Ql8IBwMeQ!kr8J_3Dfu#49NDz4~%Ku@k z-@OxDNu3ZffKR&(-p zda;#Ww4Z7F?f!mj=S9<-Q-x0ir$wcRtSso; z64>!MswQcEh{dW6_os;O$T-_+_>9y4hQ6?L!y)&**W4%maaQNjsV$K&J(le;-J{`p zR^?BT^5}Is*>(zkXL6_1@6!5SA?z@D=ago}65*#dO0O7{nw1**?9%0D_T+xr!{`$t W!TbE@Q&RB9q>!)4KoPI=5fFCh_Y|NT&Teg8 z2XvBpNswPK1A~BqlCFWZgGWGMNJ36QdBcP$a~CW-aPY*1OAnqrdH(9dhu?qy{R=X> z768<8*VDx@q~ccT#G7JG20Sj#f=5^{DlOX47#;KJ|NZnJFVPE?FEjJgXPG>3RJQNB zRFY&q@44xT>((wRi_$H(ZgKNkEGQ`}V$s^lxcg6y=^LZ+{*a=1Gj=b2+kG=TOjuR! zty9zFA4~$eGmd;%YPxxGr!fDP5TnUfru!G)(x?!cx~IsVvt-M|f3Ixzcel;4XzuvD zK>eM5t6|bT_w+@oNB&>%V(PZr<+)(W!d)o^^%tI$f8>1hul#h^`t09v#R$ zYuI&?H}zq|OqYdkCTJh|(Cl=?YumMSpL1c=_m&Du`|Y~FFc;`i22WQ%mvv4FO#tny B$jATy diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png deleted file mode 100644 index 38d0ee1af9d11ae541c4170c8f62b44b71e18633..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsg?kr5ZBNjDgXcfmy}llBB#v( zK+vtzPF-*Y|nEb zHQrYuLFBb4|A(=D_hNwTfM@YR?-GMQB?W&>3i+B06tP?4?f*Cq$PWFU0(8LZ9Hs3* z_o$Tw`2{mD2q@?pSUY%l281N!6jU@!m@;j_+AUiT9K3Mp!IS5&K79E7_uoI0{S$5h zb=>lFaSW-rRXXveSd#&d%VnlVO;rY_`xU@|oQka zCpEcgJ}OQ6Z#!fy&Q&f@eqLNFwQBM16U(dItup7ae%Z3z@0QK_ez!RiL2^7UpH&T9 z&$O-BR>phLeoJBFPk~s?3AZoQ8a*?sGuyxC?(F-@*}rO4yS;X6)Sk`y^}07@^(T#u wF{izs1Szc$Xy)e<(Eb(jNO4n*aa+ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png deleted file mode 100644 index 8d745ea5ecc806c4c04388b0435b531e61ae3318..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llB2^zl zr_BLChUElTAnAQM5=c644RF~R2xQnT^YJ_z4rD-(+u;xh!}DCY$B8iCJ24)o!XP64 z4`coA#Q@m>&*FpLB?f;=3jUZB@--PKVqE9o|2PiF4*i}2v|4?0uqx0cDkVXF!3+!n z3TnCr)(##4fguTrIRy<9rYtyc@WhFe7alx+_2I+szyJQ-oANIXsQ!+pi(^Q|t#+nM^yH%R6Xp2_#s3~Lxy1Lk_EBZBiTe@V)(&^!h5LJ^T@wo~Da`Y~XdhB^ zAgk*gheqlP!^nc<{oVEpe%#l6{p+L2+XbH#Pkl5|-qw3u`eVrNr7E@$eM54)7*_LC mi9}Q%3f*dF_SNj;3iHNx$=jxx6-R*nWbkzLb6Mw<&;$T0lEBme diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png deleted file mode 100644 index 7e0b2eda3b9c6dbc62f64778e632420629efdd26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mg^)dvK zj_drKHU|J1mJ?ior1#}WqbhqK!+C3f%g#U`1CHDdhXC2Wx1xch=ecm78&N=p_mxQh z$8o-QVt{O)o6+9aqWtcG*a6Sty)H%sy-N)Klob3i2`FM*=MeZhA>?av==T($8)R~q z9|Sr{y(Gvln1Mk+L_t&6z}msXGaxV|p`c;Hlm%N496WpB(v>?89z1#V;lt10fB*IT z%S#7px$o)X7*cVobmDWNCI_CD;tmC~FB=+pxsMgS|6h})tQ>U5{>;a-c~X+eFO==O zrk$Lkz#QmVvpYTQ`5oi=5B|E&OPv}l`PjT?4e|ef1>^r z7b%;t#zV$6jE=>MhJQ13-z?aB(Ao50yN7n^9xJXTaeDDH)7^MeFP&6BpTDS4&25sq z?#UX?-kIUKC8c@ZAMLj|CfsskWxCjQUB!9J#k+s}`YziRy;qv9DZX{ZV%GceRROnS z@_W{Xxz7w|neK5~XyF{AH327TCN0;jH3&8PI>X%2S^Dk0IDx4^uQGVL`njxgN@xNA DL0P}$ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png deleted file mode 100644 index 9943eeb5dd15a0e0278ed3a1c5fdea45a04b0d92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 481 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}llBKwuT zj_drKHU|LNmJ?ior1#}WAnCj{z-4D3kl}eY97wwE4F-}x}uceyG=pu8}IkV*2R;j zozd!Xk+PY{&}do1=cqVY^l!%bH(I+7I?EnxKhg0#BKFCLJ#+ToQC~1G=w0~po3$p7 zkC;}@ac3^vKSL}^-8n>}c+pk+5{Cpc7k`!{-qrrkG|cw>5j@==HZLqFQfPbZmTKw8 z!Z~~IcQ1{9{w?s(v>aSz&SAQgA<$lok%Gl&4V`H#n$q%4!89ZJ6 KT-G@yGywnhJfLui@9p78gAzzb2zo!5laB{0qEYLk_ zB|(0{3=9GaY8tu*)(!z7aS1sE4HKp;SbE^#*$bDhJb3c_#miS8KK%asPpRa=b)b$r zo-U3d6}L(!zT|5%5NMgcC?v$i=A+xesxN=FZI}>fc|9gboFyt=akR{09E_P>Hq)$ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png deleted file mode 100644 index b6dcbdb4327f3e255b5b9b0ba482e8b02e019e39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 468 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsm1`G5ZBNjDgXcfmy}llBKwuT zPMZTDY?B5@Aj4&6AdvJt9j0Ke4dU(!g2=iZ4uLQ{&xL!O2=l!Y<8vblDB^u3637NJ z{2#{p-2*WKp2P*cOAP*$6#Ov>$W^q`@qZi#WQTlB4*i}2wEbpx(q5orR7!&Uf*BYD z6f|@V9DD;p64EmYiW??OS#aRsi4$ipT)Ok%`Ku3~e*gW)Rh<+FRDaLY#WAGfR_Vl> z)0!LvoV&vgQ;4OiN>@=+Qgr-@S@8yKp%(GhXCCe|{T9Lb^noYmkH|yfMSeXxPkpVE z>wB8rtD5+2*Ty{jrBM^%TYcLuTk=KD@4cVYoVO$-$7I^qn9!NTG4RF~R2xP#K+u;x(+xJ#9kn}tk?t3Q&$nd@r2_$`PMtfh2 z^1BBT33wLoempejU1IR3q~MQ9KoN}~W1y~(ugRg`Q-JpFZ;-MDIzzc6$S;_IK|n!U z*TCArGaxV|p`c;Hlm%N496WL1(t`(2UVZrREaktajSIV>uF5} zJS~@%I=G_Nzo?45Tk-O@{ldu;o0(q!QG9A`>>E^g?xgq8nwifzz0)Vn5^1mQ2@EZ7 z+~WAsF35}3U&GB!ylMaEEcdWfr;ZKm|L$*J=}?#TWd?_-!THVxgBj;H|6`0i-R`wv zrl{~|5ASE2PcXmpynW*Fz6SkgH@)10&$NG=VE$%NOK0&JWm`qV>ApSB?V?uprT*oe zDpv7wF;`=b@>`+vX8X*~Pptp@Us-(nYQ0+-2lrGZrmdKI;Vst04D>&TmS$7 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png deleted file mode 100644 index 9d74037d9a09ba96305d458a83fe4a5d59e4b8b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 473 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mAuC)h} zjvM`*HU|J1mJ?ioBoMjm476S74P` zuS9~#Yf=6WWBu;M0NDZ0;sc(<1-(lQ{*)B_F$pN5Xr-f@X$545d`%Aho&t2k_h~I_ zflg8@3GxeOU=Wa2(AG7ub_fWGOGqy$ZkRA-!R7-8Pn^AQ>A{oduReVE^!x9>1?RqL z0Cn8bNJ5jIaXm@8a&pZ;;r{$KyB$UGP^Vv6r-RBchZb*Kgc<`qElv8%yT#^++ zOoy2E37+3LY3HBJ)o+eiR4iH8ZS>_z(aubz|BFq(uKw(D>5}8~)t~m5Nf{env3cpC z?va}Gc3F4cxwk3z+P`s}+4Z8&;fg`-qs4CZKj%&K?mKfNR5*4fd-U(h>t1~}g_?!F zkDl+2;+nQ*HBXkvjqsf!3cnIo<=zyqKQ8{DUi#<$!sbgrZ!&ng`njxgN@xNAN*c+I diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png deleted file mode 100644 index 4b8abe16349c9c41a2ef0dcf5521760daad1c1cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspEQ;90Fu}oCpIcI2R6N_}qvBvVHHw z07?IcvDULZ{O*C+0Z-zB-X#WqN&>QVGp$0tCWn4c0oohn^|l%443&}~zhDLi0R;_h zT?2=Jkc9M{g7St56Q?X#d*I-Svls3>c>dzmhflx%x*yVB15|&@)5S5Q;#TRzmqJYr zJk75canEbeh|`eMe)HRYVWxzu!)Nwo_f;n6r7jT)6>yC-6Yg?mI&=Au<@3Gky)N+^ ztz2vqdW&tPx{Z*?&%H|3M_wi02z{=tJmIePT5(>ZSDvB)%R=tY_|JJFX5SWOYhyp# zOA~)>Q9mWRtUIT7){mt5Q@@lPs`HJz;BKtuc4Bhl21m8v=bsdJRqvnr)4t@wfyV;2 z0w<)F`Rioq)K9;^_|N;PzDK9d-0^ae^z}u#ANH8+?Vq$ICHAq#Hp{e*;5J>Z`TM$D h1#1nqdg%$X>QCG&<@>TsUkB(*22WQ%mvv4FO#pHHxKIE9 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png deleted file mode 100644 index b9896bf982f8de94c18cf4b67b7137f3362dd544..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png deleted file mode 100644 index b9896bf982f8de94c18cf4b67b7137f3362dd544..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png deleted file mode 100644 index d0777a130427a4498121e2ce215c748b927ea096..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspZhelFbOL>NTe`&yL$ z!&tw2F+gs>v-qHQiNT+ef@8= z$K|q82Y1I&)&P->S^xiY&uys^33%Q*r8;fak|k3X`FYMeUU*=t(B6O``#HteZk;j^ zRdKoQm*VQiEn>2tBV%@~>YAPoR-@dH{{vf&{OftCsc^ByHl2})_xF}7?h|ZlHCUsI zH?pi;ss61nrMLQ!-K8DpA9Po8NogFeanXL_+paO;h|K2;9v(fJuVaketh2?p{BI57 z+tU|#vXS%F4BpOXX8%n6+kdaU6z@Jw(yVsNM4SB^OLoLvzUjR`bA9P|jjq=(QgXkz m3C(tE+9MfdkWBYH{(<>pkq`^g8YIR z7z7kFbPcQ>JUjzJ64DDACQMndW$W$(2QOTD@Z|Zc4RFn7=T_Kedu zi?7;G$#{_4!Nqw*VzQl~@5a}U=lzqByXyUUvys)4*eS~EHx^#nA9wu*_nj~9r@sZB r2s^XQb=uBN6{$b2T)A~cm?b~pu#}lm@)LESM;Sa_{an^LB{Ts5qrgFht&e@qJbnhX?iI}{T5IswQI{hk7}U9(d65705H zB|(0{3=9Ga8oCD74jus^3F$cncfZMfB*d}75dN))bPmD z#WAGfR_Vmt;wA%umi`3IwHi`cxq*&rHqQG0pLzH0_zBbXjGw5TfAdUlBa2aHWN78Q zRX!U3w%)&!+@sjCSY1@8(05AHler5nJX88``E7qvsT#|ss)YZA6~@23FPWKUI-dX8 zx^%(v^DFHdYi>^F+OW2YIWtG$mqh2YcX=H$=RQXmp5NXZl4Q5p^i$k%jnJgl${Zz? zXWs;q&0T*B+e7Sh#_6yHR_Gz1>9OEzknF#bPgQu&X%Q~loCID}e B%s2o5 diff --git a/res/skins/ShadeDark1024x600-Netbook/skin.xml b/res/skins/ShadeDark1024x600-Netbook/skin.xml deleted file mode 100644 index c8ee92ab3d2..00000000000 --- a/res/skins/ShadeDark1024x600-Netbook/skin.xml +++ /dev/null @@ -1,6057 +0,0 @@ - - - - - - - - - - - - background1024x600.png - #000000 - - - - - 5,266 - - 1014,285 - vertical - - - - - - 1014,100 - horizontal - - - - - 0,0 - 254,100 - - - - - Track title Displays the artist and title of the loaded track. Informations are extracted from the tracks tags. - - [Sampler1] - 2,3 - 165,21 - - - - - - Tempo Displays the tempo of the loaded track in BPM (beats per minute) - - [Sampler1] - 164,3 - 55,21 - - right - - [Sampler1],visual_bpm - - - - - - - - Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM - - 1 - - 0 - btn_tap_sampler_over.png - btn_tap_sampler.png - - 175,5 - - [Sampler1],bpm_tap - true - - - - - - Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. - - [Sampler1] - 5,25 - 170,35 - #E4E4E4 - #00FF00 - #E4E4E4 - 80 - - [Sampler1],playposition - false - - - - - - Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 184,66 - - [Sampler1],pregain - - - - - - Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). - - btn_clipping_sampler_over.png - btn_clipping_sampler.png - 219,5 - - [Sampler1],PeakIndicator - - - - - - Channel volume meter Shows the current channel volume - - btn_volume_display_sampler_over.png - btn_volume_display_sampler.png - 219,24 - false - 5 - 500 - 50 - 2 - - [Sampler1],VuMeter - - - - - - Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value - - knob_pitch_sampler.png - slider_pitch_sampler.png - 228,8 - false - - [Sampler1],rate - false - - - - - - Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Jumps to the beginning of the track. - - 2 - - 0 - btn_play_sampler.png - btn_play_sampler.png - - - 1 - btn_play_sampler_over.png - btn_play_sampler_over.png - - 11,68 - - [Sampler1],play - true - LeftButton - - - [Sampler1],start - true - RightButton - - - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_1_over.png - btn_hotcue1_1.png - - - 1 - btn_hotcue1_1.png - btn_hotcue1_1_over.png - - 47,68 - - [Sampler1],hotcue_1_activate - true - LeftButton - false - - - [Sampler1],hotcue_1_clear - true - RightButton - false - - - [Sampler1],hotcue_1_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_2_over.png - btn_hotcue1_2.png - - - 1 - btn_hotcue1_2.png - btn_hotcue1_2_over.png - - 68,68 - - [Sampler1],hotcue_2_activate - true - LeftButton - false - - - [Sampler1],hotcue_2_clear - true - RightButton - false - - - [Sampler1],hotcue_2_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_3_over.png - btn_hotcue1_3.png - - - 1 - btn_hotcue1_3.png - btn_hotcue1_3_over.png - - 89,68 - - [Sampler1],hotcue_3_activate - true - LeftButton - false - - - [Sampler1],hotcue_3_clear - true - RightButton - false - - - [Sampler1],hotcue_3_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_4_over.png - btn_hotcue1_4.png - - - 1 - btn_hotcue1_4.png - btn_hotcue1_4_over.png - - 110,68 - - [Sampler1],hotcue_4_activate - true - LeftButton - false - - - [Sampler1],hotcue_4_clear - true - RightButton - false - - - [Sampler1],hotcue_4_enabled - false - - - - - - Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. - - 2 - - 0 - btn_pfl_sampler.png - btn_pfl_sampler.png - - - 1 - btn_pfl_sampler_over.png - btn_pfl_sampler_over.png - - 146,68 - - [Sampler1],pfl - - - - - - Eject Eject currently loaded track from deck. - - 1 - - 0 - btn_eject_sampler_over.png - btn_eject_sampler.png - - 198,23 - - [Sampler1],eject - true - LeftButton - false - - - - - - Repeat When active the track will repeat if you go past the end or reverse before the start. - - 2 - - 0 - btn_repeat_sampler.png - btn_repeat_sampler.png - - - 1 - btn_repeat_sampler_over.png - btn_repeat_sampler_over.png - - 176,23 - - [Sampler1],repeat - - - - - - Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) - - 3 - - 0 - btn_orientation_sampler_left_over.png - btn_orientation_sampler_left_over.png - - - 1 - btn_orientation_sampler_master.png - btn_orientation_sampler_master.png - - - 2 - btn_orientation_sampler_right_over.png - btn_orientation_sampler_right_over.png - - 176,41 - - [Sampler1],orientation - true - LeftButton - - - - - - Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. - - 2 - - 0 - btn_keylock_sampler.png - btn_keylock_sampler.png - - - 1 - btn_keylock_sampler_over.png - btn_keylock_sampler_over.png - - 197,41 - - [Sampler1],keylock - - - - - - 0,0 - 254,100 - - - - - Track title Displays the artist and title of the loaded track. Informations are extracted from the tracks tags. - - [Sampler2] - 2,3 - 165,21 - - - - - - Tempo Displays the tempo of the loaded track in BPM (beats per minute) - - [Sampler2] - 164,3 - 55,21 - - right - - [Sampler2],visual_bpm - - - - - - - - Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM - - 1 - - 0 - btn_tap_sampler_over.png - btn_tap_sampler.png - - 175,5 - - [Sampler2],bpm_tap - true - - - - - - Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. - - [Sampler2] - 5,25 - 170,35 - #E4E4E4 - #00FF00 - #E4E4E4 - 80 - - [Sampler2],playposition - false - - - - - - Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 184,66 - - [Sampler2],pregain - - - - - - Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). - - btn_clipping_sampler_over.png - btn_clipping_sampler.png - 219,5 - - [Sampler2],PeakIndicator - - - - - - Channel volume meter Shows the current channel volume - - btn_volume_display_sampler_over.png - btn_volume_display_sampler.png - 219,24 - false - 5 - 500 - 50 - 2 - - [Sampler2],VuMeter - - - - - - Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value - - knob_pitch_sampler.png - slider_pitch_sampler.png - 228,8 - false - - [Sampler2],rate - false - - - - - - Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Jumps to the beginning of the track. - - 2 - - 0 - btn_play_sampler.png - btn_play_sampler.png - - - 1 - btn_play_sampler_over.png - btn_play_sampler_over.png - - 11,68 - - [Sampler2],play - true - LeftButton - - - [Sampler2],start - true - RightButton - - - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_1_over.png - btn_hotcue1_1.png - - - 1 - btn_hotcue1_1.png - btn_hotcue1_1_over.png - - 47,68 - - [Sampler2],hotcue_1_activate - true - LeftButton - false - - - [Sampler2],hotcue_1_clear - true - RightButton - false - - - [Sampler2],hotcue_1_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_2_over.png - btn_hotcue1_2.png - - - 1 - btn_hotcue1_2.png - btn_hotcue1_2_over.png - - 68,68 - - [Sampler2],hotcue_2_activate - true - LeftButton - false - - - [Sampler2],hotcue_2_clear - true - RightButton - false - - - [Sampler2],hotcue_2_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_3_over.png - btn_hotcue1_3.png - - - 1 - btn_hotcue1_3.png - btn_hotcue1_3_over.png - - 89,68 - - [Sampler2],hotcue_3_activate - true - LeftButton - false - - - [Sampler2],hotcue_3_clear - true - RightButton - false - - - [Sampler2],hotcue_3_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_4_over.png - btn_hotcue1_4.png - - - 1 - btn_hotcue1_4.png - btn_hotcue1_4_over.png - - 110,68 - - [Sampler2],hotcue_4_activate - true - LeftButton - false - - - [Sampler2],hotcue_4_clear - true - RightButton - false - - - [Sampler2],hotcue_4_enabled - false - - - - - - Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. - - 2 - - 0 - btn_pfl_sampler.png - btn_pfl_sampler.png - - - 1 - btn_pfl_sampler_over.png - btn_pfl_sampler_over.png - - 146,68 - - [Sampler2],pfl - - - - - - Eject Eject currently loaded track from deck. - - 1 - - 0 - btn_eject_sampler_over.png - btn_eject_sampler.png - - 198,23 - - [Sampler2],eject - true - LeftButton - false - - - - - - Repeat When active the track will repeat if you go past the end or reverse before the start. - - 2 - - 0 - btn_repeat_sampler.png - btn_repeat_sampler.png - - - 1 - btn_repeat_sampler_over.png - btn_repeat_sampler_over.png - - 176,23 - - [Sampler2],repeat - - - - - - Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) - - 3 - - 0 - btn_orientation_sampler_left_over.png - btn_orientation_sampler_left_over.png - - - 1 - btn_orientation_sampler_master.png - btn_orientation_sampler_master.png - - - 2 - btn_orientation_sampler_right_over.png - btn_orientation_sampler_right_over.png - - 176,41 - - [Sampler2],orientation - true - LeftButton - - - - - - Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. - - 2 - - 0 - btn_keylock_sampler.png - btn_keylock_sampler.png - - - 1 - btn_keylock_sampler_over.png - btn_keylock_sampler_over.png - - 197,41 - - [Sampler2],keylock - - - - - - 0,0 - 254,100 - - - - - Track title Displays the artist and title of the loaded track. Informations are extracted from the tracks tags. - - [Sampler3] - 2,3 - 165,21 - - - - - - Tempo Displays the tempo of the loaded track in BPM (beats per minute) - - [Sampler3] - 164,3 - 55,21 - - right - - [Sampler3],visual_bpm - - - - - - - - Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM - - 1 - - 0 - btn_tap_sampler_over.png - btn_tap_sampler.png - - 175,5 - - [Sampler3],bpm_tap - true - - - - - - Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. - - [Sampler3] - 5,25 - 170,35 - #E4E4E4 - #00FF00 - #E4E4E4 - 80 - - [Sampler3],playposition - false - - - - - - Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 184,66 - - [Sampler3],pregain - - - - - - Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). - - btn_clipping_sampler_over.png - btn_clipping_sampler.png - 219,5 - - [Sampler3],PeakIndicator - - - - - - Channel volume meter Shows the current channel volume - - btn_volume_display_sampler_over.png - btn_volume_display_sampler.png - 219,24 - false - 5 - 500 - 50 - 2 - - [Sampler3],VuMeter - - - - - - Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value - - knob_pitch_sampler.png - slider_pitch_sampler.png - 228,8 - false - - [Sampler3],rate - false - - - - - - Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Jumps to the beginning of the track. - - 2 - - 0 - btn_play_sampler.png - btn_play_sampler.png - - - 1 - btn_play_sampler_over.png - btn_play_sampler_over.png - - 11,68 - - [Sampler3],play - true - LeftButton - - - [Sampler3],start - true - RightButton - - - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_1_over.png - btn_hotcue1_1.png - - - 1 - btn_hotcue1_1.png - btn_hotcue1_1_over.png - - 47,68 - - [Sampler3],hotcue_1_activate - true - LeftButton - false - - - [Sampler3],hotcue_1_clear - true - RightButton - false - - - [Sampler3],hotcue_1_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_2_over.png - btn_hotcue1_2.png - - - 1 - btn_hotcue1_2.png - btn_hotcue1_2_over.png - - 68,68 - - [Sampler3],hotcue_2_activate - true - LeftButton - false - - - [Sampler3],hotcue_2_clear - true - RightButton - false - - - [Sampler3],hotcue_2_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_3_over.png - btn_hotcue1_3.png - - - 1 - btn_hotcue1_3.png - btn_hotcue1_3_over.png - - 89,68 - - [Sampler3],hotcue_3_activate - true - LeftButton - false - - - [Sampler3],hotcue_3_clear - true - RightButton - false - - - [Sampler3],hotcue_3_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_4_over.png - btn_hotcue1_4.png - - - 1 - btn_hotcue1_4.png - btn_hotcue1_4_over.png - - 110,68 - - [Sampler3],hotcue_4_activate - true - LeftButton - false - - - [Sampler3],hotcue_4_clear - true - RightButton - false - - - [Sampler3],hotcue_4_enabled - false - - - - - - Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. - - 2 - - 0 - btn_pfl_sampler.png - btn_pfl_sampler.png - - - 1 - btn_pfl_sampler_over.png - btn_pfl_sampler_over.png - - 146,68 - - [Sampler3],pfl - - - - - - Eject Eject currently loaded track from deck. - - 1 - - 0 - btn_eject_sampler_over.png - btn_eject_sampler.png - - 198,23 - - [Sampler3],eject - true - LeftButton - false - - - - - - Repeat When active the track will repeat if you go past the end or reverse before the start. - - 2 - - 0 - btn_repeat_sampler.png - btn_repeat_sampler.png - - - 1 - btn_repeat_sampler_over.png - btn_repeat_sampler_over.png - - 176,23 - - [Sampler3],repeat - - - - - - Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) - - 3 - - 0 - btn_orientation_sampler_left_over.png - btn_orientation_sampler_left_over.png - - - 1 - btn_orientation_sampler_master.png - btn_orientation_sampler_master.png - - - 2 - btn_orientation_sampler_right_over.png - btn_orientation_sampler_right_over.png - - 176,41 - - [Sampler3],orientation - true - LeftButton - - - - - - Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. - - 2 - - 0 - btn_keylock_sampler.png - btn_keylock_sampler.png - - - 1 - btn_keylock_sampler_over.png - btn_keylock_sampler_over.png - - 197,41 - - [Sampler3],keylock - - - - - - 0,0 - 252,100 - - - - - Track title Displays the artist and title of the loaded track. Informations are extracted from the tracks tags. - - [Sampler4] - 2,3 - 165,21 - - - - - - Tempo Displays the tempo of the loaded track in BPM (beats per minute) - - [Sampler4] - 164,3 - 55,21 - - right - - [Sampler4],visual_bpm - - - - - - - - Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM - - 1 - - 0 - btn_tap_sampler_over.png - btn_tap_sampler.png - - 175,5 - - [Sampler4],bpm_tap - true - - - - - - Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. - - [Sampler4] - 5,25 - 170,35 - #E4E4E4 - #00FF00 - #E4E4E4 - 80 - - [Sampler4],playposition - false - - - - - - Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 184,66 - - [Sampler4],pregain - - - - - - Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). - - btn_clipping_sampler_over.png - btn_clipping_sampler.png - 219,5 - - [Sampler4],PeakIndicator - - - - - - Channel volume meter Shows the current channel volume - - btn_volume_display_sampler_over.png - btn_volume_display_sampler.png - 219,24 - false - 5 - 500 - 50 - 2 - - [Sampler4],VuMeter - - - - - - Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value - - knob_pitch_sampler.png - slider_pitch_sampler.png - 228,8 - false - - [Sampler4],rate - false - - - - - - Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Jumps to the beginning of the track. - - 2 - - 0 - btn_play_sampler.png - btn_play_sampler.png - - - 1 - btn_play_sampler_over.png - btn_play_sampler_over.png - - 11,68 - - [Sampler4],play - true - LeftButton - - - [Sampler4],start - true - RightButton - - - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_1_over.png - btn_hotcue1_1.png - - - 1 - btn_hotcue1_1.png - btn_hotcue1_1_over.png - - 47,68 - - [Sampler4],hotcue_1_activate - true - LeftButton - false - - - [Sampler4],hotcue_1_clear - true - RightButton - false - - - [Sampler4],hotcue_1_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_2_over.png - btn_hotcue1_2.png - - - 1 - btn_hotcue1_2.png - btn_hotcue1_2_over.png - - 68,68 - - [Sampler4],hotcue_2_activate - true - LeftButton - false - - - [Sampler4],hotcue_2_clear - true - RightButton - false - - - [Sampler4],hotcue_2_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_3_over.png - btn_hotcue1_3.png - - - 1 - btn_hotcue1_3.png - btn_hotcue1_3_over.png - - 89,68 - - [Sampler4],hotcue_3_activate - true - LeftButton - false - - - [Sampler4],hotcue_3_clear - true - RightButton - false - - - [Sampler4],hotcue_3_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_4_over.png - btn_hotcue1_4.png - - - 1 - btn_hotcue1_4.png - btn_hotcue1_4_over.png - - 110,68 - - [Sampler4],hotcue_4_activate - true - LeftButton - false - - - [Sampler4],hotcue_4_clear - true - RightButton - false - - - [Sampler4],hotcue_4_enabled - false - - - - - - Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. - - 2 - - 0 - btn_pfl_sampler.png - btn_pfl_sampler.png - - - 1 - btn_pfl_sampler_over.png - btn_pfl_sampler_over.png - - 146,68 - - [Sampler4],pfl - - - - - - Eject Eject currently loaded track from deck. - - 1 - - 0 - btn_eject_sampler_over.png - btn_eject_sampler.png - - 198,23 - - [Sampler4],eject - true - LeftButton - false - - - - - - Repeat When active the track will repeat if you go past the end or reverse before the start. - - 2 - - 0 - btn_repeat_sampler.png - btn_repeat_sampler.png - - - 1 - btn_repeat_sampler_over.png - btn_repeat_sampler_over.png - - 176,23 - - [Sampler4],repeat - - - - - - Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) - - 3 - - 0 - btn_orientation_sampler_left_over.png - btn_orientation_sampler_left_over.png - - - 1 - btn_orientation_sampler_master.png - btn_orientation_sampler_master.png - - - 2 - btn_orientation_sampler_right_over.png - btn_orientation_sampler_right_over.png - - 176,41 - - [Sampler4],orientation - true - LeftButton - - - - - - Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. - - 2 - - 0 - btn_keylock_sampler.png - btn_keylock_sampler.png - - - 1 - btn_keylock_sampler_over.png - btn_keylock_sampler_over.png - - 197,41 - - [Sampler4],keylock - - - - - - - [Samplers],show_samplers - visible - - - - - - 0,0 - 1012,i - horizontal - - - - - - - - - - - - - - - 384,6 - 255,255 - - - - - - 0,0 - 250,250 - - - - - Microphone Show/hide the microphone widget. - - 2 - - 0 - tab_microphone.png - tab_microphone.png - - - 1 - tab_microphone_over.png - tab_microphone_over.png - - 68,77 - - [Microphone],show_microphone - - - - - - Sampler Show/hide the Sampler widget. - - 2 - - 0 - tab_sampler.png - tab_sampler.png - - - 1 - tab_sampler_over.png - tab_sampler_over.png - - 105,77 - - [Samplers],show_samplers - - - - - - - Vinyl Control Show/hide the Vinyl Control widget. Activate Vinyl Control from the Menu→Options - - 2 - - 0 - tab_vinylcontrol.png - tab_vinylcontrol.png - - - 1 - tab_vinylcontrol_over.png - tab_vinylcontrol_over.png - - 160,77 - - [Vinylcontrol],show_vinylcontrol - - - - - - Volume control Adjust the volume of the selected channel. Right-click: Reset to default value - - knob_volume1.png - slider_volume1.png - 72,91 - false - - [Channel1],volume - false - - - - Volume control Adjust the volume of the selected channel. Right-click: Reset to default value - - knob_volume2.png - slider_volume2.png - 168,91 - false - - [Channel2],volume - false - - - - - - Crossfader Fade between the channels and define what you hear through the master output. Right-click: Reset to default value Change the crossfader curve in Preferences→Crossfader - - knob_crossfader.png - slider_crossfader.png - 72,230 - true - - [Master],crossfader - false - - - - - - Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Places a Cue-point at the current position on the waveform. - - 2 - true - - 0 - btn_play1.png - btn_play1.png - - - 1 - btn_play1_over.png - btn_play1_over.png - - 11,226 - - [Channel1],play - true - LeftButton - - - [Channel1],cue_set - true - RightButton - false - - - - Play/Pause Left-click: Toggles playing or pausing the track. Right-click: Places a Cue-point at the current position on the waveform. - - 2 - true - - 0 - btn_play2.png - btn_play2.png - - - 1 - btn_play2_over.png - btn_play2_over.png - - 203,226 - - [Channel2],play - true - LeftButton - - - [Channel2],cue_set - true - RightButton - false - - - - - - Cue Left-click (while playing): The track will seek to the cue-point and stop (=CDJ) OR play (=simple). Change the default cue behaviour in Preferences→Interface. Left-click (while stopped): Places a cue-point at the current position on the waveform. Right-click: The track will seek to the cue-point and stop. - - 1 - - 0 - btn_cue1_over.png - btn_cue1.png - - 11,205 - - [Channel1],cue_default - true - LeftButton - - - [Channel1],cue_gotoandstop - true - RightButton - - - - Cue Left-click (while playing): The track will seek to the cue-point and stop (=CDJ) OR play (=simple). Change the default cue behaviour in Preferences→Interface. Left-click (while stopped): Places a cue-point at the current position on the waveform. Right-click: The track will seek to the cue-point and stop. - - 1 - - 0 - btn_cue2_over.png - btn_cue2.png - - 203,205 - - [Channel2],cue_default - true - LeftButton - - - [Channel2],cue_gotoandstop - true - RightButton - - - - - - Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. - - 2 - - 0 - btn_pfl1.png - btn_pfl1.png - - - 1 - btn_pfl1_over.png - btn_pfl1_over.png - - 64,197 - - [Channel1],pfl - - - - Headphone Sends the selected channel's audio to the Headphones output audio device selected in Preferences→Sound Hardware. - - 2 - - 0 - btn_pfl2.png - btn_pfl2.png - - - 1 - btn_pfl2_over.png - btn_pfl2_over.png - - 160,197 - - [Channel2],pfl - - - - - - Flanger Toggles the flange effect. Use the depth/delay/lfo knobs to adjust - - 2 - - 0 - btn_fx1.png - btn_fx1.png - - - 1 - btn_fx1_over.png - btn_fx1_over.png - - 11,49 - - [Channel1],flanger - - - - Flanger Toggles the flange effect. Use the depth/delay/lfo knobs to adjust - - 2 - - 0 - btn_fx2.png - btn_fx1.png - - - 1 - btn_fx2_over.png - btn_fx2_over.png - - 214,49 - - [Channel2],flanger - - - - - - High EQ kill Holds the gain of the high EQ to zero while active. - - 2 - - 0 - btn_kill_over.png - btn_kill.png - - - 1 - btn_kill.png - btn_kill_over.png - - 11,120 - - [Channel1],filterHighKill - true - LeftButton - - - - Mid EQ kill Holds the gain of the mid EQ to zero while active. - - 2 - - 0 - btn_kill_over.png - btn_kill.png - - - 1 - btn_kill.png - btn_kill_over.png - - 11,149 - - [Channel1],filterMidKill - true - LeftButton - - - - Low EQ kill Holds the gain of the low EQ to zero while active. - - 2 - - 0 - btn_kill_over.png - btn_kill.png - - - 1 - btn_kill.png - btn_kill_over.png - - 11,178 - - [Channel1],filterLowKill - true - LeftButton - - - - - High EQ kill Holds the gain of the high EQ to zero while active. - - 2 - - 0 - btn_kill_over.png - btn_kill.png - - - 1 - btn_kill.png - btn_kill_over.png - - 229,120 - - [Channel2],filterHighKill - true - LeftButton - - - - Mid EQ kill Holds the gain of the mid EQ to zero while active. - - 2 - - 0 - btn_kill_over.png - btn_kill.png - - - 1 - btn_kill.png - btn_kill_over.png - - 229,149 - - [Channel2],filterMidKill - true - LeftButton - - - - Low EQ kill Holds the gain of the low EQ to zero while active. - - 2 - - 0 - btn_kill_over.png - btn_kill.png - - - 1 - btn_kill.png - btn_kill_over.png - - 229,178 - - [Channel2],filterLowKill - true - LeftButton - - - - - - Master volume Adjusts the Master output volume. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 208,15 - - [Master],volume - - - - Balance Adjusts the left/right channel balance on the Master output. Right-click: Reset to default value - - 63 - knobs/knob_rotary_s%1.png - 145,15 - - [Master],balance - - - - - - Headphone volume Adjusts the headphone output volume. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 20,15 - - [Master],headVolume - - - - Headphone mix Controls what you hear on the headphone output. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 82,15 - - [Master],headMix - - - - - - Flanger delay Adjusts the phase delay of the flange effect (when active). Right-click: Reset to default value - - 63 - knobs/knob_rotary_s%1.png - 171,51 - - [Flanger],lfoDelay - - - - Flanger depth Adjusts the intensity of the flange effect (when active). Right-click: Reset to default value - - 63 - knobs/knob_rotary_s%1.png - 114,51 - - [Flanger],lfoDepth - - - - Flanger LFO period Adjusts the wavelength of the flange effect (when active). Right-click: Reset to default value - - 63 - knobs/knob_rotary_s%1.png - 57,51 - - [Flanger],lfoPeriod - - - - - - Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 32,83 - - [Channel1],pregain - - - - High EQ Adjusts the gain of the high EQ filter. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 32,112 - - [Channel1],filterHigh - - - - Mid EQ Adjusts the gain of the mid EQ filter. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 32,141 - - [Channel1],filterMid - - - - Low EQ Adjusts the gain of the low EQ filter. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 32,170 - - [Channel1],filterLow - - - - - Gain Adjusts the pre-fader gain of the track (to avoid clipping) Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 196,83 - - [Channel2],pregain - - - - High EQ Adjusts the gain of the high EQ filter. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 196,112 - - [Channel2],filterHigh - - - - Mid EQ Adjusts the gain of the mid EQ filter. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 196,141 - - [Channel2],filterMid - - - - Low EQ Adjusts the gain of the low EQ filter. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 196,170 - - [Channel2],filterLow - - - - - - Channel volume meter Shows the current channel volume - - btn_volume_display1_over.png - btn_volume_display1.png - 107,112 - false - 5 - 500 - 50 - 2 - - [Channel1],VuMeter - - - - Channel volume meter Shows the current channel volume - - btn_volume_display2_over.png - btn_volume_display2.png - 143,112 - false - 5 - 500 - 50 - 2 - - [Channel2],VuMeter - - - - - Master channel volume meter Outputs the current instantaneous master volume for the left channel. - - btn_volume_display_master1_over.png - btn_volume_display_master1.png - 122,112 - 5 - 500 - 50 - 2 - - [Master],VuMeterL - - - - Master channel volume meter Outputs the current instantaneous master volume for the right channel. - - btn_volume_display_master2_over.png - btn_volume_display_master2.png - 128,112 - 5 - 500 - 50 - 2 - - [Master],VuMeterR - - - - - - Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). - - btn_clipping1_over.png - btn_clipping1.png - 107,93 - - [Channel1],PeakIndicator - - - - Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). - - btn_clipping2_over.png - btn_clipping2.png - 143,93 - - [Channel2],PeakIndicator - - - - Master Peak Indicator Indicates when the signal on the Master output is clipping, (too loud for the hardware and is being distorted). - - btn_clipping_master_over.png - btn_clipping_master.png - 122,93 - - [Master],PeakIndicator - - - - - - - - - - - 8,8 - 370,250 - - - - - 370,250 - - - - - Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected - - 3 - btn_vinylcontrol_indicator_horizontal1.png - btn_vinylcontrol_indicator_horizontal2.png - btn_vinylcontrol_indicator_horizontal3.png - -1,0 - - [Channel1],vinylcontrol_status - - - - Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected - - 3 - btn_vinylcontrol_indicator_horizontal1.png - btn_vinylcontrol_indicator_horizontal2.png - btn_vinylcontrol_indicator_horizontal3.png - -1,248 - - [Channel1],vinylcontrol_status - - - - Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected - - 3 - btn_vinylcontrol_indicator_vertical1.png - btn_vinylcontrol_indicator_vertical2.png - btn_vinylcontrol_indicator_vertical3.png - 0,0 - - [Channel1],vinylcontrol_status - - - - Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected - - 3 - btn_vinylcontrol_indicator_vertical1.png - btn_vinylcontrol_indicator_vertical2.png - btn_vinylcontrol_indicator_vertical3.png - 367,0 - - [Channel1],vinylcontrol_status - - - - - - 2,43 - 318,118 - horizontal - - - - - - - Waveform display Shows the loaded tracks' waveforms near the playback position. Left-click: Use the mouse to scratch, halt, spin back and push forward a track. Right-click: Drag with mouse to make temporary pitch adjustments. Drop tracks from library or external file manager here. - - 1 - 0,0 - - - #3F4249 - - #E4E4E4 - #E4E4E4 - #565E6B - #565E6B - #00FF00 - #FFFF00 - - loop_start_position - loop_end_position - loop_enabled - #00FF00 - #BCDBFB - - - loop_start_position - LOOP IN - top - #00FF00 - #FFFFFF - - - loop_end_position - LOOP OUT - top - #00FF00 - #FFFFFF - - - - hotcue_1_position - marker_hotcue1_1.png - HOTCUE 1 - bottom - #3FC7FA - #FFFFFF - - - hotcue_2_position - marker_hotcue1_2.png - HOTCUE 2 - bottom - #3FC7FA - #FFFFFF - - - hotcue_3_position - marker_hotcue1_3.png - HOTCUE 3 - bottom - #3FC7FA - #FFFFFF - - - hotcue_4_position - marker_hotcue1_4.png - HOTCUE 4 - bottom - #3FC7FA - #FFFFFF - - - hotcue_5_position - HOTCUE 5 - center - #AE5CFF - #FFFFFF - - - hotcue_6_position - HOTCUE 6 - center - #AE5CFF - #FFFFFF - - - hotcue_7_position - HOTCUE 7 - center - #AE5CFF - #FFFFFF - - - hotcue_8_position - HOTCUE 8 - center - #AE5CFF - #FFFFFF - - - hotcue_9_position - HOTCUE 9 - center - #AE5CFF - #FFFFFF - - - hotcue_10_position - HOTCUE 10 - center - #AE5CFF - #FFFFFF - - - hotcue_11_position - HOTCUE 11 - center - #AE5CFF - #FFFFFF - - - hotcue_12_position - HOTCUE 12 - center - #AE5CFF - #FFFFFF - - - hotcue_13_position - HOTCUE 13 - center - #AE5CFF - #FFFFFF - - - hotcue_14_position - HOTCUE 14 - center - #AE5CFF - #FFFFFF - - - hotcue_15_position - HOTCUE 15 - center - #AE5CFF - #FFFFFF - - - hotcue_16_position - HOTCUE 16 - center - #AE5CFF - #FFFFFF - - - hotcue_17_position - HOTCUE 17 - center - #AE5CFF - #FFFFFF - - - hotcue_18_position - HOTCUE 18 - center - #AE5CFF - #FFFFFF - - - hotcue_19_position - HOTCUE 19 - center - #AE5CFF - #FFFFFF - - - hotcue_20_position - HOTCUE 20 - center - #AE5CFF - #FFFFFF - - - hotcue_21_position - HOTCUE 21 - center - #AE5CFF - #FFFFFF - - - hotcue_22_position - HOTCUE 22 - center - #AE5CFF - #FFFFFF - - - hotcue_23_position - HOTCUE 23 - center - #AE5CFF - #FFFFFF - - - hotcue_24_position - HOTCUE 24 - center - #AE5CFF - #FFFFFF - - - hotcue_25_position - HOTCUE 25 - center - #AE5CFF - #FFFFFF - - - hotcue_26_position - HOTCUE 26 - center - #AE5CFF - #FFFFFF - - - hotcue_27_position - HOTCUE 27 - center - #AE5CFF - #FFFFFF - - - hotcue_28_position - HOTCUE 28 - center - #AE5CFF - #FFFFFF - - - hotcue_29_position - HOTCUE 29 - center - #AE5CFF - #FFFFFF - - - hotcue_30_position - HOTCUE 30 - center - #AE5CFF - #FFFFFF - - - hotcue_31_position - HOTCUE 31 - center - #AE5CFF - #FFFFFF - - - hotcue_32_position - HOTCUE 32 - center - #AE5CFF - #FFFFFF - - - hotcue_33_position - HOTCUE 33 - center - #AE5CFF - #FFFFFF - - - hotcue_34_position - HOTCUE 34 - center - #AE5CFF - #FFFFFF - - - hotcue_35_position - HOTCUE 35 - center - #AE5CFF - #FFFFFF - - - hotcue_36_position - HOTCUE 36 - center - #AE5CFF - #FFFFFF - - - cue_point - marker_cue1.png - CUE - bottom - #FF001C - #FFFFFF - - - - - - - vertical - - - - - - 88,82 - - - - Spinning vinyl Rotates during playback and shows the position of a track. Use the mouse to scratch, halt, spin back and push forward a track. Drop tracks from library or external file manager here. If Vinyl control is enabled, it can display the timecoded vinyls signal quality (see Preferences→Vinyl Control). - - 1 - 0,0 - i,i - vinyl_spinny1_background.png - vinyl_spinny1_foreground.png - vinyl_spinny1_foreground_ghost.png - - - - [Spinny1],show_spinny - visible - - - - - 0,0 - 88,35 - - - - - - - - - Vinyl Control Mode Absolute mode - track position equals needle position and speed Relative mode - track speed equals needle speed regardless of needle position Constant mode - track speed equals last known-steady speed regardless of needle input - - 3 - - 0 - btn_vinylcontrol_mode_abs1.png - btn_vinylcontrol_mode_abs1.png - - - 1 - btn_vinylcontrol_mode_rel1.png - btn_vinylcontrol_mode_rel1.png - - - 2 - btn_vinylcontrol_mode_const1.png - btn_vinylcontrol_mode_const1.png - - -1,2 - - [Channel1],vinylcontrol_mode - - - - - Vinyl Cueing Mode Determines how cue points are treated in vinyl control Relative mode Off - Cue points ignored One Cue - If needle is dropped after the cue point, track will seek to that cue point Hot Cue - Track will seek to nearest previous hot cue point - - 3 - - 0 - btn_vinylcontrol_cueing_off1.png - btn_vinylcontrol_cueing_off1.png - - - 1 - btn_vinylcontrol_cueing_one1.png - btn_vinylcontrol_cueing_one1.png - - - 2 - btn_vinylcontrol_cueing_hot1.png - btn_vinylcontrol_cueing_hot1.png - - 44,2 - - [Channel1],vinylcontrol_cueing - - - - - [Vinylcontrol],show_vinylcontrol - visible - - - - - - - - - - - Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. - - 1 - 2,161 - 250,37 - - #E4E4E4 - #00FF00 - #E4E4E4 - 80 - - [Channel1],playposition - false - - - - - - Track title Displays the title of the loaded track. Informations are extracted from the tracks tags. - - title - 1 - 0,4 - 230,18 - - - - - Track Artist Displays the artist of the loaded track. Informations are extracted from the tracks tags. - - artist - 1 - 0,24 - 210,18 - - - - - Tempo Displays the tempo of the loaded track in BPM (beats per minute) - - 1 - 245,4 - 70,18 - - right - - [Channel1],visual_bpm - - - - - - Time Displays the elapsed or remaining time of the track loaded. Click to toggle between time elapsed/remaining time. - - 1 - 215,24 - 100,18 - - right - - [Channel1],playposition - - - - - - - - Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM - - 1 - - 0 - btn_tap1_over.png - btn_tap1.png - - 232,2 - - [Channel1],bpm_tap - true - - - - - - Pitch rate Displays the current pitch of the track based on the original tempo. - - 1 - 316,28 - 50,14 - - center - - - - - Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value - - knob_pitch1.png - slider_pitch1.png - 335,45 - false - - [Channel1],rate - false - - - - - - Syncronize Left-click: Syncs the tempo (BPM) and phase to that of the other track, if BPM is detected on both Right-click: Syncs the tempo (BPM) to that of the other track, if BPM is detected on both - - 1 - - 0 - btn_sync1_over.png - btn_sync1.png - - 321,7 - - [Channel1],beatsync - true - LeftButton - - - [Channel1],beatsync_tempo - true - RightButton - - - - - - Raise pitch Left-click: Sets the pitch higher Right-click: Sets the pitch higher in small steps. Change the the amount of the steps in Preferences→Interface menu. - - 1 - - 0 - btn_pitch_up1_over.png - btn_pitch_up1.png - - 342,153 - - [Channel1],rate_perm_up - true - LeftButton - - - [Channel1],rate_perm_up_small - true - RightButton - - - - Lower pitch Left-click: Sets the pitch lower Right-click: Sets the pitch lower in small steps. Change the the amount of the steps in Preferences→Interface menu. - - 1 - - 0 - btn_pitch_down1_over.png - btn_pitch_down1.png - - 321,153 - - [Channel1],rate_perm_down - true - LeftButton - - - [Channel1],rate_perm_down_small - true - RightButton - - - - - - Raise pitch temporary (nudge) Left-click: Holds the pitch higher while active Right-click: Holds the pitch higher (small amount) while active Change the amount of the steps in Preferences→Interface - - 1 - - 0 - btn_nudge_up1_over.png - btn_nudge_up1.png - - 342,174 - - [Channel1],rate_temp_up - true - LeftButton - - - [Channel1],rate_temp_up_small - true - RightButton - - - - Lower pitch temporary (nudge) Left-click: Holds the pitch lower while active Right-click: Holds the pitch lower (small amount) while active Change the amount of the steps in Preferences→Interface. - - 1 - - 0 - btn_nudge_down1_over.png - btn_nudge_down1.png - - 321,174 - - [Channel1],rate_temp_down - true - LeftButton - - - [Channel1],rate_temp_down_small - true - RightButton - - - - - - Spinning vinyl Show/hide the spinning vinyl widget - - 2 - - 0 - btn_spinny1.png - btn_spinny1.png - - - 1 - btn_spinny1_over.png - btn_spinny1_over.png - - 253,161 - - [Spinny1],show_spinny - true - LeftButton - - - - - - Adjust beatgrid Adjust beatgrid so closest beat is aligned with the current playposition. - - 1 - - 0 - btn_beatgrid1_over.png - btn_beatgrid1.png - - 253,180 - - [Channel1],beats_translate_curpos - true - LeftButton - - - - - - Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. - - 2 - - 0 - btn_keylock1.png - btn_keylock1.png - - - 1 - btn_keylock1_over.png - btn_keylock1_over.png - - 295,180 - - [Channel1],keylock - - - - - - Quantize Toggles quantization in loops and cues - - 2 - - 0 - btn_quantize1.png - btn_quantize1.png - - - 1 - btn_quantize1_over.png - btn_quantize1_over.png - - 274,180 - - [Channel1],quantize - true - LeftButton - - - - - - Repeat When active the track will repeat if you go past the end or reverse before the start. - - 2 - - 0 - btn_repeat1.png - btn_repeat1.png - - - 1 - btn_repeat1_over.png - btn_repeat1_over.png - - 274,161 - - [Channel1],repeat - - - - - - Eject Eject currently loaded track from deck. - - 1 - - 0 - btn_eject1_over.png - btn_eject1.png - - 295,161 - - [Channel1],eject - true - LeftButton - false - - - - - - 86,204 - 277,43 - - - - - Fast Forward Left-click: Fast forward through the track. Right-click: Jumps to the end of the track. - - 1 - - 0 - btn_forward1_over.png - btn_forward1.png - - 21,0 - - [Channel1],fwd - true - LeftButton - - - [Channel1],end - true - RightButton - - - - Fast Rewind Left-click: Fast rewind through the track. Right-click: Jumps to the beginning of the track. - - 1 - - 0 - btn_rewind1_over.png - btn_rewind1.png - - 0,0 - - [Channel1],back - true - LeftButton - - - [Channel1],start - true - RightButton - - - - - - Reverse Toggles reverse playback when pressed during regular playback - - 1 - - 0 - btn_reverse1_over.png - btn_reverse1.png - - 0,21 - - [Channel1],reverse - true - LeftButton - - - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop1_0125.png - btn_beatloop1_0125.png - - - 1 - btn_beatloop1_0125_over.png - btn_beatloop1_0125_over.png - - 72,0 - - [Channel1],beatloop_0.125 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop1_0250.png - btn_beatloop1_0250.png - - - 1 - btn_beatloop1_0250_over.png - btn_beatloop1_0250_over.png - - 93,0 - - [Channel1],beatloop_0.25 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop1_0500.png - btn_beatloop1_0500.png - - - 1 - btn_beatloop1_0500_over.png - btn_beatloop1_0500_over.png - - 114,0 - - [Channel1],beatloop_0.5 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop1_1.png - btn_beatloop1_1.png - - - 1 - btn_beatloop1_1_over.png - btn_beatloop1_1_over.png - - 135,0 - - [Channel1],beatloop_1 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop1_2.png - btn_beatloop1_2.png - - - 1 - btn_beatloop1_2_over.png - btn_beatloop1_2_over.png - - 72,21 - - [Channel1],beatloop_2 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop1_4.png - btn_beatloop1_4.png - - - 1 - btn_beatloop1_4_over.png - btn_beatloop1_4_over.png - - 93,21 - - [Channel1],beatloop_4 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop1_8_over.png - btn_beatloop1_8.png - - - 1 - btn_beatloop1_8.png - btn_beatloop1_8_over.png - - 114,21 - - [Channel1],beatloop_8 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop1_16.png - btn_beatloop1_16.png - - - 1 - btn_beatloop1_16_over.png - btn_beatloop1_16_over.png - - 135,21 - - [Channel1],beatloop_16 - true - LeftButton - - - - - - Loop halve Halves the current loop's length by moving the end marker. Deck immediately loops if past the new endpoint. - - 1 - - 0 - btn_beatloop1_halve_over.png - btn_beatloop1_halve.png - - 50,0 - - [Channel1],loop_halve - true - LeftButton - - - - Loop double Doubles the current loop's length by moving the end marker. - - 1 - - 0 - btn_beatloop1_double_over.png - btn_beatloop1_double.png - - 157,0 - - [Channel1],loop_double - true - LeftButton - - - - - - Loop-In marker Sets the deck loop-in position to the current play position. - - 1 - - 0 - btn_loop_in1_over.png - btn_loop_in1.png - - 186,0 - - [Channel1],loop_in - true - LeftButton - - - - Loop-Out marker Sets the deck loop-out position to the current play position. - - 1 - - 0 - btn_loop_out1_over.png - btn_loop_out1.png - - 207,0 - - [Channel1],loop_out - true - LeftButton - - - - Reloop/Exit Toggles the current loop on or off. Works only if Loop-In and Loop-Out marker are set. - - 2 - true - - 0 - btn_reloop1.png - btn_reloop1.png - - - 1 - btn_reloop1_over.png - btn_reloop1_over.png - - 186,21 - - [Channel1],reloop_exit - true - LeftButton - false - - - [Channel1],loop_enabled - false - - - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_1_over.png - btn_hotcue1_1.png - - - 1 - btn_hotcue1_1.png - btn_hotcue1_1_over.png - - 236,0 - - [Channel1],hotcue_1_activate - true - LeftButton - false - - - [Channel1],hotcue_1_clear - true - RightButton - false - - - [Channel1],hotcue_1_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_2_over.png - btn_hotcue1_2.png - - - 1 - btn_hotcue1_2.png - btn_hotcue1_2_over.png - - 257,0 - - [Channel1],hotcue_2_activate - true - LeftButton - false - - - [Channel1],hotcue_2_clear - true - RightButton - false - - - [Channel1],hotcue_2_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_3_over.png - btn_hotcue1_3.png - - - 1 - btn_hotcue1_3.png - btn_hotcue1_3_over.png - - 236,21 - - [Channel1],hotcue_3_activate - true - LeftButton - false - - - [Channel1],hotcue_3_clear - true - RightButton - false - - - [Channel1],hotcue_3_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue1_4_over.png - btn_hotcue1_4.png - - - 1 - btn_hotcue1_4.png - btn_hotcue1_4_over.png - - 257,21 - - [Channel1],hotcue_4_activate - true - LeftButton - false - - - [Channel1],hotcue_4_clear - true - RightButton - false - - - [Channel1],hotcue_4_enabled - false - - - - - - - - - - - - - - - 646,8 - 370,250 - - - - - 370,250 - - - - - Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected - - 3 - btn_vinylcontrol_indicator_horizontal1.png - btn_vinylcontrol_indicator_horizontal2.png - btn_vinylcontrol_indicator_horizontal3.png - -1,0 - - [Channel2],vinylcontrol_status - - - - Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected - - 3 - btn_vinylcontrol_indicator_horizontal1.png - btn_vinylcontrol_indicator_horizontal2.png - btn_vinylcontrol_indicator_horizontal3.png - -1,248 - - [Channel2],vinylcontrol_status - - - - Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected - - 3 - btn_vinylcontrol_indicator_vertical1.png - btn_vinylcontrol_indicator_vertical2.png - btn_vinylcontrol_indicator_vertical3.png - 0,0 - - [Channel2],vinylcontrol_status - - - - Vinyl Status Provides visual feedback with regards to vinyl control status Green for control enabled Blinking yellow for when the needle reaches the end of the record Red for needle skip detected - - 3 - btn_vinylcontrol_indicator_vertical1.png - btn_vinylcontrol_indicator_vertical2.png - btn_vinylcontrol_indicator_vertical3.png - 367,0 - - [Channel2],vinylcontrol_status - - - - - - 2,43 - 318,118 - horizontal - - - - - - - Waveform display Shows the loaded tracks' waveforms near the playback position. Left-click: Use the mouse to scratch, halt, spin back and push forward a track. Right-click: Drag with mouse to make temporary pitch adjustments. Drop tracks from library or external file manager here. - - 2 - 0,0 - - - #3F4249 - - #E4E4E4 - #E4E4E4 - #565E6B - #565E6B - #00FF00 - #FFFF00 - - loop_start_position - loop_end_position - loop_enabled - #00FF00 - #BCDBFB - - - loop_start_position - LOOP IN - top - #00FF00 - #FFFFFF - - - loop_end_position - LOOP OUT - top - #00FF00 - #FFFFFF - - - - hotcue_1_position - marker_hotcue1_2.png - HOTCUE 1 - bottom - #3FC7FA - #FFFFFF - - - hotcue_2_position - marker_hotcue1_2.png - HOTCUE 2 - bottom - #3FC7FA - #FFFFFF - - - hotcue_3_position - marker_hotcue1_3.png - HOTCUE 3 - bottom - #3FC7FA - #FFFFFF - - - hotcue_4_position - marker_hotcue1_4.png - HOTCUE 4 - bottom - #3FC7FA - #FFFFFF - - - hotcue_5_position - HOTCUE 5 - center - #AE5CFF - #FFFFFF - - - hotcue_6_position - HOTCUE 6 - center - #AE5CFF - #FFFFFF - - - hotcue_7_position - HOTCUE 7 - center - #AE5CFF - #FFFFFF - - - hotcue_8_position - HOTCUE 8 - center - #AE5CFF - #FFFFFF - - - hotcue_9_position - HOTCUE 9 - center - #AE5CFF - #FFFFFF - - - hotcue_10_position - HOTCUE 10 - center - #AE5CFF - #FFFFFF - - - hotcue_11_position - HOTCUE 11 - center - #AE5CFF - #FFFFFF - - - hotcue_12_position - HOTCUE 12 - center - #AE5CFF - #FFFFFF - - - hotcue_13_position - HOTCUE 13 - center - #AE5CFF - #FFFFFF - - - hotcue_14_position - HOTCUE 14 - center - #AE5CFF - #FFFFFF - - - hotcue_15_position - HOTCUE 15 - center - #AE5CFF - #FFFFFF - - - hotcue_16_position - HOTCUE 16 - center - #AE5CFF - #FFFFFF - - - hotcue_17_position - HOTCUE 17 - center - #AE5CFF - #FFFFFF - - - hotcue_18_position - HOTCUE 18 - center - #AE5CFF - #FFFFFF - - - hotcue_19_position - HOTCUE 19 - center - #AE5CFF - #FFFFFF - - - hotcue_20_position - HOTCUE 20 - center - #AE5CFF - #FFFFFF - - - hotcue_21_position - HOTCUE 21 - center - #AE5CFF - #FFFFFF - - - hotcue_22_position - HOTCUE 22 - center - #AE5CFF - #FFFFFF - - - hotcue_23_position - HOTCUE 23 - center - #AE5CFF - #FFFFFF - - - hotcue_24_position - HOTCUE 24 - center - #AE5CFF - #FFFFFF - - - hotcue_25_position - HOTCUE 25 - center - #AE5CFF - #FFFFFF - - - hotcue_26_position - HOTCUE 26 - center - #AE5CFF - #FFFFFF - - - hotcue_27_position - HOTCUE 27 - center - #AE5CFF - #FFFFFF - - - hotcue_28_position - HOTCUE 28 - center - #AE5CFF - #FFFFFF - - - hotcue_29_position - HOTCUE 29 - center - #AE5CFF - #FFFFFF - - - hotcue_30_position - HOTCUE 30 - center - #AE5CFF - #FFFFFF - - - hotcue_31_position - HOTCUE 31 - center - #AE5CFF - #FFFFFF - - - hotcue_32_position - HOTCUE 32 - center - #AE5CFF - #FFFFFF - - - hotcue_33_position - HOTCUE 33 - center - #AE5CFF - #FFFFFF - - - hotcue_34_position - HOTCUE 34 - center - #AE5CFF - #FFFFFF - - - hotcue_35_position - HOTCUE 35 - center - #AE5CFF - #FFFFFF - - - hotcue_36_position - HOTCUE 36 - center - #AE5CFF - #FFFFFF - - - cue_point - marker_cue2.png - CUE - bottom - #FF001C - #FFFFFF - - - - - - - vertical - - - - - - 88,82 - - - - Spinning vinyl Rotates during playback and shows the position of a track. Use the mouse to scratch, halt, spin back and push forward a track. Drop tracks from library or external file manager here. If Vinyl control is enabled, it can display the timecoded vinyls signal quality (see Preferences→Vinyl Control). - - 2 - 0,0 - i,i - vinyl_spinny2_background.png - vinyl_spinny2_foreground.png - vinyl_spinny2_foreground_ghost.png - - - - [Spinny2],show_spinny - visible - - - - - 0,0 - 88,35 - - - - - - - - - Vinyl Control Mode Absolute mode - track position equals needle position and speed Relative mode - track speed equals needle speed regardless of needle position Constant mode - track speed equals last known-steady speed regardless of needle input - - 3 - - 0 - btn_vinylcontrol_mode_abs2.png - btn_vinylcontrol_mode_abs2.png - - - 1 - btn_vinylcontrol_mode_rel2.png - btn_vinylcontrol_mode_rel2.png - - - 2 - btn_vinylcontrol_mode_const2.png - btn_vinylcontrol_mode_const2.png - - -1,2 - - [Channel2],vinylcontrol_mode - - - - - Vinyl Cueing Mode Determines how cue points are treated in vinyl control Relative mode Off - Cue points ignored One Cue - If needle is dropped after the cue point, track will seek to that cue point Hot Cue - Track will seek to nearest previous hot cue point - - 3 - - 0 - btn_vinylcontrol_cueing_off2.png - btn_vinylcontrol_cueing_off2.png - - - 1 - btn_vinylcontrol_cueing_one2.png - btn_vinylcontrol_cueing_one2.png - - - 2 - btn_vinylcontrol_cueing_hot2.png - btn_vinylcontrol_cueing_hot2.png - - 44,2 - - [Channel2],vinylcontrol_cueing - - - - - [Vinylcontrol],show_vinylcontrol - visible - - - - - - - - - - - Waveform overview Shows information about the track currently loaded in this channel. Jump around in the track by clicking somewhere on the waveform. Drop tracks from library or external file manager here. - - 2 - 2,161 - 250,37 - - #E4E4E4 - #00FF00 - #E4E4E4 - 80 - - [Channel2],playposition - false - - - - - - Track title Displays the title of the loaded track. Informations are extracted from the tracks tags. - - title - 2 - 0,4 - 230,18 - - - - - Track Artist Displays the artist of the loaded track. Informations are extracted from the tracks tags. - - artist - 2 - 0,24 - 210,18 - - - - - Tempo Displays the tempo of the loaded track in BPM (beats per minute) - - 2 - 245,4 - 70,18 - - right - - [Channel2],visual_bpm - - - - - - Time Displays the elapsed or remaining time of the track loaded. Click to toggle between time elapsed/remaining time. - - 2 - 215,24 - 100,18 - - right - - [Channel2],playposition - - - - - - - - Tempo and BPM tap Displays the tempo of the loaded track in BPM (beats per minute) When tapped repeatedly, adjusts the BPM to match the tapped BPM - - 1 - - 0 - btn_tap2_over.png - btn_tap2.png - - 232,2 - - [Channel2],bpm_tap - true - - - - - - Pitch rate Displays the current pitch of the track based on the original tempo. - - 2 - 316,28 - 50,14 - - center - - - - - Pitch control Changes the tempo of the track currently loaded when it is moved Right-click: Reset to default value - - knob_pitch2.png - slider_pitch2.png - 335,45 - false - - [Channel2],rate - false - - - - - - Syncronize Left-click: Syncs the tempo (BPM) and phase to that of the other track, if BPM is detected on both Right-click: Syncs the tempo (BPM) to that of the other track, if BPM is detected on both - - 1 - - 0 - btn_sync2_over.png - btn_sync2.png - - 321,7 - - [Channel2],beatsync - true - LeftButton - - - [Channel2],beatsync_tempo - true - RightButton - - - - - - Raise pitch Left-click: Sets the pitch higher Right-click: Sets the pitch higher in small steps. Change the the amount of the steps in Preferences→Interface menu. - - 1 - - 0 - btn_pitch_up2_over.png - btn_pitch_up2.png - - 342,153 - - [Channel2],rate_perm_up - true - LeftButton - - - [Channel2],rate_perm_up_small - true - RightButton - - - - Lower pitch Left-click: Sets the pitch lower Right-click: Sets the pitch lower in small steps. Change the the amount of the steps in Preferences→Interface menu. - - 1 - - 0 - btn_pitch_down2_over.png - btn_pitch_down2.png - - 321,153 - - [Channel2],rate_perm_down - true - LeftButton - - - [Channel2],rate_perm_down_small - true - RightButton - - - - - - Raise pitch temporary (nudge) Left-click: Holds the pitch higher while active Right-click: Holds the pitch higher (small amount) while active Change the amount of the steps in Preferences→Interface - - 1 - - 0 - btn_nudge_up2_over.png - btn_nudge_up2.png - - 342,174 - - [Channel2],rate_temp_up - true - LeftButton - - - [Channel2],rate_temp_up_small - true - RightButton - - - - Lower pitch temporary (nudge) Left-click: Holds the pitch lower while active Right-click: Holds the pitch lower (small amount) while active Change the amount of the steps in Preferences→Interface. - - 1 - - 0 - btn_nudge_down2_over.png - btn_nudge_down2.png - - 321,174 - - [Channel2],rate_temp_down - true - LeftButton - - - [Channel2],rate_temp_down_small - true - RightButton - - - - - - Spinning vinyl Show/hide the spinning vinyl widget - - 2 - - 0 - btn_spinny2.png - btn_spinny2.png - - - 1 - btn_spinny2_over.png - btn_spinny2_over.png - - 253,161 - - [Spinny2],show_spinny - true - LeftButton - - - - - - Adjust beatgrid Adjust beatgrid so closest beat is aligned with the current playposition. - - 1 - - 0 - btn_beatgrid2_over.png - btn_beatgrid2.png - - 253,180 - - [Channel2],beats_translate_curpos - true - LeftButton - - - - - - Key-lock Activates pitch-independent time stretch in the deck. Toggling key-lock during playback may result in a momentary audio glitch. - - 2 - - 0 - btn_keylock2.png - btn_keylock2.png - - - 1 - btn_keylock2_over.png - btn_keylock2_over.png - - 295,180 - - [Channel2],keylock - - - - - - Quantize Toggles quantization in loops and cues - - 2 - - 0 - btn_quantize2.png - btn_quantize2.png - - - 1 - btn_quantize2_over.png - btn_quantize2_over.png - - 274,180 - - [Channel2],quantize - true - LeftButton - - - - - - Repeat When active the track will repeat if you go past the end or reverse before the start. - - 2 - - 0 - btn_repeat2.png - btn_repeat2.png - - - 1 - btn_repeat2_over.png - btn_repeat2_over.png - - 274,161 - - [Channel2],repeat - - - - - - Eject Eject currently loaded track from deck. - - 1 - - 0 - btn_eject2_over.png - btn_eject2.png - - 295,161 - - [Channel2],eject - true - LeftButton - false - - - - - - 86,204 - 277,43 - - - - - Fast Forward Left-click: Fast forward through the track. Right-click: Jumps to the end of the track. - - 1 - - 0 - btn_forward2_over.png - btn_forward2.png - - 21,0 - - [Channel2],fwd - true - LeftButton - - - [Channel2],end - true - RightButton - - - - Fast Rewind Left-click: Fast rewind through the track. Right-click: Jumps to the beginning of the track. - - 1 - - 0 - btn_rewind2_over.png - btn_rewind2.png - - 0,0 - - [Channel2],back - true - LeftButton - - - [Channel2],start - true - RightButton - - - - - - Reverse Toggles reverse playback when pressed during regular playback - - 1 - - 0 - btn_reverse2_over.png - btn_reverse2.png - - 0,21 - - [Channel2],reverse - true - LeftButton - - - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop2_0125.png - btn_beatloop2_0125.png - - - 1 - btn_beatloop2_0125_over.png - btn_beatloop2_0125_over.png - - 72,0 - - [Channel2],beatloop_0.125 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop2_0250.png - btn_beatloop2_0250.png - - - 1 - btn_beatloop2_0250_over.png - btn_beatloop2_0250_over.png - - 93,0 - - [Channel2],beatloop_0.25 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop2_0500.png - btn_beatloop2_0500.png - - - 1 - btn_beatloop2_0500_over.png - btn_beatloop2_0500_over.png - - 114,0 - - [Channel2],beatloop_0.5 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop2_1.png - btn_beatloop2_1.png - - - 1 - btn_beatloop2_1_over.png - btn_beatloop2_1_over.png - - 135,0 - - [Channel2],beatloop_1 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop2_2.png - btn_beatloop2_2.png - - - 1 - btn_beatloop2_2_over.png - btn_beatloop2_2_over.png - - 72,21 - - [Channel2],beatloop_2 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop2_4.png - btn_beatloop2_4.png - - - 1 - btn_beatloop2_4_over.png - btn_beatloop2_4_over.png - - 93,21 - - [Channel2],beatloop_4 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop2_8_over.png - btn_beatloop2_8.png - - - 1 - btn_beatloop2_8.png - btn_beatloop2_8_over.png - - 114,21 - - [Channel2],beatloop_8 - true - LeftButton - - - - Beatloop Setup a loop over X beats - - 2 - - 0 - btn_beatloop2_16.png - btn_beatloop2_16.png - - - 1 - btn_beatloop2_16_over.png - btn_beatloop2_16_over.png - - 135,21 - - [Channel2],beatloop_16 - true - LeftButton - - - - - - Loop halve Halves the current loop's length by moving the end marker. Deck immediately loops if past the new endpoint. - - 1 - - 0 - btn_beatloop2_halve_over.png - btn_beatloop2_halve.png - - 50,0 - - [Channel2],loop_halve - true - LeftButton - - - - Loop double Doubles the current loop's length by moving the end marker. - - 1 - - 0 - btn_beatloop2_double_over.png - btn_beatloop2_double.png - - 157,0 - - [Channel2],loop_double - true - LeftButton - - - - - - Loop-In marker Sets the deck loop-in position to the current play position. - - 1 - - 0 - btn_loop_in2_over.png - btn_loop_in2.png - - 186,0 - - [Channel2],loop_in - true - LeftButton - - - - Loop-Out marker Sets the deck loop-out position to the current play position. - - 1 - - 0 - btn_loop_out2_over.png - btn_loop_out2.png - - 207,0 - - [Channel2],loop_out - true - LeftButton - - - - Reloop/Exit Toggles the current loop on or off. Works only if Loop-In and Loop-Out marker are set. - - 2 - true - - 0 - btn_reloop2.png - btn_reloop2.png - - - 1 - btn_reloop2_over.png - btn_reloop2_over.png - - 186,21 - - [Channel2],reloop_exit - true - LeftButton - false - - - [Channel2],loop_enabled - false - - - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue2_1_over.png - btn_hotcue2_1.png - - - 1 - btn_hotcue2_1.png - btn_hotcue2_1_over.png - - 236,0 - - [Channel2],hotcue_1_activate - true - LeftButton - false - - - [Channel2],hotcue_1_clear - true - RightButton - false - - - [Channel2],hotcue_1_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue2_2_over.png - btn_hotcue2_2.png - - - 1 - btn_hotcue2_2.png - btn_hotcue2_2_over.png - - 257,0 - - [Channel2],hotcue_2_activate - true - LeftButton - false - - - [Channel2],hotcue_2_clear - true - RightButton - false - - - [Channel2],hotcue_2_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue2_3_over.png - btn_hotcue2_3.png - - - 1 - btn_hotcue2_3.png - btn_hotcue2_3_over.png - - 236,21 - - [Channel2],hotcue_3_activate - true - LeftButton - false - - - [Channel2],hotcue_3_clear - true - RightButton - false - - - [Channel2],hotcue_3_enabled - false - - - - Hotcue Left-click: If Hotcue is set, seeks the deck to Hotcue position. If Hotcue is not set, sets Hotcue to the current play position. Right-click: If Hotcue is set, clears its hotcue status (delete). - - 2 - true - true - - 0 - btn_hotcue2_4_over.png - btn_hotcue2_4.png - - - 1 - btn_hotcue2_4.png - btn_hotcue2_4_over.png - - 257,21 - - [Channel2],hotcue_4_activate - true - LeftButton - false - - - [Channel2],hotcue_4_clear - true - RightButton - false - - - [Channel2],hotcue_4_enabled - false - - - - - - - - - - - - - - - 10,206 - 80,50 - - - - - - Peak Indicator Indicates when the signal on the channel is clipping, (too loud for the hardware and is being distorted). - - btn_clipping_microphone_over.png - btn_clipping_microphone.png - 64,11 - - [Microphone],PeakIndicator - - - - - - Microphone volume meter Outputs the current instantaneous microphone volume - - btn_volume_display_microphone_over.png - btn_volume_display_microphone.png - 24,11 - true - 5 - 500 - 50 - 2 - - [Microphone],VuMeter - - - - - - Mix orientation Set the channel's mix orientation. L = left side of crossfader, R = right side of crossfader, M = center (default) - - 3 - - 0 - btn_orientation_microphone_left_over.png - btn_orientation_microphone_left_over.png - - - 1 - btn_orientation_microphone_master.png - btn_orientation_microphone_master.png - - - 2 - btn_orientation_microphone_right_over.png - btn_orientation_microphone_right_over.png - - 4,4 - - [Microphone],orientation - true - LeftButton - - - - - - Microphone volume Adjusts the microphone volume. Right-click: Reset to default value - - 64 - knobs/knob_rotary_s%1.png - 49,24 - - [Microphone],volume - - - - - - Talkover Hold-to-talk and mix microphone input into the master output. - - 1 - - 0 - btn_microphone_talkover_over.png - btn_microphone_talkover.png - - 4,27 - - [Microphone],talkover - true - LeftButton - - - - - [Microphone],show_microphone - visible - - - - diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_crossfader.png b/res/skins/ShadeDark1024x600-Netbook/slider_crossfader.png deleted file mode 100644 index 1cfec86233249b3b77ab6c432d307523a23f5c8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 113 zcmeAS@N?(olHy`uVBq!ia0vp^`9RFa!3-qDHg8%Bq}T#{LR=-~RqUNS*QqVp0Tfa5 zba4!+xb^nZL0$$1j%J5D^_l4}mK(B(?cnz@x818U@40@}^u4U#99d0IZ+dYJsDr`N L)z4*}Q$iB}8RsFT diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_pitch1.png b/res/skins/ShadeDark1024x600-Netbook/slider_pitch1.png deleted file mode 100644 index cb1cc25e12cac81d331993c01f897833f966fb21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^d_bJR!3-q7SsN|^QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2Rb)5S5Q;?~>a8+jQRIG7cF|GzvhAe~vdc;bpxue!^))2A_mdKI;Vst09zU*Q2+n{ diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_pitch2.png b/res/skins/ShadeDark1024x600-Netbook/slider_pitch2.png deleted file mode 100644 index cb1cc25e12cac81d331993c01f897833f966fb21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^d_bJR!3-q7SsN|^QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2Rb)5S5Q;?~>a8+jQRIG7cF|GzvhAe~vdc;bpxue!^))2A_mdKI;Vst09zU*Q2+n{ diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_pitch_sampler.png b/res/skins/ShadeDark1024x600-Netbook/slider_pitch_sampler.png deleted file mode 100644 index ded167cec3b38f2430962ad67b938225f4498010..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^d_Ww^!3-q*?yFu0QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2RL)5S5Q;?~>aih@8Mli`basZK@4@5Pr>g1uhGYA?!WSYG~f1wT-Z N!PC{xWt~$(69Dg*BANgI diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_volume1.png b/res/skins/ShadeDark1024x600-Netbook/slider_volume1.png deleted file mode 100644 index cb1cc25e12cac81d331993c01f897833f966fb21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^d_bJR!3-q7SsN|^QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2Rb)5S5Q;?~>a8+jQRIG7cF|GzvhAe~vdc;bpxue!^))2A_mdKI;Vst09zU*Q2+n{ diff --git a/res/skins/ShadeDark1024x600-Netbook/slider_volume2.png b/res/skins/ShadeDark1024x600-Netbook/slider_volume2.png deleted file mode 100644 index cb1cc25e12cac81d331993c01f897833f966fb21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^d_bJR!3-q7SsN|^QfvV}A+G=b|Cf|k*`BlT98iR@ zB*-tA!Qt7BG$2Rb)5S5Q;?~>a8+jQRIG7cF|GzvhAe~vdc;bpxue!^))2A_mdKI;Vst09zU*Q2+n{ diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png deleted file mode 100644 index d39f1544686eb0e7d9646125799c7b7cec89ad22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^K0s{3!3-qDE%demDb4_&5LbIA&;S4bhs9;D<9X}= z6l5w1@(X5QD4TrN0?5<%ba4!+xb^nTMn(n!0hSH+zxmf*T=`*cN`px3q+O;}YjkE$ lOg5VNX7L(#MurbLOiN8!wJW+0odRlQ@O1TaS?83{1OOjEE8qYC diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png deleted file mode 100644 index 374f20eec6f925ca115226be5742e8d7a98c1162..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^zkoP}gBeK9h`Vb8q}T#{LR{^gJj3F$nRvStfg&?K zT^vIyZoR#;kc&Zq$HDRYfBRe^!ERTH+j|o>$j(Z!Sd{zX#f!Nw7AI}IW?g~`zSMl# zG}ZcR#XT2yRPcXZxRkh@EHbdQy{mI}BBvIr30QcWP5$+`xx3%@ea|5vDybN?4(Lb* MPgg&ebxsLQ0LgGs3;+NC diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png deleted file mode 100644 index 962f91fbccb6e784a5002aa9efa6815e7d68fae7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15109 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 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png b/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png deleted file mode 100644 index 17ac682340e02bf53e85a7101cce4dc43f299b35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^96-#;!3HGxgLCzORG_DeV@SoV)(abX8w_|D4hC#y zZcdT7mN?gAo@#bWn}{rl0G+92289?P_yFV mI+GcM(vHfQ^{WzxHrCu@fjdmuxx$(dHlQtL6RZPlZ0b0o5>FVdQ I&MBb@08?%}rvLx| diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png deleted file mode 100644 index c75ef514746d6540cf0d194e0e33f9b3dbf7cdc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OG6Q@t<7|2UN4KM)l%yh>pB zlECmOk>P(T!^Z@MA4v?ql0j@B14#Z)V|W|S@HUR&dlJJJpqey>4+$XluM`mXb0Wjv zREB3U46kBAYJgHe5r`~M9H{tI%V&@aq)LMPf*CkDIVB{lta^c9{f2{w4j(>o`ufeA z_y7J)h_yZiR2Amw;uunK%lDMKP=f-4^Tmh_-@BAL?);yS7Nb!A)kt`Ug2j(N9=pED zZ=HSed2yYLW#<0NY1$LaX9Z6AF4b%@PbDjDR*Uc)I$ztaD0e0sv0+dDH*^ diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png deleted file mode 100644 index bea88dec076ede1dc0129ccffdfc28423e76c738..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1O*aCb)T>t<7zj^a!{-~ZRpa@e* zkY6x^1A~9h3LsC$)5S5Q;?~jQhP*)Dp$%vEXWBI}{}N>nTHnLK$HJh?`9=9AP=>+N L)z4*}Q$iB}#T_8a diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png deleted file mode 100644 index 1d6afaf116119d481fb7397e4d95f64844dc50b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c1!3HEbvi3>O=$B>F!ThA>N1&XxF!ThA>N1d25{m=~vu q9a(ktvcpdohMTt=tg0HjFEhE?O^W(e}LdVL_m722WQ%mvv4FO#sppEW-c* diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_microphone_over.png b/res/skins/ShadeDark1024x600-Netbook/tab_microphone_over.png deleted file mode 100644 index ac716d02b0edbb9759e59959213578e8de929b9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^8bHj)!3-p`uC6NqQk(%kA+A9B|Ns9%KT^6}wnqaw zOeH~n!3+##lh0ZJc}AWtjv*Ddu1+`zG>OIe<9~VI?nz30%XU^w3s7EH#l)qf_FlH5 lQm3bL1@Da7T_4{b77aIJv^}qVSP-b2!PC{xWt~$(69Bm|EPnt1 diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_sampler.png b/res/skins/ShadeDark1024x600-Netbook/tab_sampler.png deleted file mode 100644 index 034630a1b92d5d962ce4ae267b6b75c202b62464..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^IzY_F!3-pm?fCeB6kC8#h%1nuzTsf#TyGU1i?Jlg zFPOpM*^M+H$J*1yF{I+w(|$)T1_hqOoBr2FiLA|tQ+O7@_V&~<_KFa@^YuG={;`)z4*}Q$iB}doC<_ diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_sampler_over.png b/res/skins/ShadeDark1024x600-Netbook/tab_sampler_over.png deleted file mode 100644 index 67dc813cdde6de4a7609a5a07d614dd399cb9bd1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^IzY_F!3-pm?fCeB6kC8#h%1l|`jHazwD&2H#aI&L z7tG-B>_!@pW9{kU7*cWTX}=>Eg96XtP5gTe~DWM4f*kdhm diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol.png b/res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol.png deleted file mode 100644 index 8d45f3cdb4baef6194163666f75164d8db0bf122..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^8bHj)!3-p`uC6NqQk(%kA+A9B|NsBfHymuXDVzo5 zFqH)P1v4;|O+IS@S3 rt$j+i$HGG54A)Lvv0EhK`v>l;ISj{BpD$kqG=ag>)z4*}Q$iB}%Nj8@ diff --git a/res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol_over.png b/res/skins/ShadeDark1024x600-Netbook/tab_vinylcontrol_over.png deleted file mode 100644 index a2104e61acb6ac17b514ec8e5375730c4b7cf38a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^8bHj)!3-p`uC6NqQk(%kA+A9B|Ns9%KT^6}wnqaw zOeH~n!3+##lh0ZJd1jt2jv*Ddu1+xIVld!1vgUt%38$91$g0&7om+VpmCi}&e9_W$ rYoC(sv9ORh!?hDv>=udm{(<{y4#V-(=gXG?ObP0l+XkKu_rL{ diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_background.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_background.png deleted file mode 100644 index 3158d8dbea3b41762b772c088bfa11ee0a09da5a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1568 zcmW;Mi9gc~9Ki7<9WT=3rKjBH7&CS-#x^_17E4C161B=AJG3DQ7F39^V{=zy}z&T_b>P+JJanH^ZiFf&A=VMsF=(%cAu!WaWgF>n(L zxTz%^ZGkYgM4+)qDIC%ahmyjh%<(7;&cqyVg29_&tV}TkQwsuGiiozfLSqSLmP9j4 zk~tPY#sF{tYYPA#U}Fie0vyHy2mo6w(FRAb#Sv}sL<*h+pyJ84crwL`M71IVjt~IW z06U_!9m&R?WNSx0Oe0h59h_b1E*>r(o~|dnPIxg+dNaHj=g%_zeEcq53b=YXFyu;5 zXh?X(waDnm=$Po(xR|)aTM0>ViOF$^x8sv;C#9q%r`}1u!%EM%pP8L=KRY)ohm)O~ zm(9t`&Ew{A3-Y*y`2|G=E=nX4#pVPN2(5bQR3~Wj@O!NvBg65VsY>{@C|O>n8i?=W8nIM`GVoM zlN;9tChPLeFBhtYkMH*%dsw#knfq>fa}dRO2JvbRIS(lwQgl*HP4hKQh+bc~6v|Ob z=jTvLeBw)ldj#)sj_ks)!0^94w(NORZowc{60f9jF}~7Cfi_gl@-UxG_ccYvaOxq- zv+VAGVli3?QTZ9_>3NTT!wc=OZC|Ij+lh8+7c@;{*Io9>933W++8Oc6vmWN}3;E>A z3@{rO3dGe#avx~CXJ5n(T?O#W;M_@U+I;PY}MLT`Z(-rpav^petY_Tz>2?h z@egdpj$xm_?w6#Q-J4h-y%1oJv|nwy@bq0l0mVs=V_y3^zq17ex-4VKN(FcB-mw?I zjObpW42_#2^2(zWWK>qatajVWBKli!ch5fPWqyw+h`v_f6sAiivffItovX)D+e_Nf z*qDNVJmJx0+-J{*rcJ_iB_sJfxC`z72k!hx$TB`kUzlFdB zl6_+^aCi=<5O-`@b9g9LaVc_ZtIuDxD`m7_Lb?c+xqM6Qlqa!-AE&MgVzrEGmZ`Z{Sb%YFVlZ!Va%IQVtyRll!# z_r_?J$_L!ExOT<^+(H+cG3m@hZi9}G6eWFkOj~M}dIaWZ4Z`?!P{O!QO4Z@BvHt=3M$s++ diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground.png deleted file mode 100644 index a4dc80c452ec1ab8b7fb6b549962dbd4ec941804..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?$y1o(uw{{R2~2`z!&)Nl)+T?!>Z ze!&d9YMMqif#H#fRg0Ew*m30a*~_=@zIgZf%hzw;fBpIUk3EKABT#LWr;B4q#jUqz zj*B)ZFt|7#nN!U6a=&-K*@p9%=QAC9P+l0j@_63dqv_jr?!G?lr`(?fy4}h@0%PV) yfr9v*_I@$TPH#26Wu&$In$*Xc5g=bPG~D2Bn$PR9t3QMn#PxLbb6Mw<&;$T*JgM*i diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground_ghost.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny1_foreground_ghost.png deleted file mode 100644 index 8f9be4de36f314d1bca93d2dffdaf56851efe0ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?$S2Ka=y{{R1f`i6tlME0lN0}Yoh z3GxeOP&2fTC@8Gx?wLG$#p<26AH00~;oJ8gKY#uG*R*(rB~V$Yr;B4q#jUqzt}`|$ zh_E;wkTF8)=(Zhx_d3ttvId&Sz_2B*e?7O-Glk%jAfBhIpUXO@geCxv^N0BW diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_background.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_background.png deleted file mode 100644 index 3158d8dbea3b41762b772c088bfa11ee0a09da5a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1568 zcmW;Mi9gc~9Ki7<9WT=3rKjBH7&CS-#x^_17E4C161B=AJG3DQ7F39^V{=zy}z&T_b>P+JJanH^ZiFf&A=VMsF=(%cAu!WaWgF>n(L zxTz%^ZGkYgM4+)qDIC%ahmyjh%<(7;&cqyVg29_&tV}TkQwsuGiiozfLSqSLmP9j4 zk~tPY#sF{tYYPA#U}Fie0vyHy2mo6w(FRAb#Sv}sL<*h+pyJ84crwL`M71IVjt~IW z06U_!9m&R?WNSx0Oe0h59h_b1E*>r(o~|dnPIxg+dNaHj=g%_zeEcq53b=YXFyu;5 zXh?X(waDnm=$Po(xR|)aTM0>ViOF$^x8sv;C#9q%r`}1u!%EM%pP8L=KRY)ohm)O~ zm(9t`&Ew{A3-Y*y`2|G=E=nX4#pVPN2(5bQR3~Wj@O!NvBg65VsY>{@C|O>n8i?=W8nIM`GVoM zlN;9tChPLeFBhtYkMH*%dsw#knfq>fa}dRO2JvbRIS(lwQgl*HP4hKQh+bc~6v|Ob z=jTvLeBw)ldj#)sj_ks)!0^94w(NORZowc{60f9jF}~7Cfi_gl@-UxG_ccYvaOxq- zv+VAGVli3?QTZ9_>3NTT!wc=OZC|Ij+lh8+7c@;{*Io9>933W++8Oc6vmWN}3;E>A z3@{rO3dGe#avx~CXJ5n(T?O#W;M_@U+I;PY}MLT`Z(-rpav^petY_Tz>2?h z@egdpj$xm_?w6#Q-J4h-y%1oJv|nwy@bq0l0mVs=V_y3^zq17ex-4VKN(FcB-mw?I zjObpW42_#2^2(zWWK>qatajVWBKli!ch5fPWqyw+h`v_f6sAiivffItovX)D+e_Nf z*qDNVJmJx0+-J{*rcJ_iB_sJfxC`z72k!hx$TB`kUzlFdB zl6_+^aCi=<5O-`@b9g9LaVc_ZtIuDxD`m7_Lb?c+xqM6Qlqa!-AE&MgVzrEGmZ`Z{Sb%YFVlZ!Va%IQVtyRll!# z_r_?J$_L!ExOT<^+(H+cG3m@hZi9}G6eWFkOj~M}dIaWZ4Z`?!P{O!QO4Z@BvHt=3M$s++ diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground.png deleted file mode 100644 index a4dc80c452ec1ab8b7fb6b549962dbd4ec941804..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 299 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?$y1o(uw{{R2~2`z!&)Nl)+T?!>Z ze!&d9YMMqif#H#fRg0Ew*m30a*~_=@zIgZf%hzw;fBpIUk3EKABT#LWr;B4q#jUqz zj*B)ZFt|7#nN!U6a=&-K*@p9%=QAC9P+l0j@_63dqv_jr?!G?lr`(?fy4}h@0%PV) yfr9v*_I@$TPH#26Wu&$In$*Xc5g=bPG~D2Bn$PR9t3QMn#PxLbb6Mw<&;$T*JgM*i diff --git a/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground_ghost.png b/res/skins/ShadeDark1024x600-Netbook/vinyl_spinny2_foreground_ghost.png deleted file mode 100644 index 8f9be4de36f314d1bca93d2dffdaf56851efe0ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^V9db`WPSKlyb?$S2Ka=y{{R1f`i6tlME0lN0}Yoh z3GxeOP&2fTC@8Gx?wLG$#p<26AH00~;oJ8gKY#uG*R*(rB~V$Yr;B4q#jUqzt}`|$ zh_E;wkTF8)=(Zhx_d3ttvId&Sz_2B*e?7O-Glk%jAfBhIpUXO@geCxv^N0BW From a1a9006a7d2130d84adee0287f2871e7d4330593 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 28 Dec 2014 00:05:46 +0100 Subject: [PATCH 005/481] all controls implemented --- src/effects/native/paneffect.cpp | 74 ++++++++++++++++++++++---------- src/effects/native/paneffect.h | 2 +- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp index f2ab327a7bc..9d498bcf6dd 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/paneffect.cpp @@ -30,7 +30,7 @@ EffectManifest PanEffect::getManifest() { depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); depth->setMinimum(0.0); depth->setMaximum(1.0); - depth->setDefault(0.5); + depth->setDefault(1.0); EffectManifestParameter* strength = manifest.addParameter(); strength->setId("strength"); @@ -42,7 +42,8 @@ EffectManifest PanEffect::getManifest() { strength->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); strength->setMinimum(0.0); strength->setMaximum(0.5); - strength->setDefault(0.25); + // strength->setDefault(0.25); + strength->setDefault(0.0); EffectManifestParameter* period = manifest.addParameter(); period->setId("period"); @@ -51,9 +52,10 @@ EffectManifest PanEffect::getManifest() { period->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); period->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); period->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - period->setDefault(250.0); period->setMinimum(1.0); period->setMaximum(500.0); + // period->setDefault(250.0); + period->setDefault(150.0); return manifest; } @@ -71,6 +73,10 @@ PanEffect::~PanEffect() { //qDebug() << debugString() << "destroyed"; } +// todo : ramping signal carré +// todo : + + void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, const CSAMPLE* pInput, CSAMPLE* pOutput, const unsigned int numSamples, @@ -78,16 +84,24 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, const EffectProcessor::EnableState enableState, const GroupFeatureState& groupFeatures) { Q_UNUSED(group); - Q_UNUSED(enableState); + // Q_UNUSED(enableState); Q_UNUSED(groupFeatures); Q_UNUSED(sampleRate); PanGroupState& gs = *pGroupState; + // if (EnableState::DISABLED == enableState) { + if (2 == enableState) { + // if (true) { + gs.time = 0; // DISABLING = 2 + return; + } + int read_position = gs.write_position; CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()); CSAMPLE stepFrac = m_pStrengthParameter->value(); + CSAMPLE depth = m_pDepthParameter->value(); gs.time++; if (gs.time > lfoPeriod) { @@ -103,52 +117,68 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // coef of the slope // a = (y2 - y1) / (x2 - x1) // 1 / ( 1 - 2 * stepfrac) - float a = 1.0 / (1.0 - stepFrac * 2.0); + float a = 1.0f / (1.0f - stepFrac * 2.0f); // merging of tests for // float inInterval = fmod( CSAMPLE(gs.time), (lfoPeriod / 2.0) ); - float inInterval = fmod( periodFraction, 0.5 ); + float inInterval = fmod( periodFraction, 0.5f ); // size of a segment of slope - float u = ( 0.5 - stepFrac ) / 2.0; + float u = ( 0.5f - stepFrac ) / 2.0f; - float quarter = floorf(periodFraction * 4.0); + float quarter = floorf(periodFraction * 4.0f); CSAMPLE position; if (inInterval > u && inInterval < ( u + stepFrac)) { // position should be stuck on 0.25 or 0.75 // Is the position in the first or second half of the period? - position = quarter < 2.0 ? 0.25 : 0.75; + position = quarter < 2.0f ? 0.25f : 0.75f; } else { // qDebug() << "calcul | a : " << a << "| step : " << stepFrac; // position should be in the slope - position = (periodFraction - (floorf((quarter+1.0)/2.0) * stepFrac )) * a; + position = (periodFraction - (floorf((quarter+1.0f)/2.0f) * stepFrac )) * a; } // set the curve between 0 and 1 CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; + // CSAMPLE frac = sin(M_PI * 2.0f * position) + 1.0f; // frac = CSAMPLE_clamp(frac); - // frac = (roundf(frac * 100.0) / 100.0); - // qDebug() << "stepFrac" << stepFrac << "| position :" << (roundf(position * 100.0) / 100.0) - // << "| time :" << gs.time << "| period :" << lfoPeriod - // << "| a :" << a - // << "| q :" << quarter - // << "| q+ :" << floorf((quarter+1.0)/2.0) - // << "| frac :" << frac; + float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; + // float lawCoef = 1.0f / sqrtf(2.0f); + + qDebug() << "stepFrac" << stepFrac + << "| position :" << (roundf(position * 100.0f) / 100.0f) + << "| time :" << gs.time + << "| period :" << lfoPeriod + // << "| a :" << a // coef of slope between 1 and -1 + // << "| q :" << quarter // current quarter in the trigo circle + // << "| q+ :" << floorf((quarter+1.0f)/2.0f) + << "| vol_coef :" << frac + << "| pos_coef :" << frac + // << "| lawCoef :" << lawCoef + << "| enableState :" << enableState + << "| numSamples :" << numSamples; // qDebug() << "frac" << frac << "| 1 :" << frac1 << "| 2: " << frac2 << "| 3 :" << frac3; + SampleUtil::mixStereoToMono(pOutput, pInput, numSamples); + // Pingpong the output. If the pingpong value is zero, all of the // math below should result in a simple copy of delay buf to pOutput. for (unsigned int i = 0; i + 1 < numSamples; i += 2) { - pOutput[i] = (pInput[i] + pInput[i + 1]) * frac; - pOutput[i + 1] = (pInput[i] + pInput[i + 1]) * (1 - frac); + pOutput[i] = pOutput[i] * (1 - depth) + + pOutput[i] * frac * lawCoef * depth; + pOutput[i+1] = pOutput[i+1] * (1 - depth) + + pOutput[i+1] * (1.0f - frac) * lawCoef * depth; } + + // qDebug() << "pOutput[1]" << pOutput[1] << "| pOutput[2]" << pOutput[2]; } -float fmod(float a, float b) { - return (a/b) - ( (int)(a/b) * b); -} +// float fmod(float a, float b) { + // return (a/b) - ( (int)(a/b) * b); +// } + diff --git a/src/effects/native/paneffect.h b/src/effects/native/paneffect.h index cf532618227..bb675add747 100644 --- a/src/effects/native/paneffect.h +++ b/src/effects/native/paneffect.h @@ -50,7 +50,7 @@ class PanEffect : public GroupEffectProcessor { private: // int getDelaySamples(double delay_time, const unsigned int sampleRate) const; - + QString debugString() const { return getId(); } From efbfc76f6ca7d957cd36b8af817e46f69dcef34c Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 28 Dec 2014 01:40:22 +0100 Subject: [PATCH 006/481] beginning ramping --- src/effects/native/paneffect.cpp | 71 ++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp index 9d498bcf6dd..58ed7b00699 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/paneffect.cpp @@ -55,7 +55,7 @@ EffectManifest PanEffect::getManifest() { period->setMinimum(1.0); period->setMaximum(500.0); // period->setDefault(250.0); - period->setDefault(150.0); + period->setDefault(50.0); return manifest; } @@ -91,10 +91,8 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, PanGroupState& gs = *pGroupState; // if (EnableState::DISABLED == enableState) { - if (2 == enableState) { - // if (true) { - gs.time = 0; // DISABLING = 2 - return; + if (0x00 == enableState) { + return; // DISABLED = 0x00 } int read_position = gs.write_position; @@ -103,49 +101,45 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, CSAMPLE stepFrac = m_pStrengthParameter->value(); CSAMPLE depth = m_pDepthParameter->value(); - gs.time++; - if (gs.time > lfoPeriod) { - gs.time = 0; + if (gs.time > lfoPeriod || 0x03 == enableState) { // ENABLING = 0x03 + // gs.time = 0; + gs.time = -1; + } else { + // gs.time++; } CSAMPLE periodFraction = CSAMPLE(gs.time) / lfoPeriod; - // size of a step (while stuck on 0.25 or 0.75) - // todo rename as stepFrac - // float stepSize = stepFrac * lfoPeriod / 2.0; - // coef of the slope // a = (y2 - y1) / (x2 - x1) // 1 / ( 1 - 2 * stepfrac) float a = 1.0f / (1.0f - stepFrac * 2.0f); // merging of tests for - // float inInterval = fmod( CSAMPLE(gs.time), (lfoPeriod / 2.0) ); - float inInterval = fmod( periodFraction, 0.5f ); - // size of a segment of slope float u = ( 0.5f - stepFrac ) / 2.0f; + // current quarter in the trigonometric circle float quarter = floorf(periodFraction * 4.0f); + // float inInterval = fmod( periodFraction, (lfoPeriod / 2.0) ); + float inInterval = fmod( periodFraction, 0.5f ); + + float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; + CSAMPLE position; + if (inInterval > u && inInterval < ( u + stepFrac)) { - // position should be stuck on 0.25 or 0.75 - // Is the position in the first or second half of the period? + // at full left or full right position = quarter < 2.0f ? 0.25f : 0.75f; } else { - // qDebug() << "calcul | a : " << a << "| step : " << stepFrac; - // position should be in the slope + // in the slope (linear function) position = (periodFraction - (floorf((quarter+1.0f)/2.0f) * stepFrac )) * a; } // set the curve between 0 and 1 CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; - // CSAMPLE frac = sin(M_PI * 2.0f * position) + 1.0f; - // frac = CSAMPLE_clamp(frac); - float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; - // float lawCoef = 1.0f / sqrtf(2.0f); - + /* qDebug() << "stepFrac" << stepFrac << "| position :" << (roundf(position * 100.0f) / 100.0f) << "| time :" << gs.time @@ -158,15 +152,34 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // << "| lawCoef :" << lawCoef << "| enableState :" << enableState << "| numSamples :" << numSamples; + */ - // qDebug() << "frac" << frac << "| 1 :" << frac1 << "| 2: " << frac2 << "| 3 :" << frac3; - + // todo (jclaveau) : stereo SampleUtil::mixStereoToMono(pOutput, pInput, numSamples); // Pingpong the output. If the pingpong value is zero, all of the // math below should result in a simple copy of delay buf to pOutput. + // todo (jclaveau) : ramping + for (unsigned int i = 0; i + 1 < numSamples; i += 2) { + gs.time++; + /* + if (inInterval > u && inInterval < ( u + stepFrac)) { + // at full left or full right + position = quarter < 2.0f ? 0.25f : 0.75f; + } else { + // in the slope (linear function) + position = (periodFraction - (floorf((quarter+1.0f)/2.0f) * stepFrac )) * a; + } + + // set the curve between 0 and 1 + CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; + */ + + position = (periodFraction - (floorf((quarter+1.0f)/2.0f) * stepFrac )) * a; + CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; + pOutput[i] = pOutput[i] * (1 - depth) + pOutput[i] * frac * lawCoef * depth; pOutput[i+1] = pOutput[i+1] * (1 - depth) @@ -174,11 +187,7 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, } + // qDebug() << "pOutput[1]" << pOutput[1] << "| pOutput[2]" << pOutput[2]; } - -// float fmod(float a, float b) { - // return (a/b) - ( (int)(a/b) * b); -// } - From d28390e58941e5a719bf997261270ba8b3f12843 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 28 Dec 2014 02:28:21 +0100 Subject: [PATCH 007/481] position changed each 2 samples --- src/effects/native/paneffect.cpp | 55 +++++++++++--------------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp index 58ed7b00699..bc6618eb015 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/paneffect.cpp @@ -84,7 +84,6 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, const EffectProcessor::EnableState enableState, const GroupFeatureState& groupFeatures) { Q_UNUSED(group); - // Q_UNUSED(enableState); Q_UNUSED(groupFeatures); Q_UNUSED(sampleRate); @@ -95,50 +94,26 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, return; // DISABLED = 0x00 } - int read_position = gs.write_position; - - CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()); + CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()) * numSamples / 2; CSAMPLE stepFrac = m_pStrengthParameter->value(); CSAMPLE depth = m_pDepthParameter->value(); if (gs.time > lfoPeriod || 0x03 == enableState) { // ENABLING = 0x03 - // gs.time = 0; gs.time = -1; - } else { - // gs.time++; } - CSAMPLE periodFraction = CSAMPLE(gs.time) / lfoPeriod; - // coef of the slope // a = (y2 - y1) / (x2 - x1) // 1 / ( 1 - 2 * stepfrac) float a = 1.0f / (1.0f - stepFrac * 2.0f); + // define the increasing of gain when only one output channel is used + float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; + // merging of tests for // size of a segment of slope float u = ( 0.5f - stepFrac ) / 2.0f; - // current quarter in the trigonometric circle - float quarter = floorf(periodFraction * 4.0f); - - // float inInterval = fmod( periodFraction, (lfoPeriod / 2.0) ); - float inInterval = fmod( periodFraction, 0.5f ); - - float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; - - CSAMPLE position; - - if (inInterval > u && inInterval < ( u + stepFrac)) { - // at full left or full right - position = quarter < 2.0f ? 0.25f : 0.75f; - } else { - // in the slope (linear function) - position = (periodFraction - (floorf((quarter+1.0f)/2.0f) * stepFrac )) * a; - } - - // set the curve between 0 and 1 - CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; /* qDebug() << "stepFrac" << stepFrac << "| position :" << (roundf(position * 100.0f) / 100.0f) @@ -159,26 +134,32 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // Pingpong the output. If the pingpong value is zero, all of the // math below should result in a simple copy of delay buf to pOutput. - // todo (jclaveau) : ramping - + // todo (jclaveau) : ramping if period changes + // todo (jclaveau) : ramping if curve is square for (unsigned int i = 0; i + 1 < numSamples; i += 2) { gs.time++; - /* + CSAMPLE periodFraction = CSAMPLE(gs.time) / lfoPeriod; + + // current quarter in the trigonometric circle + float quarter = floorf(periodFraction * 4.0f); + + CSAMPLE stepFracCoef = floorf((quarter+1.0f)/2.0f) * stepFrac; + + // float inInterval = fmod( periodFraction, (lfoPeriod / 2.0) ); + float inInterval = fmod( periodFraction, 0.5f ); + + CSAMPLE position; if (inInterval > u && inInterval < ( u + stepFrac)) { // at full left or full right position = quarter < 2.0f ? 0.25f : 0.75f; } else { // in the slope (linear function) - position = (periodFraction - (floorf((quarter+1.0f)/2.0f) * stepFrac )) * a; + position = (periodFraction - stepFracCoef) * a; } // set the curve between 0 and 1 CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; - */ - - position = (periodFraction - (floorf((quarter+1.0f)/2.0f) * stepFrac )) * a; - CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; pOutput[i] = pOutput[i] * (1 - depth) + pOutput[i] * frac * lawCoef * depth; From 92aee711c1705fe50174d3328f34dfb6030975f5 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Wed, 31 Dec 2014 00:12:12 +0100 Subject: [PATCH 008/481] ramping + cleaning --- m_framebuffer.png | Bin 0 -> 13251 bytes .../knobs/knob_rotary_s0.png | Bin 0 -> 393 bytes .../knobs/knob_rotary_s1.png | Bin 0 -> 418 bytes .../knobs/knob_rotary_s10.png | Bin 0 -> 457 bytes .../knobs/knob_rotary_s11.png | Bin 0 -> 448 bytes .../knobs/knob_rotary_s12.png | Bin 0 -> 455 bytes .../knobs/knob_rotary_s13.png | Bin 0 -> 445 bytes .../knobs/knob_rotary_s14.png | Bin 0 -> 453 bytes .../knobs/knob_rotary_s15.png | Bin 0 -> 435 bytes .../knobs/knob_rotary_s16.png | Bin 0 -> 420 bytes .../knobs/knob_rotary_s17.png | Bin 0 -> 423 bytes .../knobs/knob_rotary_s18.png | Bin 0 -> 416 bytes .../knobs/knob_rotary_s19.png | Bin 0 -> 410 bytes .../knobs/knob_rotary_s2.png | Bin 0 -> 454 bytes .../knobs/knob_rotary_s20.png | Bin 0 -> 410 bytes .../knobs/knob_rotary_s21.png | Bin 0 -> 396 bytes .../knobs/knob_rotary_s22.png | Bin 0 -> 403 bytes .../knobs/knob_rotary_s23.png | Bin 0 -> 390 bytes .../knobs/knob_rotary_s24.png | Bin 0 -> 399 bytes .../knobs/knob_rotary_s25.png | Bin 0 -> 390 bytes .../knobs/knob_rotary_s26.png | Bin 0 -> 395 bytes .../knobs/knob_rotary_s27.png | Bin 0 -> 382 bytes .../knobs/knob_rotary_s28.png | Bin 0 -> 377 bytes .../knobs/knob_rotary_s29.png | Bin 0 -> 367 bytes .../knobs/knob_rotary_s3.png | Bin 0 -> 457 bytes .../knobs/knob_rotary_s30.png | Bin 0 -> 362 bytes .../knobs/knob_rotary_s31.png | Bin 0 -> 328 bytes .../knobs/knob_rotary_s32.png | Bin 0 -> 344 bytes .../knobs/knob_rotary_s33.png | Bin 0 -> 369 bytes .../knobs/knob_rotary_s34.png | Bin 0 -> 378 bytes .../knobs/knob_rotary_s35.png | Bin 0 -> 384 bytes .../knobs/knob_rotary_s36.png | Bin 0 -> 401 bytes .../knobs/knob_rotary_s37.png | Bin 0 -> 409 bytes .../knobs/knob_rotary_s38.png | Bin 0 -> 403 bytes .../knobs/knob_rotary_s39.png | Bin 0 -> 409 bytes .../knobs/knob_rotary_s4.png | Bin 0 -> 451 bytes .../knobs/knob_rotary_s40.png | Bin 0 -> 433 bytes .../knobs/knob_rotary_s41.png | Bin 0 -> 425 bytes .../knobs/knob_rotary_s42.png | Bin 0 -> 432 bytes .../knobs/knob_rotary_s43.png | Bin 0 -> 428 bytes .../knobs/knob_rotary_s44.png | Bin 0 -> 442 bytes .../knobs/knob_rotary_s45.png | Bin 0 -> 422 bytes .../knobs/knob_rotary_s46.png | Bin 0 -> 416 bytes .../knobs/knob_rotary_s47.png | Bin 0 -> 428 bytes .../knobs/knob_rotary_s48.png | Bin 0 -> 434 bytes .../knobs/knob_rotary_s49.png | Bin 0 -> 435 bytes .../knobs/knob_rotary_s5.png | Bin 0 -> 463 bytes .../knobs/knob_rotary_s50.png | Bin 0 -> 435 bytes .../knobs/knob_rotary_s51.png | Bin 0 -> 439 bytes .../knobs/knob_rotary_s52.png | Bin 0 -> 446 bytes .../knobs/knob_rotary_s53.png | Bin 0 -> 476 bytes .../knobs/knob_rotary_s54.png | Bin 0 -> 469 bytes .../knobs/knob_rotary_s55.png | Bin 0 -> 463 bytes .../knobs/knob_rotary_s56.png | Bin 0 -> 478 bytes .../knobs/knob_rotary_s57.png | Bin 0 -> 481 bytes .../knobs/knob_rotary_s58.png | Bin 0 -> 471 bytes .../knobs/knob_rotary_s59.png | Bin 0 -> 468 bytes .../knobs/knob_rotary_s6.png | Bin 0 -> 460 bytes .../knobs/knob_rotary_s60.png | Bin 0 -> 473 bytes .../knobs/knob_rotary_s61.png | Bin 0 -> 458 bytes .../knobs/knob_rotary_s62.png | Bin 0 -> 467 bytes .../knobs/knob_rotary_s63.png | Bin 0 -> 467 bytes .../knobs/knob_rotary_s7.png | Bin 0 -> 464 bytes .../knobs/knob_rotary_s8.png | Bin 0 -> 468 bytes .../knobs/knob_rotary_s9.png | Bin 0 -> 473 bytes .../style/style_bg_microphone.png | Bin 0 -> 139 bytes .../style/style_bg_sampler.png | Bin 0 -> 228 bytes .../style/style_bg_waveform.png | Bin 0 -> 15109 bytes .../style/style_bg_woverview.png | Bin 0 -> 183 bytes .../style/style_branch_closed.png | Bin 0 -> 138 bytes .../style/style_branch_open.png | Bin 0 -> 158 bytes .../style/style_checkbox_checked.png | Bin 0 -> 298 bytes .../style/style_checkbox_unchecked.png | Bin 0 -> 117 bytes .../style/style_handle_checked.png | Bin 0 -> 92 bytes .../style/style_handle_unchecked.png | Bin 0 -> 93 bytes script/console/__init__.js | 8 ++ src/effects/native/paneffect.cpp | 130 ++++++++++++------ src/effects/native/paneffect.h | 17 +-- 78 files changed, 101 insertions(+), 54 deletions(-) create mode 100644 m_framebuffer.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s10.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s11.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s12.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s16.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s19.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s2.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s21.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s22.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s28.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s35.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s37.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s38.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s39.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s40.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s47.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s48.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s50.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s53.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s58.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s6.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s8.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s9.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_woverview.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_open.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png create mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_unchecked.png diff --git a/m_framebuffer.png b/m_framebuffer.png new file mode 100644 index 0000000000000000000000000000000000000000..5c219b9cccf3722e31c47eb79c34ebfba6634e58 GIT binary patch literal 13251 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 oB}-$!E3$MS0Q6d}I1_(Oa zWwRG!KhPrSk|4ie1_1?K1BZZ+go5&h2~!rVJ#g^Cr3VjQefaeIFPl_<3Q*YrPZ!6K zid(f4ZwfUl@VGW-+}%*RG*svR|CwqFFN;W4$ItnA#+HR?(x%s4O3f-u@9zD6Zs*M- zA<^dxWfr%XNH^EMO;1byH+ApJ|4SGbXDvG49};}rC?R8o_#Ey@jCDbZ*^%zD9Zt4E zQ8w9_J@-NEpAfya*V)lOjPWwqH8AAH5Sy!jzlsLGunlSLDbUr1k+ z(ewJ@|G1gG>m}~q6wiA3St|0B(vqo59hz6?usq%26<7Qs&@*dcF+*jVaHf@B@duz^ O7(8A5T-G@yGywnz$(h0c literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png new file mode 100644 index 0000000000000000000000000000000000000000..f06258ae5beb286177a80d9d788350391a630c41 GIT binary patch literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(r03aiAnCd{7$WX=I0VS>z8ndXIv4J7A`Hmzy%Pf@%{pEDAIAFKi-B;1-X#Wq zN&<@OW?F@OO%DB@0<>lNy2-&nOXW&}{DK(-6m$(70zwjU3d$QMOj)q|z`+w2?mT$< z{MFkJzyDfXcLcfhl&6bhNX4zviLb?)6a-qtFYbws+oYrV-4oeuCiW|ByrWgC;>&Fu-H?;Sko0FdV@cr8DOJAItcuJSm qYwy0K_N17n-=>%wSO=MYjWuK6riO!g6>{GH>i{Z`2{mD2q0yCvCg4pIM)*sS{~lI!AIzkZq^` zvo#mg-zfLiiRHhUyt+~7&Gl!xZ5j!WuKivv#o0OWR%N~X;>pF{rGMI!Sj@H?o)S3G zSLLy#=V0H-Cm&+=UYM+HTzKET`q{;{!nXO>w|(o0|FwF{UXgn|1p!GQayuO2vJ=F1+~^M^JV+*CJV)iN5_w4z8CT91GZiZ>S;Tum$9%$(E zXI<|esl9rQ(#3_7tIBUbxV>b-+7_m@4>H3TxMXYE7VMtD{Z~V9+RHN^E;xKhJm>an zwxZFc^CvZb$7C)E`Nm!O+OW&SaJtPb520kM%KtMKO{(;MZaL*b+_%|_xEfz%96OT0 z^LEBR`FV3>jJ1_FN-T2v+|)bykBU1o;84`Ku4V|BAcR7Xnq^_jGX#skoIp>E)zD20YEaUe1lW zu|a*CWKwyb{FN8(jQr5Q?3GuqE&tCb f?;{`m$%b+B83~)5iJ87YuQGVL`njxgN@xNAC4;tF literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fb3fa1df5cb35363e78da6d707504baf142afc GIT binary patch literal 445 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFA-p zPMZUO49f|wK+^kiB#?C88sM@s5XjI@F!wwg4rI9Q4F-}x91tE%{xxM(aZ_0|-Xk$k(` z`rzlVc_+2_**4Wa{I|v7ZP5j_&I~qxTV@5G50=-$mEC^^r{(k~@%ERn$Uj@Nw*R$K z`roFx+s=Ddoyy%mWBa?sEuF1TPdZGSaq8K}9Xx?6cO6;sRO6J&*1v5loD{5f*%eAO z%YOd&Q1yo4J|kiN*ph^}Et%Kq?+LsWjsLaU)?T%4S$@c0{>rPzxCLqr{^nf#%=R`{ WeC3rJyDtMh$>8bg=d#Wzp$P!Xq_&0t literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png new file mode 100644 index 0000000000000000000000000000000000000000..fee0a6942c02e0dfcb4afbcc0de57d1f215ffb96 GIT binary patch literal 453 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDgXcfmy}llBFByX zPMZUO49f|wK+^kiB#?C88sM@sP%qmG$o4!N4kTUo1_MbTayuLXVR)VciF;oGsrSAX z<^M3&?_La$?Rz&S=v`v)rz9ZTZi%7Xr6kBNn1Mk+L0i|r z+QB0rFeE%7r=YxH!juIE4qmwQ;K}n>AAbM-`)u0DrZ92V@5NU&R@+LP_|*9Qtw{OwW;tbNLz{L9hs1{Sf?LElE!0mvbZy6y4*~8U z7M`DbrE~WQ&%FxE&um^guW07^PnYbvObQG4&dpHp^vO6D!y98JOL_Bu1^v}uSeDq$ eurrH(B787P!t_G&RY{;P89ZJ6T-G@yGywpoK(_q= literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png new file mode 100644 index 0000000000000000000000000000000000000000..a9f30d663eca38ecd1cbebda0ffc176294911d24 GIT binary patch literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBB#v( zK+yf=Ro4#S3rvWAIAFKivco% z-X#WqN&+&RwgmV;jtlvk9Qr*4Xi;*_jd?(OO?z%BLRU(g~`_Q2cQrUA^>>s!`DSlhMz1Ui8{| z#yutey<)8L>P|zu8y-T*UNc_nu9_))>fb`1#R>{v9D&L@YiNT+efNYH* z_TdhhDi32|Cm zzT3wCiR#+EuiN+)88ZGjxKG&D$n^AXi&=QnzaZ0;QnN*055GEo<=HxuTdM0#K5t=Z zxmVNodGe&@niT=tQs-2rsC|x1SzUX{vWWLVsm=Yvfl5;ylqWH4Pwf#>dv-^_Tevuq z@v8Z%hN&X|0}>m*ny)gQARFs5N48n^^!;}6_07L4Q`O!*&p5y5V4G51oc)dLeP5z) szYJD+-H_uXd7*IUg7ZF;b3fQHuFw~rf6D!$4A657p00i_>zopr033Izr2qf` literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png new file mode 100644 index 0000000000000000000000000000000000000000..7280d9a0b883d3ba32398313f081f96398d9daae GIT binary patch literal 423 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1k5?Pcv{te1g0&F%#8Bh`M*17cGJSLe;+nn zFE&$i5jov|rq-y9Ua6+X$`kSwF*`j=;}Zr!>KL5(+tN;K2 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png new file mode 100644 index 0000000000000000000000000000000000000000..07ea2fd98c27f7e87c1d80370cdb2d9a0de87896 GIT binary patch literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 z&RYXqb_N0&cFTM`&xXUr-42I9*q-M=g5Fmmfu#S#SigHQKt{l`_@H-*Kr;AKQt-zl zporZP?~t#_q2E)0wydy!9RRdct|Z7Wm_a~6*T5klFeD){r=VfNlm!P4Ubyt&`Ku2f ze*gLVZ|@t~lR%XxJY5_^DsGidd@I$Yz|+cjaFK|q(c5gv@AVUNC(Ai~{v-LJich+0 zN$h2b#Yen;Cvqt=IJ_^4;%Q{nQ5Uwfk+8{+J%8r4zU97;HCEzVrioq*JZkcZheyKd z)7hEgD(WAVyVophJ#=lIlJHNF>(}?5n0Vy3OqkBph!ux^8uXvt`Do!o{-VMuf~pSN z-DDj!--vAzd2#(jvci3h=`Kamx6gilvGW^6JBf-+%vw z2Z`kY6&~|+aSW-rRXXt{Q`;jNdpBnRLBuX=m2(w1lEH`Saea14mMo|%=n zxMs$*RL3Ltj+;n|33lz_OsN+x*uQP>hI4yQ<}LgAa>j=nQ+(WidnKR$z-c(I_SU=( znFrpi8@t%Wd9 z{tsjQ?!^Gv0ng$Cp2P*cOZ0yd7yKzH_+t`KP|->U$O!qG9Qr*4X!~3?g&jc0sFnoz z1v3aJ=o(l%1O$d8B<2*9H%yqaV9V|U2hU!(^x(GBm`k*y=PUNdEDYqY0PxfqbNqF&ATB~?*mkYtABPsnFl zt?4CrV)K(39P13@=c$_;PxgJ3-0^J=d+Y(e$M>bbx>sIbsJhUW>+QU)MLIOA}2JSh{gT;r`kuQ7PK`njxgN@xNAsM)%> literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png new file mode 100644 index 0000000000000000000000000000000000000000..571a07dcc19ef12fa48991fbd09bea058ee1416c GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(q}?(f&$HohLAS#pK(^O~2q5Ws4kYeuIms>LYjWuK6re5h)MO3*P+O z2TN7;OJ-ev;{Qlpl)-s2Peq)I(5FoXRx_1eGFbP24(N8xIuIy-I`h!7XY7{>ri!s% zSQp4GvgnqRsAyUC6~LhTfK1e8lgnMY}*pMnQB` zPoki1k4zo@S`Z4O@{A^xJ~tb z+Yb56`7dtv@rlh`v7T#nt4t&B)w-813(rkw-(SRHxN^k;foRQ(3Vzupie^svioR`W zy*!eaIs=3jWaq^Hdq4SR;hBpk>I#!~$-Eb^SM{@6j(#EY7|wb-(Ok ZbhssS+NE^U8=!9(JYD@<);T3K0RRxKnj-)J literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png new file mode 100644 index 0000000000000000000000000000000000000000..f50be60339603fb705844bb266d502373cda6845 GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILm*s_6JZdB-@OtW1C6yP z>zo9%MY1HwFPMQrKtb2QAs{57pkcz41qV)Cc<}1Ohu?qyP2kiQ0xCM>>EaktajSOX zZJ}lb9v5aYQCD3L{oAMh|4&m_h}v2fKcnD=FN4sO(DK+wv0}xR6D#DF3otRH8~J+l zDRKq6T~hkm@t{&ZCM7I2=tnrm<*!12d4uiSy}0+}^8{tym~Ir!Fl*lu#w~_@(wE)d zXkU8uM9g!#pY?);bEOU}-IK)qDeWXXUy*~SsDaEyhL9!aCp>egcA3V!bMYPFlgV@V z|NlQV=SF_u{P$UPw(BG9Sl@cx`Woy}>9E#=u}*Vo>g6o&dHntS0%uBn5~YE@VeoYI Kb6Mw<&;$VcSC4f7 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png new file mode 100644 index 0000000000000000000000000000000000000000..bdaa5a414364a2d8b1c670c67710f64465d40fb2 GIT binary patch literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx803t!Rj#_mxQ3eZiVhW@V-k>Ix5PW-YjWuK z6rjlmvmO+IY%U4%3ua&tP|!7S2nb0iXqYf%!PWy8E?v3(;K_&I|5l{g2?JFf_jGX# zskl`;@vT&o0#7S@S6hJR(kn*q{u@ar+b&Cc&VT#f+YhW>jV}sp%zZ57*3B@pJP6jh(!IE zw6rOC_Z~x?DbH7J;;ZWX60j@tWx~?FTix+nm4b41Bx-KUTHyL-`wI7*gYOh)^Bxvs zHF_fZbykMmv3C#u$IX=W_b&N$=j#8eIQ#mp*`2fPS$$kgjEy=b2B{VssU+JhzSrU} V*u0Iw;yBPt44$rjF6*2UngElko+khR literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png new file mode 100644 index 0000000000000000000000000000000000000000..393a509891b5db1e753f9ce029442e9583bad86f GIT binary patch literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#!gXBd2W0r(i2;&+_dsOOyTss+NkE38m5$vK?~t#_q2E)0#?A__ ziU-;vQ4-`A%)lU^VBp{p5Ry>PFk#Aq1E(+CdGPAP?|&1k@;3n09QJf^45_$PJMkt{ zlY)S2yRP|}jy1O*{r{hKZiV2ix#ALh5Jw0NK2xiW_#bP0l+XkK1~!@> literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png new file mode 100644 index 0000000000000000000000000000000000000000..6a7e0e737b97f5bd8a28e84d055a2ee3e95017f9 GIT binary patch literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx7~$g$RhC_mxN>!~0qkkOZm>dY1@fC|c>b9SRBgnjHE)1!!#3 zUyD0HTO>+?{DK)61Qc`)90EcT3K}L%S+MlLg-Z{feE9nN`h{D6foe{8x;TbZ+^U`U znyE>Fr78VRMp2Z4!A3dx^#ad+TX24!CaGM!^jn!7!*AfVn67A01${{6K&R?cg_=lpi2X>OUun@qg2LO*TPynSTxy;rXUqU>iK ROa}Ui!PC{xWt~$(697?7ndkrj literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png new file mode 100644 index 0000000000000000000000000000000000000000..a0c644070588c549167b0da9a0a8533b5248cc35 GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#B)BUGBItQK9Kr~Cmk4AiTIskQ3JLj|3^dK;i-|VSaIumgzhDLi z0R;mGkARSbf`$oG7VJ52;m(5(zZs8RJ_%HD%+tj&q~ccX#J5sS3Oubp&j^@sMxXo} zpK-gR{lc3+jUV61v~y@K)!5}XdF!n0lR_r$%ePEt@MzRfR?|A5Eu6%9bi%|=hVyf_ zAD=YCH8{yIXX%35tzM==v4&GtM>u;)o(T8qk5=FJ+c*R!Dz{*4-UF zNy`Oqd(V64XuIXkh3>^`Bp9YCM=`KHJU3zSfeIHv=AYeJ{vy4Fc7N_`7eBhQc;`LS zKfm@awC&12vfW)Ujn#lxvO(1Q=f{aFZ=O(oDjmL$-|isChFd@%F?hQAxvX0H|LeGegC_U@y&D2 zFsncCcSd1iriRiBqw|kfY?KYS+rigg8avaHb)kcTmcf1&M@L4cnGsy)zt+YJMVK5~ zAuw0`vD|fUya6FH&ue}-dYmzQ&|Ggv5bB`>4Ju9R?WYzN1k22Jc&&is1 zZ*i1N;R5kRZnI`gSB{#YzA62LSAxx=2&RwjTh+O8=a~Kfe#&Fd*B5OzH|lKiGn_y6 t{xB0Unt9VCWya=%QyvNjZ|+Q&u=CO2Yc9~^bO(Bf!PC{xWt~$(6990XkDCAh literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png new file mode 100644 index 0000000000000000000000000000000000000000..5b3f01b10d6a92616515403b8c8e20656f58c42b GIT binary patch literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDUSf35ZBNjDgXcfmy}n5Aj=7^ za5kLbb~psihA@KOB?1|COT2w=MTdM%1{%WiHtZqLOyQCszhDLi0R;nVhk%5Fh6z&^ z9Juh{!|#gbm)n5Shdo^!Ln?07PP{GDtia=PcqzxCrlnW^|G%u}E!)+1SKL5-HBNhZCg0uY2d9ko&HOw irij)gWtn}{_{!KT%xh&GG7ab(1_n=8KbLh*2~7ZYA&s^G literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png new file mode 100644 index 0000000000000000000000000000000000000000..72782ab6a738a7b190593ec722609d79c696e74d GIT binary patch literal 457 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspXHth)a z^3LL93p*ax96ovKEYW)&i{$>7Te6FEcV2VWQ1?%Do}n;b^S#Hp4~qpN7galdYM6P# zed3Ln^UpgTRx|ClIL|BARcO~>`;685$*UtG&l{&2Tx{jE7c z1-BiX-V!Li>u1gGN~xWt{an^LB{Ts5)H1yl literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png new file mode 100644 index 0000000000000000000000000000000000000000..c48b7e5e7dc4f8b198cbe10b6af25d107706aac2 GIT binary patch literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDc1m>5ZBNjDgXcfmy}n*h1?E@ zph>yx3=Ddg2owx_oe=sx1!#iLvzPA^4Aof*aSJutzV>KI zzv`?|(YU^2-=F*Hs~;OJO0CYetdGmR)i$YKCEt0XV!6_P=Xon$Uu&D0xIE_T^B)YX XRXn!~^#2qCJ;UJX>gTe~DWM4f*;a>W literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png new file mode 100644 index 0000000000000000000000000000000000000000..4d50a6d306da9c27d3f3b145dd357bec922f273d GIT binary patch literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDdPa25ZBNjDgXcfmy}l_9(~J< zQ3h%kC<*cl22u(J4gm=T4HFg|xbWb^-p?K;K)EfRE{-7;w`wQe7HU!8ao)}PL`mWQ z|E;&pPHR}@#Z8!}*08j{D@3nDX6Bk7%Q8nh6Yir|_A8gKRH<;gTe~DWM4f*{yOt literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png new file mode 100644 index 0000000000000000000000000000000000000000..95f9935432605e11cbe875e8adb6b63bfbbf771a GIT binary patch literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDW?FR5ZBNjDU$LkK&0wp2w|X+ zZihqAM0{^W2fa%K3I=~n0&2Hn+?5P8O}He;FPK3v!*k zgA0Jtn><|{Ln?07PQ1<6tRUc0u6e?h_14P&|5u;gTVG@M*`}c_P$g;_N zyc6eL-1wnm$BTDNELN%vuC>p4N-mykGh9`)h2eKkS%PAH>mH8JJ)1aq8SI4F64vq8 z|395{$8C%0{AmdK II;Vst02hgV;s5{u literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png new file mode 100644 index 0000000000000000000000000000000000000000..8df606d5406dd440388088dd23566d4dd5207a7d GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDZc=p5ZBNjDgXcfmy}mAuC)h} zKxEPD0%0JNZihpVxj=T%yF?)Av?aj*aa_pPAA8D0bsL~h7(8A5T-G@yGywp!2ah)Z literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a4fe1cb26877adc174763146d154416c4a68f3 GIT binary patch literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bd-e) zK(@=yK)48y;dVF#NJ6CG;z92cgFht!xt5dMd~ZdEd`$+LmY3eG0yJE#B*-tAfk8mQ zz`->jB%z>T!juI|4_tWg;rGAu{f!HNDh_$NIEGZ*s-1XS=#T=B%i*(L2cx>&p8mHF zaWZP&68yb&!sOTt#-$D^b9Ucf7qePRAn8$C$I>b3CQgzsJ5-$VkF1as+O)DUx3=cg zYO#||CsJOW&;4c;Tre$Q?uK@cibuICmbT0Pyv3p0XJgP&({|mr-y`29cDiKQtk)LL zXUa}owzx4+=4pacOn-*!oCEU}{Tp9)2(e!3tnxRBef;OsfBTt2(*<>P`*V-qm;SxD u#m}i;WP{|zvJ^q%+@tDh8%_IvF?~J7&vD_<#=k({FnGH9xvXV@SoV+KCsXniP0i*;yo; zq};SC|9|iC{mF6m9Pftu=N2q24Zf#lB<}baHi0Xs&h5yQOP_Y+h|KWlUo+*ao4%yc zrN)Ccw$rD?bvTMIQMy|FZtp!!3$28cZ!{HEZPvI?-!8TFVdQ&MBb@ E0K%b_PXGV_ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png new file mode 100644 index 0000000000000000000000000000000000000000..b04d702333736cb3437d686ca4746b27b9d545a1 GIT binary patch literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}mgFxLi> zPMZUOBm_Bc4RF~R2I9A;hS2&Dx6p$8+@0O|UBaci`Rj=M7B9Bm#B{ted_$hnG9;QIgw{ zpi|H5f-ID!14AO-XRtB}&u4k^@u%XKLr)wPcXa-qdqyswUrUW$>9ibQpsa;W=FQ97 zTIIK1dGR#wO=Ihg=Ot&$I&-dNMp;FyTwHjzID5(21I*r;Aq{MWcNPh5Y%Js7Dl=o7 zM&N=u8P|_|GX19i|NCUs!iYtVpRR2CyLY3VSG;GuJnNs97uugXgr}x`+qm-9k>q_J XIR)QdJ12hz=qUzIS3j3^P6P0|$?Qz>tK3h6z&^95{dB(t}qYe*a_2D%cHFb%sX*l%fx3V zFSQ-!vI{9(c=6^DX5-HDw{Jd{NzC8=b4B99S?i6l(GWrJE0G?j!gW(Efn4uvQ9v^2U1IRZBp}0SOF+oi zWT45PdITkb7D<-``2{mD2q>uN8aM=mBos7En6hB&feV*z-+l1p!|#7(71+)w}dZ@#-=xg_l~`;C6du<1P=3gIHtKXXPmziQu6 zmhw#Ev5h7x(?(rq!C8C_4s{F?zmE&Qc{g>UfcTcUXd&74#WjTmERkQgFf^E3^1Zxq zV%sA9(4dS9v#T-`b5AWT-Qt~cyd-+stguy2t4q_qvpjO$ttOWH#_a$1$rEqr2DaPWsQYuzjDP9LB`2K~fu!5v5D44jL>PqOcP|FW2zVAB^e!>@Q&RB9Bp}!KRwEp_X^Q;|k3ZwzbpPCrT_GJDmr`$*s@sUI zsO!wLHEtKUm%PM5VO6Sz!(t{DBfbOt+u3jYoiU-scjG+`Az8im0`n%TdARy>GgQdqF$) g@;;k$zK9%UUo9Y%Ait>KE6`&Mp00i_>zopr0Dt$LM*si- literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png new file mode 100644 index 0000000000000000000000000000000000000000..8cc83bcf3f0e90228dd17a9fbbb109f0e3026ef0 GIT binary patch literal 451 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsp$aOmu67n@U^m_`>-s!z7*n!SaDGBlmW?&Fd z&^54j2nb0?%qeJ?FlE8g0|!r>y>RK)gC|d)zxwdu_uqdaSW-rRXg#v zP?G_Vi?imU3eB#rMNJ7u{{0u9wN6$EC@Kw_9!r&HAZe?`qxR{-+E47aP3^!zL4E#WSK7QxU>ATTAIJT$|JKG zXH!?SpWb<@d(PtDUMDNv)@?f9$+jkSzyJN56;3A0Hu3jPI5vat+O_1VhUZ^eEENjB z(G#lID&frEdq`sP;s3Fcz0ddETI8?scJkzh&rNq+4zGFHclP`8fDl9Xcu~XfyNgc= ezDhd5l7Cu2GW7FDh7CaPF?hQAxvX~Up-Y?h0P$<&`)ryEY%d%nzD zSLmwV-QTH(90!>=S0;pZOj;4WsBeWx7O$qnEL9_eBjTxEDz_r?|NWo%@W!zfyy;h8 zz7IX(ny7Ks$AD9gtIbB>sqv}KwmbJ_uPn^aGZy-&|36K1V$Qzv-+`WE@O1TaS?83{ F1OVpOth@jK literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png new file mode 100644 index 0000000000000000000000000000000000000000..61674b87aa15b34a956ebfa0d1567a3891a5b618 GIT binary patch literal 425 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4ac-5U&HxE&5rw9Yq~E<5AQ|*7G5Awb@W&(| zTO-IA$O!qG9Qr*4XiH?rt?NKbWlMtmf*BYD6m$(70s=!43d$QMOqsFZz`+w2?ml?+ z;rHKv^SPhT2P(Ye>EaktajSOHL$M|Wo>orQVByP3Tcc*a|3Bev-($m%H~uQ_$luMp z+{%H=Yt7o%G24WuvOij{ zr_EP3-m&l)r@#&4U9H!gTe~DWM4fA=IY0 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png new file mode 100644 index 0000000000000000000000000000000000000000..1fbb754f79f21f9e53bcedfbb1fd1c1af49e9851 GIT binary patch literal 432 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsr&$+5ZBNjDgXcfmy}nrnBWQ| zoi+ymNeFV;83<(9E%Wg_9R?S4I~)RJdtHbClAhrLY=Mmf!&Hla<|?# z-f=s{c;03f`!AE=JPFUhmAVbD#6u4p+N3$faKk@WQ|6zp=4yS%XW0F@udQ8Z?GpX; zqS?Q_k}1D*ZaThx_v_2geb&oOwq`njxg HN@xNAi6*QB literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png new file mode 100644 index 0000000000000000000000000000000000000000..ebe7a5011267837056fa5e2a354e8a5d88504a9a GIT binary patch literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBHIOC zj_drKHU~hsK+@P^ZpeiZ#PwgOc8WX`Cf|Sqo;_O1@pDR&e#N=Qj(^bZr-i@H$%sq-`sx4tHL~;195HfH{hTqo zHlJzMKHrmSN^@6i&0m(uXspP*CFWOJSLUY5^V0WziRPa?w_6tIHwI5vKbLh*2~7Z> C1E`b$ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png new file mode 100644 index 0000000000000000000000000000000000000000..f91a7a1452e06f180d3cd41dbb9852d3813b274f GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFByX zPMZUO46{yWh=|M1Kp?|znUCk$a3Dj$TpJ?pb~psW@H__+_r4MdB>f-8`rV5GG6J5( z2fa%S{*)B_F$u_3w9;`q6cX|^IrMu9(7KPeez^c`S11Yc3ua&tP|(&juy6u|dURD0di#WAGfR_VmsLQMudF3Ex{flInpJW%cW z^Iw0K?+nMTcix$PWw(W*99Ud4cimrqvgHwjQhnO9eeZW4(+~)I9(i^3tQ!JTu5g{Z z`gY-)eTao@l@ zd*{8F_FY6IL;dYE@zxVvno>@RT1$I&&RX1Mx%iL#DZz;IfxQlMUN}7}=)FH(zhL`( z|FTQ(+RbH_y;xAWp>@A`x-qY_rhAj;m2+(~x12fIA@nS4yP3}nx7k%MnECFB**#TH RZv=Xg!PC{xWt~$(697~Tv`GK} literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png new file mode 100644 index 0000000000000000000000000000000000000000..8e798dee8b1186839a99ca4e929fa9b32f33c4d3 GIT binary patch literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk8u}5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1kaQz>~`rV5GvV-0w27gKd zGVGRkhkQ*A{hk7}La4G*0BECZNswPK1A~Bqu7N{9U`RqvL3zW3DGLr9Ja*#3gXgb4 z{QhgU#(Nb|;U!NO$B>F!r4wI^H5u?UziwFA;TJG-LEyK4`kH6&1O|Lw9=(6^o1n;* zE>k0`EpnbLT{-jDIc5$;n_s?8QwkR_U45RS;_Ck3_Uq<1+q^ZTx5P`wK2Klz*i31$ zocn%q!dHtzzCYb8!vNK2P>EdAB4#rtMoAbZ5TjA=(cNZIShGynTOx%WA{;l`PZ)%IT1-M^j5H&df} v;UyI-;r2)#hmUTo1_jdXlD;S1`fV5^N<`eAsInXb`i{ZV)z4*}Q$iB}nS-Zt literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png new file mode 100644 index 0000000000000000000000000000000000000000..36daa7aaabe5fe6b9258a9cee059be4fddb0384b GIT binary patch literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspJ5k5ZBNjDgXcfmy}llBB#v( zK+>$!8Nzkh83BP5UO$q`of~G#7wN2KVJ^nj?(_KO1i_iEsd_VJsNh?WH!))2Q z^QXF3d+&YKdBW11cV5*3?Isx$6>TqFhx>MF8=j{+e$@EtblUD^%f8wbV*acQ79IKr z_oYl)Egw7cu7JAS)`>qK1xu{^6Fes8+!9WxwZ$x4-x@Y0<+y6BJIZ=vi@h z@idL#pY~I{7u;1o-6-MQ?vlLi@I&r1WhJS1A2&|8krUW#bEE0`S)v1>2}Ev(Lm&*#b0Bf=D@?3he85hCxm=W4*i}2wCLMrj#WT=?yJD|#oo-U3d6}L(!UKDFm5O4|CF*X%8HCe0s`2YWnu@0VF zDjy!OH9o`SB4}|mGwQnfjHl;Q`#YjuXPyZ6@G!aLc34nM^;E;YTAw!C_1-s5JYULi zX=n1UojXjvc`)T!Z#bV8=EC*kV(*Q+M>evDCCPuEQqmu>__2#==F&*yd5Fa9u!A@J{aj8qd4m+{O0RD){AGTQM_z z>ch`s9%}zJr>g%EJ{3E$r=!!I^U;au=gE_|N1hkw&=qY`|M$lQ=s5;YS3j3^P6AURzSAr*>E7~x;Gd|0+HL{5D3Hb97x>z3P`>8wJ86Gv3~br zfb5`miT;mbgFht!*=~nIyv|32d`%Aho&vPaOqFpr&~}BAAirP+0R>$HYX^^jz>x5S zoPzR(2~!pvIC$aGgD1~lefa(7ZzH_ju%?AhGcA!TC2BG{ZhmQ)9VK}{eCEY z?el*phRnN0wQpmks<$vK|8i)b=4O>e58U08(xs2faU8LGxALWo;G&WRv%V~?^2~mCTk(V)3l64>JM1#^CAd K=d#Wzp$P!dSgWT1 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png new file mode 100644 index 0000000000000000000000000000000000000000..80991f557d771f79021d8326243ebd2cbca1977f GIT binary patch literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#U`shwc%c{Uu#aNQdWB!S57a0rCqc`n@JL>Q3meFdc6X`{dY!&tw2 zF+g_EyTss6Ng-d8fo$VC2e(5ZKz8W&6rgqGrrEqe+Z9TJ{DK(-6m$)&9XtX;5>j&t z${Qw3S#aRsi3^t=Jb(V`!|%WUUY(f30aSaz)5S5Q;#TRzhfGZhJS~nVFBt`%)>*6Z z`EUG2=>V1+|5nu8%Qc^|TIKYss-ug%Qr1tI^}JH5h`*q-xH-W2RmTgH7q?$8xtaUl zRQlzfCtvp23D&+{Bb}+zu(E_a)=73^iry&CKFl;f^TdNfDO(gF_+(@V>*PCRk4=NYg3buoV~W{0UjzcF~a L`njxgN@xNA=}WPn literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png new file mode 100644 index 0000000000000000000000000000000000000000..a99fefcc2a34e2a834cd0e3e9eaeb21d736c9c69 GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llBB#v( zK+yf=fXWsg!$fyaa`x;b0Z2U z=zRsG(f?tr-@OfAt7IrL%*j0t*)rn_6NE|r6kBN zn1Mk+LD#_A!6P6rBq67uykWwW1qTkEIC1*yg-Z{fzxwdu_uqd$Wea};)!*@SaSW-r zRXQXJI7!pSsU;^-DF9j)xO zhR@bSdnfHV#N~FO&m(Yl(2>XcY`ahY4Qn;s7e3SdK;-7VyHqOXXu5Pvt~($PRj!82l*-$S|&R2>F^E`aK0`omS-9nLyhWOM?7@83Yt`4XhnJ0zwiJ za|((ZCQO;TVCjK_7cO0U@Z|Zc55NEV^ocD7Dn9S&;uunKD|g~sDJKVk*7S>EUSUFl zQn!BZKbqEdOYHf1z4yiEYIH1Eqwa4~UL!I?N^tGg{ie6#v{ z$m-vGjeYO6m%aZy=a}4K2a#=WW*Y@78UA_c_eQhu4Ws{?$9B9|1MRq0#(ArsoqWJa z^sM<)R_`s7U3@Dy2&twOIOtqkvHcs@N3|-R!>!DxO|=BKZdo|Zr7-{YS@kQoznxe$ zjny;h-Qqvit0UK$KU=%Y-Bb4AbdBdif$b8OB8p{yEPvm4k~8@^^A{bl|9VqmL6O1W M>FVdQ&MBb@0GyYs`~Uy| literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png new file mode 100644 index 0000000000000000000000000000000000000000..4fded0ba158a603a1dd2aaf41ac08b43ac9d53b3 GIT binary patch literal 439 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsnP(S5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#W(Y%3tc^K3Yfbln>aB!S57aEQy!ARxnWqd$=JJQwb9A`Hmzy%Pf@ zy{|?2KaBOe2Vw`kOAP*$6!JA0$TqHX05U?qrvPpAY@MzRaz{y!UoeA!g06wJgGWF} zctTD=dBcP$a~CW-aPY*1I}e^bfA!(_-+$2x3ugjVpZ9ce45_$PI`OSolY)SY<4Ly`Kl8~&mWaaUHe4=}WlKj^z6@6bO9x>xx zXW_3Vw(CJna@-adZ9g9af0jc>oN^de36x%wbN$6XSK~sgTe~DWM4f2Z^u~ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png new file mode 100644 index 0000000000000000000000000000000000000000..d979b1b4f9723b2b709532b99d88bcc761013dc1 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDU$LkK;*PJKsVD0 z$grH?3K4PM8sM@s5XiP$<^vG{l5U4XTy_Ql8IBwMeQ!kr8J_3Dfu#49NDz4~%Ku@k z-@OxDNu3ZffKR&(-p zda;#Ww4Z7F?f!mj=S9<-Q-x0ir$wcRtSso; z64>!MswQcEh{dW6_os;O$T-_+_>9y4hQ6?L!y)&**W4%maaQNjsV$K&J(le;-J{`p zR^?BT^5}Is*>(zkXL6_1@6!5SA?z@D=ago}65*#dO0O7{nw1**?9%0D_T+xr!{`$t W!TbE@Q&RB9q>!)4KoPI=5fFCh_Y|NT&Teg8 z2XvBpNswPK1A~BqlCFWZgGWGMNJ36QdBcP$a~CW-aPY*1OAnqrdH(9dhu?qy{R=X> z768<8*VDx@q~ccT#G7JG20Sj#f=5^{DlOX47#;KJ|NZnJFVPE?FEjJgXPG>3RJQNB zRFY&q@44xT>((wRi_$H(ZgKNkEGQ`}V$s^lxcg6y=^LZ+{*a=1Gj=b2+kG=TOjuR! zty9zFA4~$eGmd;%YPxxGr!fDP5TnUfru!G)(x?!cx~IsVvt-M|f3Ixzcel;4XzuvD zK>eM5t6|bT_w+@oNB&>%V(PZr<+)(W!d)o^^%tI$f8>1hul#h^`t09v#R$ zYuI&?H}zq|OqYdkCTJh|(Cl=?YumMSpL1c=_m&Du`|Y~FFc;`i22WQ%mvv4FO#tny B$jATy literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png new file mode 100644 index 0000000000000000000000000000000000000000..38d0ee1af9d11ae541c4170c8f62b44b71e18633 GIT binary patch literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsg?kr5ZBNjDgXcfmy}llBB#v( zK+vtzPF-*Y|nEb zHQrYuLFBb4|A(=D_hNwTfM@YR?-GMQB?W&>3i+B06tP?4?f*Cq$PWFU0(8LZ9Hs3* z_o$Tw`2{mD2q@?pSUY%l281N!6jU@!m@;j_+AUiT9K3Mp!IS5&K79E7_uoI0{S$5h zb=>lFaSW-rRXXveSd#&d%VnlVO;rY_`xU@|oQka zCpEcgJ}OQ6Z#!fy&Q&f@eqLNFwQBM16U(dItup7ae%Z3z@0QK_ez!RiL2^7UpH&T9 z&$O-BR>phLeoJBFPk~s?3AZoQ8a*?sGuyxC?(F-@*}rO4yS;X6)Sk`y^}07@^(T#u wF{izs1Szc$Xy)e<(Eb(jNO4n*aa+ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png new file mode 100644 index 0000000000000000000000000000000000000000..8d745ea5ecc806c4c04388b0435b531e61ae3318 GIT binary patch literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llB2^zl zr_BLChUElTAnAQM5=c644RF~R2xQnT^YJ_z4rD-(+u;xh!}DCY$B8iCJ24)o!XP64 z4`coA#Q@m>&*FpLB?f;=3jUZB@--PKVqE9o|2PiF4*i}2v|4?0uqx0cDkVXF!3+!n z3TnCr)(##4fguTrIRy<9rYtyc@WhFe7alx+_2I+szyJQ-oANIXsQ!+pi(^Q|t#+nM^yH%R6Xp2_#s3~Lxy1Lk_EBZBiTe@V)(&^!h5LJ^T@wo~Da`Y~XdhB^ zAgk*gheqlP!^nc<{oVEpe%#l6{p+L2+XbH#Pkl5|-qw3u`eVrNr7E@$eM54)7*_LC mi9}Q%3f*dF_SNj;3iHNx$=jxx6-R*nWbkzLb6Mw<&;$T0lEBme literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png new file mode 100644 index 0000000000000000000000000000000000000000..7e0b2eda3b9c6dbc62f64778e632420629efdd26 GIT binary patch literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mg^)dvK zj_drKHU|J1mJ?ior1#}WqbhqK!+C3f%g#U`1CHDdhXC2Wx1xch=ecm78&N=p_mxQh z$8o-QVt{O)o6+9aqWtcG*a6Sty)H%sy-N)Klob3i2`FM*=MeZhA>?av==T($8)R~q z9|Sr{y(Gvln1Mk+L_t&6z}msXGaxV|p`c;Hlm%N496WpB(v>?89z1#V;lt10fB*IT z%S#7px$o)X7*cVobmDWNCI_CD;tmC~FB=+pxsMgS|6h})tQ>U5{>;a-c~X+eFO==O zrk$Lkz#QmVvpYTQ`5oi=5B|E&OPv}l`PjT?4e|ef1>^r z7b%;t#zV$6jE=>MhJQ13-z?aB(Ao50yN7n^9xJXTaeDDH)7^MeFP&6BpTDS4&25sq z?#UX?-kIUKC8c@ZAMLj|CfsskWxCjQUB!9J#k+s}`YziRy;qv9DZX{ZV%GceRROnS z@_W{Xxz7w|neK5~XyF{AH327TCN0;jH3&8PI>X%2S^Dk0IDx4^uQGVL`njxgN@xNA DL0P}$ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png new file mode 100644 index 0000000000000000000000000000000000000000..9943eeb5dd15a0e0278ed3a1c5fdea45a04b0d92 GIT binary patch literal 481 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}llBKwuT zj_drKHU|LNmJ?ior1#}WAnCj{z-4D3kl}eY97wwE4F-}x}uceyG=pu8}IkV*2R;j zozd!Xk+PY{&}do1=cqVY^l!%bH(I+7I?EnxKhg0#BKFCLJ#+ToQC~1G=w0~po3$p7 zkC;}@ac3^vKSL}^-8n>}c+pk+5{Cpc7k`!{-qrrkG|cw>5j@==HZLqFQfPbZmTKw8 z!Z~~IcQ1{9{w?s(v>aSz&SAQgA<$lok%Gl&4V`H#n$q%4!89ZJ6 KT-G@yGywnhJfLui@9p78gAzzb2zo!5laB{0qEYLk_ zB|(0{3=9GaY8tu*)(!z7aS1sE4HKp;SbE^#*$bDhJb3c_#miS8KK%asPpRa=b)b$r zo-U3d6}L(!zT|5%5NMgcC?v$i=A+xesxN=FZI}>fc|9gboFyt=akR{09E_P>Hq)$ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png new file mode 100644 index 0000000000000000000000000000000000000000..b6dcbdb4327f3e255b5b9b0ba482e8b02e019e39 GIT binary patch literal 468 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsm1`G5ZBNjDgXcfmy}llBKwuT zPMZTDY?B5@Aj4&6AdvJt9j0Ke4dU(!g2=iZ4uLQ{&xL!O2=l!Y<8vblDB^u3637NJ z{2#{p-2*WKp2P*cOAP*$6#Ov>$W^q`@qZi#WQTlB4*i}2wEbpx(q5orR7!&Uf*BYD z6f|@V9DD;p64EmYiW??OS#aRsi4$ipT)Ok%`Ku3~e*gW)Rh<+FRDaLY#WAGfR_Vl> z)0!LvoV&vgQ;4OiN>@=+Qgr-@S@8yKp%(GhXCCe|{T9Lb^noYmkH|yfMSeXxPkpVE z>wB8rtD5+2*Ty{jrBM^%TYcLuTk=KD@4cVYoVO$-$7I^qn9!NTG4RF~R2xP#K+u;x(+xJ#9kn}tk?t3Q&$nd@r2_$`PMtfh2 z^1BBT33wLoempejU1IR3q~MQ9KoN}~W1y~(ugRg`Q-JpFZ;-MDIzzc6$S;_IK|n!U z*TCArGaxV|p`c;Hlm%N496WL1(t`(2UVZrREaktajSIV>uF5} zJS~@%I=G_Nzo?45Tk-O@{ldu;o0(q!QG9A`>>E^g?xgq8nwifzz0)Vn5^1mQ2@EZ7 z+~WAsF35}3U&GB!ylMaEEcdWfr;ZKm|L$*J=}?#TWd?_-!THVxgBj;H|6`0i-R`wv zrl{~|5ASE2PcXmpynW*Fz6SkgH@)10&$NG=VE$%NOK0&JWm`qV>ApSB?V?uprT*oe zDpv7wF;`=b@>`+vX8X*~Pptp@Us-(nYQ0+-2lrGZrmdKI;Vst04D>&TmS$7 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png new file mode 100644 index 0000000000000000000000000000000000000000..9d74037d9a09ba96305d458a83fe4a5d59e4b8b9 GIT binary patch literal 473 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mAuC)h} zjvM`*HU|J1mJ?ioBoMjm476S74P` zuS9~#Yf=6WWBu;M0NDZ0;sc(<1-(lQ{*)B_F$pN5Xr-f@X$545d`%Aho&t2k_h~I_ zflg8@3GxeOU=Wa2(AG7ub_fWGOGqy$ZkRA-!R7-8Pn^AQ>A{oduReVE^!x9>1?RqL z0Cn8bNJ5jIaXm@8a&pZ;;r{$KyB$UGP^Vv6r-RBchZb*Kgc<`qElv8%yT#^++ zOoy2E37+3LY3HBJ)o+eiR4iH8ZS>_z(aubz|BFq(uKw(D>5}8~)t~m5Nf{env3cpC z?va}Gc3F4cxwk3z+P`s}+4Z8&;fg`-qs4CZKj%&K?mKfNR5*4fd-U(h>t1~}g_?!F zkDl+2;+nQ*HBXkvjqsf!3cnIo<=zyqKQ8{DUi#<$!sbgrZ!&ng`njxgN@xNAN*c+I literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png new file mode 100644 index 0000000000000000000000000000000000000000..4b8abe16349c9c41a2ef0dcf5521760daad1c1cb GIT binary patch literal 458 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspEQ;90Fu}oCpIcI2R6N_}qvBvVHHw z07?IcvDULZ{O*C+0Z-zB-X#WqN&>QVGp$0tCWn4c0oohn^|l%443&}~zhDLi0R;_h zT?2=Jkc9M{g7St56Q?X#d*I-Svls3>c>dzmhflx%x*yVB15|&@)5S5Q;#TRzmqJYr zJk75canEbeh|`eMe)HRYVWxzu!)Nwo_f;n6r7jT)6>yC-6Yg?mI&=Au<@3Gky)N+^ ztz2vqdW&tPx{Z*?&%H|3M_wi02z{=tJmIePT5(>ZSDvB)%R=tY_|JJFX5SWOYhyp# zOA~)>Q9mWRtUIT7){mt5Q@@lPs`HJz;BKtuc4Bhl21m8v=bsdJRqvnr)4t@wfyV;2 z0w<)F`Rioq)K9;^_|N;PzDK9d-0^ae^z}u#ANH8+?Vq$ICHAq#Hp{e*;5J>Z`TM$D h1#1nqdg%$X>QCG&<@>TsUkB(*22WQ%mvv4FO#pHHxKIE9 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png new file mode 100644 index 0000000000000000000000000000000000000000..b9896bf982f8de94c18cf4b67b7137f3362dd544 GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png new file mode 100644 index 0000000000000000000000000000000000000000..b9896bf982f8de94c18cf4b67b7137f3362dd544 GIT binary patch literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png new file mode 100644 index 0000000000000000000000000000000000000000..d0777a130427a4498121e2ce215c748b927ea096 GIT binary patch literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspZhelFbOL>NTe`&yL$ z!&tw2F+gs>v-qHQiNT+ef@8= z$K|q82Y1I&)&P->S^xiY&uys^33%Q*r8;fak|k3X`FYMeUU*=t(B6O``#HteZk;j^ zRdKoQm*VQiEn>2tBV%@~>YAPoR-@dH{{vf&{OftCsc^ByHl2})_xF}7?h|ZlHCUsI zH?pi;ss61nrMLQ!-K8DpA9Po8NogFeanXL_+paO;h|K2;9v(fJuVaketh2?p{BI57 z+tU|#vXS%F4BpOXX8%n6+kdaU6z@Jw(yVsNM4SB^OLoLvzUjR`bA9P|jjq=(QgXkz m3C(tE+9MfdkWBYH{(<>pkq`^g8YIR z7z7kFbPcQ>JUjzJ64DDACQMndW$W$(2QOTD@Z|Zc4RFn7=T_Kedu zi?7;G$#{_4!Nqw*VzQl~@5a}U=lzqByXyUUvys)4*eS~EHx^#nA9wu*_nj~9r@sZB r2s^XQb=uBN6{$b2T)A~cm?b~pu#}lm@)LESM;Sa_{an^LB{Ts5qrgFht&e@qJbnhX?iI}{T5IswQI{hk7}U9(d65705H zB|(0{3=9Ga8oCD74jus^3F$cncfZMfB*d}75dN))bPmD z#WAGfR_Vmt;wA%umi`3IwHi`cxq*&rHqQG0pLzH0_zBbXjGw5TfAdUlBa2aHWN78Q zRX!U3w%)&!+@sjCSY1@8(05AHler5nJX88``E7qvsT#|ss)YZA6~@23FPWKUI-dX8 zx^%(v^DFHdYi>^F+OW2YIWtG$mqh2YcX=H$=RQXmp5NXZl4Q5p^i$k%jnJgl${Zz? zXWs;q&0T*B+e7Sh#_6yHR_Gz1>9OEzknF#bPgQu&X%Q~loCID}e B%s2o5 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png new file mode 100644 index 0000000000000000000000000000000000000000..d39f1544686eb0e7d9646125799c7b7cec89ad22 GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^K0s{3!3-qDE%demDb4_&5LbIA&;S4bhs9;D<9X}= z6l5w1@(X5QD4TrN0?5<%ba4!+xb^nTMn(n!0hSH+zxmf*T=`*cN`px3q+O;}YjkE$ lOg5VNX7L(#MurbLOiN8!wJW+0odRlQ@O1TaS?83{1OOjEE8qYC literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png new file mode 100644 index 0000000000000000000000000000000000000000..374f20eec6f925ca115226be5742e8d7a98c1162 GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^zkoP}gBeK9h`Vb8q}T#{LR{^gJj3F$nRvStfg&?K zT^vIyZoR#;kc&Zq$HDRYfBRe^!ERTH+j|o>$j(Z!Sd{zX#f!Nw7AI}IW?g~`zSMl# zG}ZcR#XT2yRPcXZxRkh@EHbdQy{mI}BBvIr30QcWP5$+`xx3%@ea|5vDybN?4(Lb* MPgg&ebxsLQ0LgGs3;+NC literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png new file mode 100644 index 0000000000000000000000000000000000000000..962f91fbccb6e784a5002aa9efa6815e7d68fae7 GIT binary patch literal 15109 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 literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png b/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..17ac682340e02bf53e85a7101cce4dc43f299b35 GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^96-#;!3HGxgLCzORG_DeV@SoV)(abX8w_|D4hC#y zZcdT7mN?gAo@#bWn}{rl0G+92289?P_yFV mI+GcM(vHfQ^{WzxHrCu@fjdmuxx$(dHlQtL6RZPlZ0b0o5>FVdQ I&MBb@08?%}rvLx| literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..c75ef514746d6540cf0d194e0e33f9b3dbf7cdc5 GIT binary patch literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OG6Q@t<7|2UN4KM)l%yh>pB zlECmOk>P(T!^Z@MA4v?ql0j@B14#Z)V|W|S@HUR&dlJJJpqey>4+$XluM`mXb0Wjv zREB3U46kBAYJgHe5r`~M9H{tI%V&@aq)LMPf*CkDIVB{lta^c9{f2{w4j(>o`ufeA z_y7J)h_yZiR2Amw;uunK%lDMKP=f-4^Tmh_-@BAL?);yS7Nb!A)kt`Ug2j(N9=pED zZ=HSed2yYLW#<0NY1$LaX9Z6AF4b%@PbDjDR*Uc)I$ztaD0e0sv0+dDH*^ literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png new file mode 100644 index 0000000000000000000000000000000000000000..bea88dec076ede1dc0129ccffdfc28423e76c738 GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1O*aCb)T>t<7zj^a!{-~ZRpa@e* zkY6x^1A~9h3LsC$)5S5Q;?~jQhP*)Dp$%vEXWBI}{}N>nTHnLK$HJh?`9=9AP=>+N L)z4*}Q$iB}#T_8a literal 0 HcmV?d00001 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png new file mode 100644 index 0000000000000000000000000000000000000000..1d6afaf116119d481fb7397e4d95f64844dc50b3 GIT binary patch literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c1!3HEbvi3>O=$B>F!ThA>N1&XxF!ThA>N1d25{m=~vu q9a(ktvcpdohMTt=tg0HjFEhE?O^WsetId("depth"); - depth->setName(QObject::tr("Depth")); - depth->setDescription("Controls the intensity of the effect."); - depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); - depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); - depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - depth->setMinimum(0.0); - depth->setMaximum(1.0); - depth->setDefault(1.0); - EffectManifestParameter* strength = manifest.addParameter(); strength->setId("strength"); strength->setName(QObject::tr("Strength")); @@ -43,7 +32,7 @@ EffectManifest PanEffect::getManifest() { strength->setMinimum(0.0); strength->setMaximum(0.5); // strength->setDefault(0.25); - strength->setDefault(0.0); + strength->setDefault(0.5); EffectManifestParameter* period = manifest.addParameter(); period->setId("period"); @@ -57,6 +46,31 @@ EffectManifest PanEffect::getManifest() { // period->setDefault(250.0); period->setDefault(50.0); + EffectManifestParameter* ramping = manifest.addParameter(); + ramping->setId("ramping_treshold"); + ramping->setName(QObject::tr("Ramping")); + ramping->setDescription(""); + ramping->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + ramping->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + ramping->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + ramping->setMinimum(0.000000000001f); + ramping->setMaximum(0.015978f); + // ramping->setDefault(250.0); + ramping->setDefault(0.003f); + // 0.00387852 + // 0.015978 + + EffectManifestParameter* depth = manifest.addParameter(); + depth->setId("depth"); + depth->setName(QObject::tr("Depth")); + depth->setDescription("Controls the intensity of the effect."); + depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + depth->setMinimum(0.0); + depth->setMaximum(1.0); + depth->setDefault(1.0); + return manifest; } @@ -64,19 +78,16 @@ PanEffect::PanEffect(EngineEffect* pEffect, const EffectManifest& manifest) : m_pDepthParameter(pEffect->getParameterById("depth")), m_pStrengthParameter(pEffect->getParameterById("strength")), - m_pPeriodParameter(pEffect->getParameterById("period")) + m_pPeriodParameter(pEffect->getParameterById("period")), + m_pRampingParameter(pEffect->getParameterById("ramping_treshold")) { + oldFrac = -1.0f; Q_UNUSED(manifest); } PanEffect::~PanEffect() { - //qDebug() << debugString() << "destroyed"; } -// todo : ramping signal carré -// todo : - - void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, const CSAMPLE* pInput, CSAMPLE* pOutput, const unsigned int numSamples, @@ -94,41 +105,31 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, return; // DISABLED = 0x00 } - CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()) * numSamples / 2; + CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()) * (float)numSamples / 2.0f; CSAMPLE stepFrac = m_pStrengthParameter->value(); CSAMPLE depth = m_pDepthParameter->value(); + float rampingTreshold = m_pRampingParameter->value(); if (gs.time > lfoPeriod || 0x03 == enableState) { // ENABLING = 0x03 - gs.time = -1; + gs.time = 0; } // coef of the slope // a = (y2 - y1) / (x2 - x1) // 1 / ( 1 - 2 * stepfrac) - float a = 1.0f / (1.0f - stepFrac * 2.0f); + float a; + if( stepFrac != 0.5f ){ + a = 1.0f / (1.0f - stepFrac * 2.0f); + } else { + a = 0.0001f; + } // define the increasing of gain when only one output channel is used float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; - // merging of tests for // size of a segment of slope float u = ( 0.5f - stepFrac ) / 2.0f; - /* - qDebug() << "stepFrac" << stepFrac - << "| position :" << (roundf(position * 100.0f) / 100.0f) - << "| time :" << gs.time - << "| period :" << lfoPeriod - // << "| a :" << a // coef of slope between 1 and -1 - // << "| q :" << quarter // current quarter in the trigo circle - // << "| q+ :" << floorf((quarter+1.0f)/2.0f) - << "| vol_coef :" << frac - << "| pos_coef :" << frac - // << "| lawCoef :" << lawCoef - << "| enableState :" << enableState - << "| numSamples :" << numSamples; - */ - // todo (jclaveau) : stereo SampleUtil::mixStereoToMono(pOutput, pInput, numSamples); @@ -136,15 +137,19 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // math below should result in a simple copy of delay buf to pOutput. // todo (jclaveau) : ramping if period changes // todo (jclaveau) : ramping if curve is square + + CSAMPLE maxDiff = 0.0f; + CSAMPLE maxDiff2 = 0.0f; + CSAMPLE frac; for (unsigned int i = 0; i + 1 < numSamples; i += 2) { - gs.time++; CSAMPLE periodFraction = CSAMPLE(gs.time) / lfoPeriod; // current quarter in the trigonometric circle float quarter = floorf(periodFraction * 4.0f); - CSAMPLE stepFracCoef = floorf((quarter+1.0f)/2.0f) * stepFrac; + // part of the period fraction being steps (not in the slope) + CSAMPLE stepsFractionPart = floorf((quarter+1.0f)/2.0f) * stepFrac; // float inInterval = fmod( periodFraction, (lfoPeriod / 2.0) ); float inInterval = fmod( periodFraction, 0.5f ); @@ -155,20 +160,63 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, position = quarter < 2.0f ? 0.25f : 0.75f; } else { // in the slope (linear function) - position = (periodFraction - stepFracCoef) * a; + position = (periodFraction - stepsFractionPart) * a; } // set the curve between 0 and 1 - CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; + frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; + + // + if (oldFrac != -1.0f) { + float diff = frac - oldFrac; + + if( fabs(diff) >= rampingTreshold ){ + frac = oldFrac + (diff / fabs(diff) * rampingTreshold); + } + + if( fabs(diff) > maxDiff ){ + maxDiff = fabs(diff); + } + } + + oldFrac = frac; pOutput[i] = pOutput[i] * (1 - depth) + pOutput[i] * frac * lawCoef * depth; + pOutput[i+1] = pOutput[i+1] * (1 - depth) + pOutput[i+1] * (1.0f - frac) * lawCoef * depth; + + gs.time++; } + /** / + qDebug() << "stepFrac" << stepFrac + << "| time :" << gs.time + // << "| period :" << lfoPeriod + << "| a :" << a // coef of slope between 1 and -1 + // << "| lawCoef :" << lawCoef + // << "| enableState :" << enableState + // << "| numSamples :" << numSamples + ; + + /**/ + + + qDebug() + // < < "| position :" << (roundf(position * 100.0f) / 100.0f) + << "| frac :" << frac + // << "| maxDiff :" << maxDiff + // << "| maxDiff2 :" << maxDiff2 + << "| time :" << gs.time + << "| rampingTreshold :" << rampingTreshold + ; + // << "| a :" << a // coef of slope between 1 and -1 + // << "| numSamples :" << numSamples; + + /**/ // qDebug() << "pOutput[1]" << pOutput[1] << "| pOutput[2]" << pOutput[2]; } diff --git a/src/effects/native/paneffect.h b/src/effects/native/paneffect.h index bb675add747..38468269c2f 100644 --- a/src/effects/native/paneffect.h +++ b/src/effects/native/paneffect.h @@ -13,21 +13,10 @@ struct PanGroupState { PanGroupState() { - delay_buf = SampleUtil::alloc(MAX_BUFFER_LEN); - SampleUtil::applyGain(delay_buf, 0, MAX_BUFFER_LEN); - prev_delay_time = 0.0; - prev_delay_samples = 0; - write_position = 0; - time = 0; } ~PanGroupState() { - SampleUtil::free(delay_buf); } - CSAMPLE* delay_buf; - double prev_delay_time; - int prev_delay_samples; - int write_position; unsigned int time; }; @@ -49,7 +38,6 @@ class PanEffect : public GroupEffectProcessor { const GroupFeatureState& groupFeatures); private: - // int getDelaySamples(double delay_time, const unsigned int sampleRate) const; QString debugString() const { return getId(); @@ -58,7 +46,10 @@ class PanEffect : public GroupEffectProcessor { EngineEffectParameter* m_pDepthParameter; EngineEffectParameter* m_pStrengthParameter; EngineEffectParameter* m_pPeriodParameter; - + EngineEffectParameter* m_pRampingParameter; + + CSAMPLE oldFrac; + DISALLOW_COPY_AND_ASSIGN(PanEffect); }; From aeae8fe540e317ce76f21df61a66110ebc1a2cc7 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Wed, 31 Dec 2014 00:22:47 +0100 Subject: [PATCH 009/481] Revert "ramping + cleaning" This reverts commit 92aee711c1705fe50174d3328f34dfb6030975f5. --- m_framebuffer.png | Bin 13251 -> 0 bytes .../knobs/knob_rotary_s0.png | Bin 393 -> 0 bytes .../knobs/knob_rotary_s1.png | Bin 418 -> 0 bytes .../knobs/knob_rotary_s10.png | Bin 457 -> 0 bytes .../knobs/knob_rotary_s11.png | Bin 448 -> 0 bytes .../knobs/knob_rotary_s12.png | Bin 455 -> 0 bytes .../knobs/knob_rotary_s13.png | Bin 445 -> 0 bytes .../knobs/knob_rotary_s14.png | Bin 453 -> 0 bytes .../knobs/knob_rotary_s15.png | Bin 435 -> 0 bytes .../knobs/knob_rotary_s16.png | Bin 420 -> 0 bytes .../knobs/knob_rotary_s17.png | Bin 423 -> 0 bytes .../knobs/knob_rotary_s18.png | Bin 416 -> 0 bytes .../knobs/knob_rotary_s19.png | Bin 410 -> 0 bytes .../knobs/knob_rotary_s2.png | Bin 454 -> 0 bytes .../knobs/knob_rotary_s20.png | Bin 410 -> 0 bytes .../knobs/knob_rotary_s21.png | Bin 396 -> 0 bytes .../knobs/knob_rotary_s22.png | Bin 403 -> 0 bytes .../knobs/knob_rotary_s23.png | Bin 390 -> 0 bytes .../knobs/knob_rotary_s24.png | Bin 399 -> 0 bytes .../knobs/knob_rotary_s25.png | Bin 390 -> 0 bytes .../knobs/knob_rotary_s26.png | Bin 395 -> 0 bytes .../knobs/knob_rotary_s27.png | Bin 382 -> 0 bytes .../knobs/knob_rotary_s28.png | Bin 377 -> 0 bytes .../knobs/knob_rotary_s29.png | Bin 367 -> 0 bytes .../knobs/knob_rotary_s3.png | Bin 457 -> 0 bytes .../knobs/knob_rotary_s30.png | Bin 362 -> 0 bytes .../knobs/knob_rotary_s31.png | Bin 328 -> 0 bytes .../knobs/knob_rotary_s32.png | Bin 344 -> 0 bytes .../knobs/knob_rotary_s33.png | Bin 369 -> 0 bytes .../knobs/knob_rotary_s34.png | Bin 378 -> 0 bytes .../knobs/knob_rotary_s35.png | Bin 384 -> 0 bytes .../knobs/knob_rotary_s36.png | Bin 401 -> 0 bytes .../knobs/knob_rotary_s37.png | Bin 409 -> 0 bytes .../knobs/knob_rotary_s38.png | Bin 403 -> 0 bytes .../knobs/knob_rotary_s39.png | Bin 409 -> 0 bytes .../knobs/knob_rotary_s4.png | Bin 451 -> 0 bytes .../knobs/knob_rotary_s40.png | Bin 433 -> 0 bytes .../knobs/knob_rotary_s41.png | Bin 425 -> 0 bytes .../knobs/knob_rotary_s42.png | Bin 432 -> 0 bytes .../knobs/knob_rotary_s43.png | Bin 428 -> 0 bytes .../knobs/knob_rotary_s44.png | Bin 442 -> 0 bytes .../knobs/knob_rotary_s45.png | Bin 422 -> 0 bytes .../knobs/knob_rotary_s46.png | Bin 416 -> 0 bytes .../knobs/knob_rotary_s47.png | Bin 428 -> 0 bytes .../knobs/knob_rotary_s48.png | Bin 434 -> 0 bytes .../knobs/knob_rotary_s49.png | Bin 435 -> 0 bytes .../knobs/knob_rotary_s5.png | Bin 463 -> 0 bytes .../knobs/knob_rotary_s50.png | Bin 435 -> 0 bytes .../knobs/knob_rotary_s51.png | Bin 439 -> 0 bytes .../knobs/knob_rotary_s52.png | Bin 446 -> 0 bytes .../knobs/knob_rotary_s53.png | Bin 476 -> 0 bytes .../knobs/knob_rotary_s54.png | Bin 469 -> 0 bytes .../knobs/knob_rotary_s55.png | Bin 463 -> 0 bytes .../knobs/knob_rotary_s56.png | Bin 478 -> 0 bytes .../knobs/knob_rotary_s57.png | Bin 481 -> 0 bytes .../knobs/knob_rotary_s58.png | Bin 471 -> 0 bytes .../knobs/knob_rotary_s59.png | Bin 468 -> 0 bytes .../knobs/knob_rotary_s6.png | Bin 460 -> 0 bytes .../knobs/knob_rotary_s60.png | Bin 473 -> 0 bytes .../knobs/knob_rotary_s61.png | Bin 458 -> 0 bytes .../knobs/knob_rotary_s62.png | Bin 467 -> 0 bytes .../knobs/knob_rotary_s63.png | Bin 467 -> 0 bytes .../knobs/knob_rotary_s7.png | Bin 464 -> 0 bytes .../knobs/knob_rotary_s8.png | Bin 468 -> 0 bytes .../knobs/knob_rotary_s9.png | Bin 473 -> 0 bytes .../style/style_bg_microphone.png | Bin 139 -> 0 bytes .../style/style_bg_sampler.png | Bin 228 -> 0 bytes .../style/style_bg_waveform.png | Bin 15109 -> 0 bytes .../style/style_bg_woverview.png | Bin 183 -> 0 bytes .../style/style_branch_closed.png | Bin 138 -> 0 bytes .../style/style_branch_open.png | Bin 158 -> 0 bytes .../style/style_checkbox_checked.png | Bin 298 -> 0 bytes .../style/style_checkbox_unchecked.png | Bin 117 -> 0 bytes .../style/style_handle_checked.png | Bin 92 -> 0 bytes .../style/style_handle_unchecked.png | Bin 93 -> 0 bytes script/console/__init__.js | 8 -- src/effects/native/paneffect.cpp | 130 ++++++------------ src/effects/native/paneffect.h | 17 ++- 78 files changed, 54 insertions(+), 101 deletions(-) delete mode 100644 m_framebuffer.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s10.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s11.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s12.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s16.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s19.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s21.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s22.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s28.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s35.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s37.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s38.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s39.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s40.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s47.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s48.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s50.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s53.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s58.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s6.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s8.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s9.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_woverview.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_open.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_unchecked.png diff --git a/m_framebuffer.png b/m_framebuffer.png deleted file mode 100644 index 5c219b9cccf3722e31c47eb79c34ebfba6634e58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13251 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 oB}-$!E3$MS0Q6d}I1_(Oa zWwRG!KhPrSk|4ie1_1?K1BZZ+go5&h2~!rVJ#g^Cr3VjQefaeIFPl_<3Q*YrPZ!6K zid(f4ZwfUl@VGW-+}%*RG*svR|CwqFFN;W4$ItnA#+HR?(x%s4O3f-u@9zD6Zs*M- zA<^dxWfr%XNH^EMO;1byH+ApJ|4SGbXDvG49};}rC?R8o_#Ey@jCDbZ*^%zD9Zt4E zQ8w9_J@-NEpAfya*V)lOjPWwqH8AAH5Sy!jzlsLGunlSLDbUr1k+ z(ewJ@|G1gG>m}~q6wiA3St|0B(vqo59hz6?usq%26<7Qs&@*dcF+*jVaHf@B@duz^ O7(8A5T-G@yGywnz$(h0c diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png deleted file mode 100644 index f06258ae5beb286177a80d9d788350391a630c41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(r03aiAnCd{7$WX=I0VS>z8ndXIv4J7A`Hmzy%Pf@%{pEDAIAFKi-B;1-X#Wq zN&<@OW?F@OO%DB@0<>lNy2-&nOXW&}{DK(-6m$(70zwjU3d$QMOj)q|z`+w2?mT$< z{MFkJzyDfXcLcfhl&6bhNX4zviLb?)6a-qtFYbws+oYrV-4oeuCiW|ByrWgC;>&Fu-H?;Sko0FdV@cr8DOJAItcuJSm qYwy0K_N17n-=>%wSO=MYjWuK6riO!g6>{GH>i{Z`2{mD2q0yCvCg4pIM)*sS{~lI!AIzkZq^` zvo#mg-zfLiiRHhUyt+~7&Gl!xZ5j!WuKivv#o0OWR%N~X;>pF{rGMI!Sj@H?o)S3G zSLLy#=V0H-Cm&+=UYM+HTzKET`q{;{!nXO>w|(o0|FwF{UXgn|1p!GQayuO2vJ=F1+~^M^JV+*CJV)iN5_w4z8CT91GZiZ>S;Tum$9%$(E zXI<|esl9rQ(#3_7tIBUbxV>b-+7_m@4>H3TxMXYE7VMtD{Z~V9+RHN^E;xKhJm>an zwxZFc^CvZb$7C)E`Nm!O+OW&SaJtPb520kM%KtMKO{(;MZaL*b+_%|_xEfz%96OT0 z^LEBR`FV3>jJ1_FN-T2v+|)bykBU1o;84`Ku4V|BAcR7Xnq^_jGX#skoIp>E)zD20YEaUe1lW zu|a*CWKwyb{FN8(jQr5Q?3GuqE&tCb f?;{`m$%b+B83~)5iJ87YuQGVL`njxgN@xNAC4;tF diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png deleted file mode 100644 index e0fb3fa1df5cb35363e78da6d707504baf142afc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFA-p zPMZUO49f|wK+^kiB#?C88sM@s5XjI@F!wwg4rI9Q4F-}x91tE%{xxM(aZ_0|-Xk$k(` z`rzlVc_+2_**4Wa{I|v7ZP5j_&I~qxTV@5G50=-$mEC^^r{(k~@%ERn$Uj@Nw*R$K z`roFx+s=Ddoyy%mWBa?sEuF1TPdZGSaq8K}9Xx?6cO6;sRO6J&*1v5loD{5f*%eAO z%YOd&Q1yo4J|kiN*ph^}Et%Kq?+LsWjsLaU)?T%4S$@c0{>rPzxCLqr{^nf#%=R`{ WeC3rJyDtMh$>8bg=d#Wzp$P!Xq_&0t diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png deleted file mode 100644 index fee0a6942c02e0dfcb4afbcc0de57d1f215ffb96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDgXcfmy}llBFByX zPMZUO49f|wK+^kiB#?C88sM@sP%qmG$o4!N4kTUo1_MbTayuLXVR)VciF;oGsrSAX z<^M3&?_La$?Rz&S=v`v)rz9ZTZi%7Xr6kBNn1Mk+L0i|r z+QB0rFeE%7r=YxH!juIE4qmwQ;K}n>AAbM-`)u0DrZ92V@5NU&R@+LP_|*9Qtw{OwW;tbNLz{L9hs1{Sf?LElE!0mvbZy6y4*~8U z7M`DbrE~WQ&%FxE&um^guW07^PnYbvObQG4&dpHp^vO6D!y98JOL_Bu1^v}uSeDq$ eurrH(B787P!t_G&RY{;P89ZJ6T-G@yGywpoK(_q= diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png deleted file mode 100644 index a9f30d663eca38ecd1cbebda0ffc176294911d24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBB#v( zK+yf=Ro4#S3rvWAIAFKivco% z-X#WqN&+&RwgmV;jtlvk9Qr*4Xi;*_jd?(OO?z%BLRU(g~`_Q2cQrUA^>>s!`DSlhMz1Ui8{| z#yutey<)8L>P|zu8y-T*UNc_nu9_))>fb`1#R>{v9D&L@YiNT+efNYH* z_TdhhDi32|Cm zzT3wCiR#+EuiN+)88ZGjxKG&D$n^AXi&=QnzaZ0;QnN*055GEo<=HxuTdM0#K5t=Z zxmVNodGe&@niT=tQs-2rsC|x1SzUX{vWWLVsm=Yvfl5;ylqWH4Pwf#>dv-^_Tevuq z@v8Z%hN&X|0}>m*ny)gQARFs5N48n^^!;}6_07L4Q`O!*&p5y5V4G51oc)dLeP5z) szYJD+-H_uXd7*IUg7ZF;b3fQHuFw~rf6D!$4A657p00i_>zopr033Izr2qf` diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png deleted file mode 100644 index 7280d9a0b883d3ba32398313f081f96398d9daae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 423 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1k5?Pcv{te1g0&F%#8Bh`M*17cGJSLe;+nn zFE&$i5jov|rq-y9Ua6+X$`kSwF*`j=;}Zr!>KL5(+tN;K2 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png deleted file mode 100644 index 07ea2fd98c27f7e87c1d80370cdb2d9a0de87896..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 z&RYXqb_N0&cFTM`&xXUr-42I9*q-M=g5Fmmfu#S#SigHQKt{l`_@H-*Kr;AKQt-zl zporZP?~t#_q2E)0wydy!9RRdct|Z7Wm_a~6*T5klFeD){r=VfNlm!P4Ubyt&`Ku2f ze*gLVZ|@t~lR%XxJY5_^DsGidd@I$Yz|+cjaFK|q(c5gv@AVUNC(Ai~{v-LJich+0 zN$h2b#Yen;Cvqt=IJ_^4;%Q{nQ5Uwfk+8{+J%8r4zU97;HCEzVrioq*JZkcZheyKd z)7hEgD(WAVyVophJ#=lIlJHNF>(}?5n0Vy3OqkBph!ux^8uXvt`Do!o{-VMuf~pSN z-DDj!--vAzd2#(jvci3h=`Kamx6gilvGW^6JBf-+%vw z2Z`kY6&~|+aSW-rRXXt{Q`;jNdpBnRLBuX=m2(w1lEH`Saea14mMo|%=n zxMs$*RL3Ltj+;n|33lz_OsN+x*uQP>hI4yQ<}LgAa>j=nQ+(WidnKR$z-c(I_SU=( znFrpi8@t%Wd9 z{tsjQ?!^Gv0ng$Cp2P*cOZ0yd7yKzH_+t`KP|->U$O!qG9Qr*4X!~3?g&jc0sFnoz z1v3aJ=o(l%1O$d8B<2*9H%yqaV9V|U2hU!(^x(GBm`k*y=PUNdEDYqY0PxfqbNqF&ATB~?*mkYtABPsnFl zt?4CrV)K(39P13@=c$_;PxgJ3-0^J=d+Y(e$M>bbx>sIbsJhUW>+QU)MLIOA}2JSh{gT;r`kuQ7PK`njxgN@xNAsM)%> diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png deleted file mode 100644 index 571a07dcc19ef12fa48991fbd09bea058ee1416c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(q}?(f&$HohLAS#pK(^O~2q5Ws4kYeuIms>LYjWuK6re5h)MO3*P+O z2TN7;OJ-ev;{Qlpl)-s2Peq)I(5FoXRx_1eGFbP24(N8xIuIy-I`h!7XY7{>ri!s% zSQp4GvgnqRsAyUC6~LhTfK1e8lgnMY}*pMnQB` zPoki1k4zo@S`Z4O@{A^xJ~tb z+Yb56`7dtv@rlh`v7T#nt4t&B)w-813(rkw-(SRHxN^k;foRQ(3Vzupie^svioR`W zy*!eaIs=3jWaq^Hdq4SR;hBpk>I#!~$-Eb^SM{@6j(#EY7|wb-(Ok ZbhssS+NE^U8=!9(JYD@<);T3K0RRxKnj-)J diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png deleted file mode 100644 index f50be60339603fb705844bb266d502373cda6845..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILm*s_6JZdB-@OtW1C6yP z>zo9%MY1HwFPMQrKtb2QAs{57pkcz41qV)Cc<}1Ohu?qyP2kiQ0xCM>>EaktajSOX zZJ}lb9v5aYQCD3L{oAMh|4&m_h}v2fKcnD=FN4sO(DK+wv0}xR6D#DF3otRH8~J+l zDRKq6T~hkm@t{&ZCM7I2=tnrm<*!12d4uiSy}0+}^8{tym~Ir!Fl*lu#w~_@(wE)d zXkU8uM9g!#pY?);bEOU}-IK)qDeWXXUy*~SsDaEyhL9!aCp>egcA3V!bMYPFlgV@V z|NlQV=SF_u{P$UPw(BG9Sl@cx`Woy}>9E#=u}*Vo>g6o&dHntS0%uBn5~YE@VeoYI Kb6Mw<&;$VcSC4f7 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png deleted file mode 100644 index bdaa5a414364a2d8b1c670c67710f64465d40fb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx803t!Rj#_mxQ3eZiVhW@V-k>Ix5PW-YjWuK z6rjlmvmO+IY%U4%3ua&tP|!7S2nb0iXqYf%!PWy8E?v3(;K_&I|5l{g2?JFf_jGX# zskl`;@vT&o0#7S@S6hJR(kn*q{u@ar+b&Cc&VT#f+YhW>jV}sp%zZ57*3B@pJP6jh(!IE zw6rOC_Z~x?DbH7J;;ZWX60j@tWx~?FTix+nm4b41Bx-KUTHyL-`wI7*gYOh)^Bxvs zHF_fZbykMmv3C#u$IX=W_b&N$=j#8eIQ#mp*`2fPS$$kgjEy=b2B{VssU+JhzSrU} V*u0Iw;yBPt44$rjF6*2UngElko+khR diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png deleted file mode 100644 index 393a509891b5db1e753f9ce029442e9583bad86f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#!gXBd2W0r(i2;&+_dsOOyTss+NkE38m5$vK?~t#_q2E)0#?A__ ziU-;vQ4-`A%)lU^VBp{p5Ry>PFk#Aq1E(+CdGPAP?|&1k@;3n09QJf^45_$PJMkt{ zlY)S2yRP|}jy1O*{r{hKZiV2ix#ALh5Jw0NK2xiW_#bP0l+XkK1~!@> diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png deleted file mode 100644 index 6a7e0e737b97f5bd8a28e84d055a2ee3e95017f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx7~$g$RhC_mxN>!~0qkkOZm>dY1@fC|c>b9SRBgnjHE)1!!#3 zUyD0HTO>+?{DK)61Qc`)90EcT3K}L%S+MlLg-Z{feE9nN`h{D6foe{8x;TbZ+^U`U znyE>Fr78VRMp2Z4!A3dx^#ad+TX24!CaGM!^jn!7!*AfVn67A01${{6K&R?cg_=lpi2X>OUun@qg2LO*TPynSTxy;rXUqU>iK ROa}Ui!PC{xWt~$(697?7ndkrj diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png deleted file mode 100644 index a0c644070588c549167b0da9a0a8533b5248cc35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#B)BUGBItQK9Kr~Cmk4AiTIskQ3JLj|3^dK;i-|VSaIumgzhDLi z0R;mGkARSbf`$oG7VJ52;m(5(zZs8RJ_%HD%+tj&q~ccX#J5sS3Oubp&j^@sMxXo} zpK-gR{lc3+jUV61v~y@K)!5}XdF!n0lR_r$%ePEt@MzRfR?|A5Eu6%9bi%|=hVyf_ zAD=YCH8{yIXX%35tzM==v4&GtM>u;)o(T8qk5=FJ+c*R!Dz{*4-UF zNy`Oqd(V64XuIXkh3>^`Bp9YCM=`KHJU3zSfeIHv=AYeJ{vy4Fc7N_`7eBhQc;`LS zKfm@awC&12vfW)Ujn#lxvO(1Q=f{aFZ=O(oDjmL$-|isChFd@%F?hQAxvX0H|LeGegC_U@y&D2 zFsncCcSd1iriRiBqw|kfY?KYS+rigg8avaHb)kcTmcf1&M@L4cnGsy)zt+YJMVK5~ zAuw0`vD|fUya6FH&ue}-dYmzQ&|Ggv5bB`>4Ju9R?WYzN1k22Jc&&is1 zZ*i1N;R5kRZnI`gSB{#YzA62LSAxx=2&RwjTh+O8=a~Kfe#&Fd*B5OzH|lKiGn_y6 t{xB0Unt9VCWya=%QyvNjZ|+Q&u=CO2Yc9~^bO(Bf!PC{xWt~$(6990XkDCAh diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png deleted file mode 100644 index 5b3f01b10d6a92616515403b8c8e20656f58c42b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDUSf35ZBNjDgXcfmy}n5Aj=7^ za5kLbb~psihA@KOB?1|COT2w=MTdM%1{%WiHtZqLOyQCszhDLi0R;nVhk%5Fh6z&^ z9Juh{!|#gbm)n5Shdo^!Ln?07PP{GDtia=PcqzxCrlnW^|G%u}E!)+1SKL5-HBNhZCg0uY2d9ko&HOw irij)gWtn}{_{!KT%xh&GG7ab(1_n=8KbLh*2~7ZYA&s^G diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png deleted file mode 100644 index 72782ab6a738a7b190593ec722609d79c696e74d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 457 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspXHth)a z^3LL93p*ax96ovKEYW)&i{$>7Te6FEcV2VWQ1?%Do}n;b^S#Hp4~qpN7galdYM6P# zed3Ln^UpgTRx|ClIL|BARcO~>`;685$*UtG&l{&2Tx{jE7c z1-BiX-V!Li>u1gGN~xWt{an^LB{Ts5)H1yl diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png deleted file mode 100644 index c48b7e5e7dc4f8b198cbe10b6af25d107706aac2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDc1m>5ZBNjDgXcfmy}n*h1?E@ zph>yx3=Ddg2owx_oe=sx1!#iLvzPA^4Aof*aSJutzV>KI zzv`?|(YU^2-=F*Hs~;OJO0CYetdGmR)i$YKCEt0XV!6_P=Xon$Uu&D0xIE_T^B)YX XRXn!~^#2qCJ;UJX>gTe~DWM4f*;a>W diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png deleted file mode 100644 index 4d50a6d306da9c27d3f3b145dd357bec922f273d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDdPa25ZBNjDgXcfmy}l_9(~J< zQ3h%kC<*cl22u(J4gm=T4HFg|xbWb^-p?K;K)EfRE{-7;w`wQe7HU!8ao)}PL`mWQ z|E;&pPHR}@#Z8!}*08j{D@3nDX6Bk7%Q8nh6Yir|_A8gKRH<;gTe~DWM4f*{yOt diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png deleted file mode 100644 index 95f9935432605e11cbe875e8adb6b63bfbbf771a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDW?FR5ZBNjDU$LkK&0wp2w|X+ zZihqAM0{^W2fa%K3I=~n0&2Hn+?5P8O}He;FPK3v!*k zgA0Jtn><|{Ln?07PQ1<6tRUc0u6e?h_14P&|5u;gTVG@M*`}c_P$g;_N zyc6eL-1wnm$BTDNELN%vuC>p4N-mykGh9`)h2eKkS%PAH>mH8JJ)1aq8SI4F64vq8 z|395{$8C%0{AmdK II;Vst02hgV;s5{u diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png deleted file mode 100644 index 8df606d5406dd440388088dd23566d4dd5207a7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDZc=p5ZBNjDgXcfmy}mAuC)h} zKxEPD0%0JNZihpVxj=T%yF?)Av?aj*aa_pPAA8D0bsL~h7(8A5T-G@yGywp!2ah)Z diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png deleted file mode 100644 index a3a4fe1cb26877adc174763146d154416c4a68f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bd-e) zK(@=yK)48y;dVF#NJ6CG;z92cgFht!xt5dMd~ZdEd`$+LmY3eG0yJE#B*-tAfk8mQ zz`->jB%z>T!juI|4_tWg;rGAu{f!HNDh_$NIEGZ*s-1XS=#T=B%i*(L2cx>&p8mHF zaWZP&68yb&!sOTt#-$D^b9Ucf7qePRAn8$C$I>b3CQgzsJ5-$VkF1as+O)DUx3=cg zYO#||CsJOW&;4c;Tre$Q?uK@cibuICmbT0Pyv3p0XJgP&({|mr-y`29cDiKQtk)LL zXUa}owzx4+=4pacOn-*!oCEU}{Tp9)2(e!3tnxRBef;OsfBTt2(*<>P`*V-qm;SxD u#m}i;WP{|zvJ^q%+@tDh8%_IvF?~J7&vD_<#=k({FnGH9xvXV@SoV+KCsXniP0i*;yo; zq};SC|9|iC{mF6m9Pftu=N2q24Zf#lB<}baHi0Xs&h5yQOP_Y+h|KWlUo+*ao4%yc zrN)Ccw$rD?bvTMIQMy|FZtp!!3$28cZ!{HEZPvI?-!8TFVdQ&MBb@ E0K%b_PXGV_ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png deleted file mode 100644 index b04d702333736cb3437d686ca4746b27b9d545a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}mgFxLi> zPMZUOBm_Bc4RF~R2I9A;hS2&Dx6p$8+@0O|UBaci`Rj=M7B9Bm#B{ted_$hnG9;QIgw{ zpi|H5f-ID!14AO-XRtB}&u4k^@u%XKLr)wPcXa-qdqyswUrUW$>9ibQpsa;W=FQ97 zTIIK1dGR#wO=Ihg=Ot&$I&-dNMp;FyTwHjzID5(21I*r;Aq{MWcNPh5Y%Js7Dl=o7 zM&N=u8P|_|GX19i|NCUs!iYtVpRR2CyLY3VSG;GuJnNs97uugXgr}x`+qm-9k>q_J XIR)QdJ12hz=qUzIS3j3^P6P0|$?Qz>tK3h6z&^95{dB(t}qYe*a_2D%cHFb%sX*l%fx3V zFSQ-!vI{9(c=6^DX5-HDw{Jd{NzC8=b4B99S?i6l(GWrJE0G?j!gW(Efn4uvQ9v^2U1IRZBp}0SOF+oi zWT45PdITkb7D<-``2{mD2q>uN8aM=mBos7En6hB&feV*z-+l1p!|#7(71+)w}dZ@#-=xg_l~`;C6du<1P=3gIHtKXXPmziQu6 zmhw#Ev5h7x(?(rq!C8C_4s{F?zmE&Qc{g>UfcTcUXd&74#WjTmERkQgFf^E3^1Zxq zV%sA9(4dS9v#T-`b5AWT-Qt~cyd-+stguy2t4q_qvpjO$ttOWH#_a$1$rEqr2DaPWsQYuzjDP9LB`2K~fu!5v5D44jL>PqOcP|FW2zVAB^e!>@Q&RB9Bp}!KRwEp_X^Q;|k3ZwzbpPCrT_GJDmr`$*s@sUI zsO!wLHEtKUm%PM5VO6Sz!(t{DBfbOt+u3jYoiU-scjG+`Az8im0`n%TdARy>GgQdqF$) g@;;k$zK9%UUo9Y%Ait>KE6`&Mp00i_>zopr0Dt$LM*si- diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png deleted file mode 100644 index 8cc83bcf3f0e90228dd17a9fbbb109f0e3026ef0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 451 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsp$aOmu67n@U^m_`>-s!z7*n!SaDGBlmW?&Fd z&^54j2nb0?%qeJ?FlE8g0|!r>y>RK)gC|d)zxwdu_uqdaSW-rRXg#v zP?G_Vi?imU3eB#rMNJ7u{{0u9wN6$EC@Kw_9!r&HAZe?`qxR{-+E47aP3^!zL4E#WSK7QxU>ATTAIJT$|JKG zXH!?SpWb<@d(PtDUMDNv)@?f9$+jkSzyJN56;3A0Hu3jPI5vat+O_1VhUZ^eEENjB z(G#lID&frEdq`sP;s3Fcz0ddETI8?scJkzh&rNq+4zGFHclP`8fDl9Xcu~XfyNgc= ezDhd5l7Cu2GW7FDh7CaPF?hQAxvX~Up-Y?h0P$<&`)ryEY%d%nzD zSLmwV-QTH(90!>=S0;pZOj;4WsBeWx7O$qnEL9_eBjTxEDz_r?|NWo%@W!zfyy;h8 zz7IX(ny7Ks$AD9gtIbB>sqv}KwmbJ_uPn^aGZy-&|36K1V$Qzv-+`WE@O1TaS?83{ F1OVpOth@jK diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png deleted file mode 100644 index 61674b87aa15b34a956ebfa0d1567a3891a5b618..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 425 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4ac-5U&HxE&5rw9Yq~E<5AQ|*7G5Awb@W&(| zTO-IA$O!qG9Qr*4XiH?rt?NKbWlMtmf*BYD6m$(70s=!43d$QMOqsFZz`+w2?ml?+ z;rHKv^SPhT2P(Ye>EaktajSOHL$M|Wo>orQVByP3Tcc*a|3Bev-($m%H~uQ_$luMp z+{%H=Yt7o%G24WuvOij{ zr_EP3-m&l)r@#&4U9H!gTe~DWM4fA=IY0 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png deleted file mode 100644 index 1fbb754f79f21f9e53bcedfbb1fd1c1af49e9851..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 432 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsr&$+5ZBNjDgXcfmy}nrnBWQ| zoi+ymNeFV;83<(9E%Wg_9R?S4I~)RJdtHbClAhrLY=Mmf!&Hla<|?# z-f=s{c;03f`!AE=JPFUhmAVbD#6u4p+N3$faKk@WQ|6zp=4yS%XW0F@udQ8Z?GpX; zqS?Q_k}1D*ZaThx_v_2geb&oOwq`njxg HN@xNAi6*QB diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png deleted file mode 100644 index ebe7a5011267837056fa5e2a354e8a5d88504a9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBHIOC zj_drKHU~hsK+@P^ZpeiZ#PwgOc8WX`Cf|Sqo;_O1@pDR&e#N=Qj(^bZr-i@H$%sq-`sx4tHL~;195HfH{hTqo zHlJzMKHrmSN^@6i&0m(uXspP*CFWOJSLUY5^V0WziRPa?w_6tIHwI5vKbLh*2~7Z> C1E`b$ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png deleted file mode 100644 index f91a7a1452e06f180d3cd41dbb9852d3813b274f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFByX zPMZUO46{yWh=|M1Kp?|znUCk$a3Dj$TpJ?pb~psW@H__+_r4MdB>f-8`rV5GG6J5( z2fa%S{*)B_F$u_3w9;`q6cX|^IrMu9(7KPeez^c`S11Yc3ua&tP|(&juy6u|dURD0di#WAGfR_VmsLQMudF3Ex{flInpJW%cW z^Iw0K?+nMTcix$PWw(W*99Ud4cimrqvgHwjQhnO9eeZW4(+~)I9(i^3tQ!JTu5g{Z z`gY-)eTao@l@ zd*{8F_FY6IL;dYE@zxVvno>@RT1$I&&RX1Mx%iL#DZz;IfxQlMUN}7}=)FH(zhL`( z|FTQ(+RbH_y;xAWp>@A`x-qY_rhAj;m2+(~x12fIA@nS4yP3}nx7k%MnECFB**#TH RZv=Xg!PC{xWt~$(697~Tv`GK} diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png deleted file mode 100644 index 8e798dee8b1186839a99ca4e929fa9b32f33c4d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk8u}5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1kaQz>~`rV5GvV-0w27gKd zGVGRkhkQ*A{hk7}La4G*0BECZNswPK1A~Bqu7N{9U`RqvL3zW3DGLr9Ja*#3gXgb4 z{QhgU#(Nb|;U!NO$B>F!r4wI^H5u?UziwFA;TJG-LEyK4`kH6&1O|Lw9=(6^o1n;* zE>k0`EpnbLT{-jDIc5$;n_s?8QwkR_U45RS;_Ck3_Uq<1+q^ZTx5P`wK2Klz*i31$ zocn%q!dHtzzCYb8!vNK2P>EdAB4#rtMoAbZ5TjA=(cNZIShGynTOx%WA{;l`PZ)%IT1-M^j5H&df} v;UyI-;r2)#hmUTo1_jdXlD;S1`fV5^N<`eAsInXb`i{ZV)z4*}Q$iB}nS-Zt diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png deleted file mode 100644 index 36daa7aaabe5fe6b9258a9cee059be4fddb0384b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspJ5k5ZBNjDgXcfmy}llBB#v( zK+>$!8Nzkh83BP5UO$q`of~G#7wN2KVJ^nj?(_KO1i_iEsd_VJsNh?WH!))2Q z^QXF3d+&YKdBW11cV5*3?Isx$6>TqFhx>MF8=j{+e$@EtblUD^%f8wbV*acQ79IKr z_oYl)Egw7cu7JAS)`>qK1xu{^6Fes8+!9WxwZ$x4-x@Y0<+y6BJIZ=vi@h z@idL#pY~I{7u;1o-6-MQ?vlLi@I&r1WhJS1A2&|8krUW#bEE0`S)v1>2}Ev(Lm&*#b0Bf=D@?3he85hCxm=W4*i}2wCLMrj#WT=?yJD|#oo-U3d6}L(!UKDFm5O4|CF*X%8HCe0s`2YWnu@0VF zDjy!OH9o`SB4}|mGwQnfjHl;Q`#YjuXPyZ6@G!aLc34nM^;E;YTAw!C_1-s5JYULi zX=n1UojXjvc`)T!Z#bV8=EC*kV(*Q+M>evDCCPuEQqmu>__2#==F&*yd5Fa9u!A@J{aj8qd4m+{O0RD){AGTQM_z z>ch`s9%}zJr>g%EJ{3E$r=!!I^U;au=gE_|N1hkw&=qY`|M$lQ=s5;YS3j3^P6AURzSAr*>E7~x;Gd|0+HL{5D3Hb97x>z3P`>8wJ86Gv3~br zfb5`miT;mbgFht!*=~nIyv|32d`%Aho&vPaOqFpr&~}BAAirP+0R>$HYX^^jz>x5S zoPzR(2~!pvIC$aGgD1~lefa(7ZzH_ju%?AhGcA!TC2BG{ZhmQ)9VK}{eCEY z?el*phRnN0wQpmks<$vK|8i)b=4O>e58U08(xs2faU8LGxALWo;G&WRv%V~?^2~mCTk(V)3l64>JM1#^CAd K=d#Wzp$P!dSgWT1 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png deleted file mode 100644 index 80991f557d771f79021d8326243ebd2cbca1977f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#U`shwc%c{Uu#aNQdWB!S57a0rCqc`n@JL>Q3meFdc6X`{dY!&tw2 zF+g_EyTss6Ng-d8fo$VC2e(5ZKz8W&6rgqGrrEqe+Z9TJ{DK(-6m$)&9XtX;5>j&t z${Qw3S#aRsi3^t=Jb(V`!|%WUUY(f30aSaz)5S5Q;#TRzhfGZhJS~nVFBt`%)>*6Z z`EUG2=>V1+|5nu8%Qc^|TIKYss-ug%Qr1tI^}JH5h`*q-xH-W2RmTgH7q?$8xtaUl zRQlzfCtvp23D&+{Bb}+zu(E_a)=73^iry&CKFl;f^TdNfDO(gF_+(@V>*PCRk4=NYg3buoV~W{0UjzcF~a L`njxgN@xNA=}WPn diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png deleted file mode 100644 index a99fefcc2a34e2a834cd0e3e9eaeb21d736c9c69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llBB#v( zK+yf=fXWsg!$fyaa`x;b0Z2U z=zRsG(f?tr-@OfAt7IrL%*j0t*)rn_6NE|r6kBN zn1Mk+LD#_A!6P6rBq67uykWwW1qTkEIC1*yg-Z{fzxwdu_uqd$Wea};)!*@SaSW-r zRXQXJI7!pSsU;^-DF9j)xO zhR@bSdnfHV#N~FO&m(Yl(2>XcY`ahY4Qn;s7e3SdK;-7VyHqOXXu5Pvt~($PRj!82l*-$S|&R2>F^E`aK0`omS-9nLyhWOM?7@83Yt`4XhnJ0zwiJ za|((ZCQO;TVCjK_7cO0U@Z|Zc55NEV^ocD7Dn9S&;uunKD|g~sDJKVk*7S>EUSUFl zQn!BZKbqEdOYHf1z4yiEYIH1Eqwa4~UL!I?N^tGg{ie6#v{ z$m-vGjeYO6m%aZy=a}4K2a#=WW*Y@78UA_c_eQhu4Ws{?$9B9|1MRq0#(ArsoqWJa z^sM<)R_`s7U3@Dy2&twOIOtqkvHcs@N3|-R!>!DxO|=BKZdo|Zr7-{YS@kQoznxe$ zjny;h-Qqvit0UK$KU=%Y-Bb4AbdBdif$b8OB8p{yEPvm4k~8@^^A{bl|9VqmL6O1W M>FVdQ&MBb@0GyYs`~Uy| diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png deleted file mode 100644 index 4fded0ba158a603a1dd2aaf41ac08b43ac9d53b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsnP(S5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#W(Y%3tc^K3Yfbln>aB!S57aEQy!ARxnWqd$=JJQwb9A`Hmzy%Pf@ zy{|?2KaBOe2Vw`kOAP*$6!JA0$TqHX05U?qrvPpAY@MzRaz{y!UoeA!g06wJgGWF} zctTD=dBcP$a~CW-aPY*1I}e^bfA!(_-+$2x3ugjVpZ9ce45_$PI`OSolY)SY<4Ly`Kl8~&mWaaUHe4=}WlKj^z6@6bO9x>xx zXW_3Vw(CJna@-adZ9g9af0jc>oN^de36x%wbN$6XSK~sgTe~DWM4f2Z^u~ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png deleted file mode 100644 index d979b1b4f9723b2b709532b99d88bcc761013dc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDU$LkK;*PJKsVD0 z$grH?3K4PM8sM@s5XiP$<^vG{l5U4XTy_Ql8IBwMeQ!kr8J_3Dfu#49NDz4~%Ku@k z-@OxDNu3ZffKR&(-p zda;#Ww4Z7F?f!mj=S9<-Q-x0ir$wcRtSso; z64>!MswQcEh{dW6_os;O$T-_+_>9y4hQ6?L!y)&**W4%maaQNjsV$K&J(le;-J{`p zR^?BT^5}Is*>(zkXL6_1@6!5SA?z@D=ago}65*#dO0O7{nw1**?9%0D_T+xr!{`$t W!TbE@Q&RB9q>!)4KoPI=5fFCh_Y|NT&Teg8 z2XvBpNswPK1A~BqlCFWZgGWGMNJ36QdBcP$a~CW-aPY*1OAnqrdH(9dhu?qy{R=X> z768<8*VDx@q~ccT#G7JG20Sj#f=5^{DlOX47#;KJ|NZnJFVPE?FEjJgXPG>3RJQNB zRFY&q@44xT>((wRi_$H(ZgKNkEGQ`}V$s^lxcg6y=^LZ+{*a=1Gj=b2+kG=TOjuR! zty9zFA4~$eGmd;%YPxxGr!fDP5TnUfru!G)(x?!cx~IsVvt-M|f3Ixzcel;4XzuvD zK>eM5t6|bT_w+@oNB&>%V(PZr<+)(W!d)o^^%tI$f8>1hul#h^`t09v#R$ zYuI&?H}zq|OqYdkCTJh|(Cl=?YumMSpL1c=_m&Du`|Y~FFc;`i22WQ%mvv4FO#tny B$jATy diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png deleted file mode 100644 index 38d0ee1af9d11ae541c4170c8f62b44b71e18633..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsg?kr5ZBNjDgXcfmy}llBB#v( zK+vtzPF-*Y|nEb zHQrYuLFBb4|A(=D_hNwTfM@YR?-GMQB?W&>3i+B06tP?4?f*Cq$PWFU0(8LZ9Hs3* z_o$Tw`2{mD2q@?pSUY%l281N!6jU@!m@;j_+AUiT9K3Mp!IS5&K79E7_uoI0{S$5h zb=>lFaSW-rRXXveSd#&d%VnlVO;rY_`xU@|oQka zCpEcgJ}OQ6Z#!fy&Q&f@eqLNFwQBM16U(dItup7ae%Z3z@0QK_ez!RiL2^7UpH&T9 z&$O-BR>phLeoJBFPk~s?3AZoQ8a*?sGuyxC?(F-@*}rO4yS;X6)Sk`y^}07@^(T#u wF{izs1Szc$Xy)e<(Eb(jNO4n*aa+ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png deleted file mode 100644 index 8d745ea5ecc806c4c04388b0435b531e61ae3318..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llB2^zl zr_BLChUElTAnAQM5=c644RF~R2xQnT^YJ_z4rD-(+u;xh!}DCY$B8iCJ24)o!XP64 z4`coA#Q@m>&*FpLB?f;=3jUZB@--PKVqE9o|2PiF4*i}2v|4?0uqx0cDkVXF!3+!n z3TnCr)(##4fguTrIRy<9rYtyc@WhFe7alx+_2I+szyJQ-oANIXsQ!+pi(^Q|t#+nM^yH%R6Xp2_#s3~Lxy1Lk_EBZBiTe@V)(&^!h5LJ^T@wo~Da`Y~XdhB^ zAgk*gheqlP!^nc<{oVEpe%#l6{p+L2+XbH#Pkl5|-qw3u`eVrNr7E@$eM54)7*_LC mi9}Q%3f*dF_SNj;3iHNx$=jxx6-R*nWbkzLb6Mw<&;$T0lEBme diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png deleted file mode 100644 index 7e0b2eda3b9c6dbc62f64778e632420629efdd26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mg^)dvK zj_drKHU|J1mJ?ior1#}WqbhqK!+C3f%g#U`1CHDdhXC2Wx1xch=ecm78&N=p_mxQh z$8o-QVt{O)o6+9aqWtcG*a6Sty)H%sy-N)Klob3i2`FM*=MeZhA>?av==T($8)R~q z9|Sr{y(Gvln1Mk+L_t&6z}msXGaxV|p`c;Hlm%N496WpB(v>?89z1#V;lt10fB*IT z%S#7px$o)X7*cVobmDWNCI_CD;tmC~FB=+pxsMgS|6h})tQ>U5{>;a-c~X+eFO==O zrk$Lkz#QmVvpYTQ`5oi=5B|E&OPv}l`PjT?4e|ef1>^r z7b%;t#zV$6jE=>MhJQ13-z?aB(Ao50yN7n^9xJXTaeDDH)7^MeFP&6BpTDS4&25sq z?#UX?-kIUKC8c@ZAMLj|CfsskWxCjQUB!9J#k+s}`YziRy;qv9DZX{ZV%GceRROnS z@_W{Xxz7w|neK5~XyF{AH327TCN0;jH3&8PI>X%2S^Dk0IDx4^uQGVL`njxgN@xNA DL0P}$ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png deleted file mode 100644 index 9943eeb5dd15a0e0278ed3a1c5fdea45a04b0d92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 481 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}llBKwuT zj_drKHU|LNmJ?ior1#}WAnCj{z-4D3kl}eY97wwE4F-}x}uceyG=pu8}IkV*2R;j zozd!Xk+PY{&}do1=cqVY^l!%bH(I+7I?EnxKhg0#BKFCLJ#+ToQC~1G=w0~po3$p7 zkC;}@ac3^vKSL}^-8n>}c+pk+5{Cpc7k`!{-qrrkG|cw>5j@==HZLqFQfPbZmTKw8 z!Z~~IcQ1{9{w?s(v>aSz&SAQgA<$lok%Gl&4V`H#n$q%4!89ZJ6 KT-G@yGywnhJfLui@9p78gAzzb2zo!5laB{0qEYLk_ zB|(0{3=9GaY8tu*)(!z7aS1sE4HKp;SbE^#*$bDhJb3c_#miS8KK%asPpRa=b)b$r zo-U3d6}L(!zT|5%5NMgcC?v$i=A+xesxN=FZI}>fc|9gboFyt=akR{09E_P>Hq)$ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png deleted file mode 100644 index b6dcbdb4327f3e255b5b9b0ba482e8b02e019e39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 468 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsm1`G5ZBNjDgXcfmy}llBKwuT zPMZTDY?B5@Aj4&6AdvJt9j0Ke4dU(!g2=iZ4uLQ{&xL!O2=l!Y<8vblDB^u3637NJ z{2#{p-2*WKp2P*cOAP*$6#Ov>$W^q`@qZi#WQTlB4*i}2wEbpx(q5orR7!&Uf*BYD z6f|@V9DD;p64EmYiW??OS#aRsi4$ipT)Ok%`Ku3~e*gW)Rh<+FRDaLY#WAGfR_Vl> z)0!LvoV&vgQ;4OiN>@=+Qgr-@S@8yKp%(GhXCCe|{T9Lb^noYmkH|yfMSeXxPkpVE z>wB8rtD5+2*Ty{jrBM^%TYcLuTk=KD@4cVYoVO$-$7I^qn9!NTG4RF~R2xP#K+u;x(+xJ#9kn}tk?t3Q&$nd@r2_$`PMtfh2 z^1BBT33wLoempejU1IR3q~MQ9KoN}~W1y~(ugRg`Q-JpFZ;-MDIzzc6$S;_IK|n!U z*TCArGaxV|p`c;Hlm%N496WL1(t`(2UVZrREaktajSIV>uF5} zJS~@%I=G_Nzo?45Tk-O@{ldu;o0(q!QG9A`>>E^g?xgq8nwifzz0)Vn5^1mQ2@EZ7 z+~WAsF35}3U&GB!ylMaEEcdWfr;ZKm|L$*J=}?#TWd?_-!THVxgBj;H|6`0i-R`wv zrl{~|5ASE2PcXmpynW*Fz6SkgH@)10&$NG=VE$%NOK0&JWm`qV>ApSB?V?uprT*oe zDpv7wF;`=b@>`+vX8X*~Pptp@Us-(nYQ0+-2lrGZrmdKI;Vst04D>&TmS$7 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png deleted file mode 100644 index 9d74037d9a09ba96305d458a83fe4a5d59e4b8b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 473 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mAuC)h} zjvM`*HU|J1mJ?ioBoMjm476S74P` zuS9~#Yf=6WWBu;M0NDZ0;sc(<1-(lQ{*)B_F$pN5Xr-f@X$545d`%Aho&t2k_h~I_ zflg8@3GxeOU=Wa2(AG7ub_fWGOGqy$ZkRA-!R7-8Pn^AQ>A{oduReVE^!x9>1?RqL z0Cn8bNJ5jIaXm@8a&pZ;;r{$KyB$UGP^Vv6r-RBchZb*Kgc<`qElv8%yT#^++ zOoy2E37+3LY3HBJ)o+eiR4iH8ZS>_z(aubz|BFq(uKw(D>5}8~)t~m5Nf{env3cpC z?va}Gc3F4cxwk3z+P`s}+4Z8&;fg`-qs4CZKj%&K?mKfNR5*4fd-U(h>t1~}g_?!F zkDl+2;+nQ*HBXkvjqsf!3cnIo<=zyqKQ8{DUi#<$!sbgrZ!&ng`njxgN@xNAN*c+I diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png deleted file mode 100644 index 4b8abe16349c9c41a2ef0dcf5521760daad1c1cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspEQ;90Fu}oCpIcI2R6N_}qvBvVHHw z07?IcvDULZ{O*C+0Z-zB-X#WqN&>QVGp$0tCWn4c0oohn^|l%443&}~zhDLi0R;_h zT?2=Jkc9M{g7St56Q?X#d*I-Svls3>c>dzmhflx%x*yVB15|&@)5S5Q;#TRzmqJYr zJk75canEbeh|`eMe)HRYVWxzu!)Nwo_f;n6r7jT)6>yC-6Yg?mI&=Au<@3Gky)N+^ ztz2vqdW&tPx{Z*?&%H|3M_wi02z{=tJmIePT5(>ZSDvB)%R=tY_|JJFX5SWOYhyp# zOA~)>Q9mWRtUIT7){mt5Q@@lPs`HJz;BKtuc4Bhl21m8v=bsdJRqvnr)4t@wfyV;2 z0w<)F`Rioq)K9;^_|N;PzDK9d-0^ae^z}u#ANH8+?Vq$ICHAq#Hp{e*;5J>Z`TM$D h1#1nqdg%$X>QCG&<@>TsUkB(*22WQ%mvv4FO#pHHxKIE9 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png deleted file mode 100644 index b9896bf982f8de94c18cf4b67b7137f3362dd544..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png deleted file mode 100644 index b9896bf982f8de94c18cf4b67b7137f3362dd544..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png deleted file mode 100644 index d0777a130427a4498121e2ce215c748b927ea096..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspZhelFbOL>NTe`&yL$ z!&tw2F+gs>v-qHQiNT+ef@8= z$K|q82Y1I&)&P->S^xiY&uys^33%Q*r8;fak|k3X`FYMeUU*=t(B6O``#HteZk;j^ zRdKoQm*VQiEn>2tBV%@~>YAPoR-@dH{{vf&{OftCsc^ByHl2})_xF}7?h|ZlHCUsI zH?pi;ss61nrMLQ!-K8DpA9Po8NogFeanXL_+paO;h|K2;9v(fJuVaketh2?p{BI57 z+tU|#vXS%F4BpOXX8%n6+kdaU6z@Jw(yVsNM4SB^OLoLvzUjR`bA9P|jjq=(QgXkz m3C(tE+9MfdkWBYH{(<>pkq`^g8YIR z7z7kFbPcQ>JUjzJ64DDACQMndW$W$(2QOTD@Z|Zc4RFn7=T_Kedu zi?7;G$#{_4!Nqw*VzQl~@5a}U=lzqByXyUUvys)4*eS~EHx^#nA9wu*_nj~9r@sZB r2s^XQb=uBN6{$b2T)A~cm?b~pu#}lm@)LESM;Sa_{an^LB{Ts5qrgFht&e@qJbnhX?iI}{T5IswQI{hk7}U9(d65705H zB|(0{3=9Ga8oCD74jus^3F$cncfZMfB*d}75dN))bPmD z#WAGfR_Vmt;wA%umi`3IwHi`cxq*&rHqQG0pLzH0_zBbXjGw5TfAdUlBa2aHWN78Q zRX!U3w%)&!+@sjCSY1@8(05AHler5nJX88``E7qvsT#|ss)YZA6~@23FPWKUI-dX8 zx^%(v^DFHdYi>^F+OW2YIWtG$mqh2YcX=H$=RQXmp5NXZl4Q5p^i$k%jnJgl${Zz? zXWs;q&0T*B+e7Sh#_6yHR_Gz1>9OEzknF#bPgQu&X%Q~loCID}e B%s2o5 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png deleted file mode 100644 index d39f1544686eb0e7d9646125799c7b7cec89ad22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^K0s{3!3-qDE%demDb4_&5LbIA&;S4bhs9;D<9X}= z6l5w1@(X5QD4TrN0?5<%ba4!+xb^nTMn(n!0hSH+zxmf*T=`*cN`px3q+O;}YjkE$ lOg5VNX7L(#MurbLOiN8!wJW+0odRlQ@O1TaS?83{1OOjEE8qYC diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png deleted file mode 100644 index 374f20eec6f925ca115226be5742e8d7a98c1162..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^zkoP}gBeK9h`Vb8q}T#{LR{^gJj3F$nRvStfg&?K zT^vIyZoR#;kc&Zq$HDRYfBRe^!ERTH+j|o>$j(Z!Sd{zX#f!Nw7AI}IW?g~`zSMl# zG}ZcR#XT2yRPcXZxRkh@EHbdQy{mI}BBvIr30QcWP5$+`xx3%@ea|5vDybN?4(Lb* MPgg&ebxsLQ0LgGs3;+NC diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png deleted file mode 100644 index 962f91fbccb6e784a5002aa9efa6815e7d68fae7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15109 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 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png b/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png deleted file mode 100644 index 17ac682340e02bf53e85a7101cce4dc43f299b35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^96-#;!3HGxgLCzORG_DeV@SoV)(abX8w_|D4hC#y zZcdT7mN?gAo@#bWn}{rl0G+92289?P_yFV mI+GcM(vHfQ^{WzxHrCu@fjdmuxx$(dHlQtL6RZPlZ0b0o5>FVdQ I&MBb@08?%}rvLx| diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png deleted file mode 100644 index c75ef514746d6540cf0d194e0e33f9b3dbf7cdc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OG6Q@t<7|2UN4KM)l%yh>pB zlECmOk>P(T!^Z@MA4v?ql0j@B14#Z)V|W|S@HUR&dlJJJpqey>4+$XluM`mXb0Wjv zREB3U46kBAYJgHe5r`~M9H{tI%V&@aq)LMPf*CkDIVB{lta^c9{f2{w4j(>o`ufeA z_y7J)h_yZiR2Amw;uunK%lDMKP=f-4^Tmh_-@BAL?);yS7Nb!A)kt`Ug2j(N9=pED zZ=HSed2yYLW#<0NY1$LaX9Z6AF4b%@PbDjDR*Uc)I$ztaD0e0sv0+dDH*^ diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png deleted file mode 100644 index bea88dec076ede1dc0129ccffdfc28423e76c738..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1O*aCb)T>t<7zj^a!{-~ZRpa@e* zkY6x^1A~9h3LsC$)5S5Q;?~jQhP*)Dp$%vEXWBI}{}N>nTHnLK$HJh?`9=9AP=>+N L)z4*}Q$iB}#T_8a diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png deleted file mode 100644 index 1d6afaf116119d481fb7397e4d95f64844dc50b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c1!3HEbvi3>O=$B>F!ThA>N1&XxF!ThA>N1d25{m=~vu q9a(ktvcpdohMTt=tg0HjFEhE?O^WsetId("depth"); + depth->setName(QObject::tr("Depth")); + depth->setDescription("Controls the intensity of the effect."); + depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + depth->setMinimum(0.0); + depth->setMaximum(1.0); + depth->setDefault(1.0); + EffectManifestParameter* strength = manifest.addParameter(); strength->setId("strength"); strength->setName(QObject::tr("Strength")); @@ -32,7 +43,7 @@ EffectManifest PanEffect::getManifest() { strength->setMinimum(0.0); strength->setMaximum(0.5); // strength->setDefault(0.25); - strength->setDefault(0.5); + strength->setDefault(0.0); EffectManifestParameter* period = manifest.addParameter(); period->setId("period"); @@ -46,31 +57,6 @@ EffectManifest PanEffect::getManifest() { // period->setDefault(250.0); period->setDefault(50.0); - EffectManifestParameter* ramping = manifest.addParameter(); - ramping->setId("ramping_treshold"); - ramping->setName(QObject::tr("Ramping")); - ramping->setDescription(""); - ramping->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); - ramping->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); - ramping->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - ramping->setMinimum(0.000000000001f); - ramping->setMaximum(0.015978f); - // ramping->setDefault(250.0); - ramping->setDefault(0.003f); - // 0.00387852 - // 0.015978 - - EffectManifestParameter* depth = manifest.addParameter(); - depth->setId("depth"); - depth->setName(QObject::tr("Depth")); - depth->setDescription("Controls the intensity of the effect."); - depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); - depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); - depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - depth->setMinimum(0.0); - depth->setMaximum(1.0); - depth->setDefault(1.0); - return manifest; } @@ -78,16 +64,19 @@ PanEffect::PanEffect(EngineEffect* pEffect, const EffectManifest& manifest) : m_pDepthParameter(pEffect->getParameterById("depth")), m_pStrengthParameter(pEffect->getParameterById("strength")), - m_pPeriodParameter(pEffect->getParameterById("period")), - m_pRampingParameter(pEffect->getParameterById("ramping_treshold")) + m_pPeriodParameter(pEffect->getParameterById("period")) { - oldFrac = -1.0f; Q_UNUSED(manifest); } PanEffect::~PanEffect() { + //qDebug() << debugString() << "destroyed"; } +// todo : ramping signal carré +// todo : + + void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, const CSAMPLE* pInput, CSAMPLE* pOutput, const unsigned int numSamples, @@ -105,31 +94,41 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, return; // DISABLED = 0x00 } - CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()) * (float)numSamples / 2.0f; + CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()) * numSamples / 2; CSAMPLE stepFrac = m_pStrengthParameter->value(); CSAMPLE depth = m_pDepthParameter->value(); - float rampingTreshold = m_pRampingParameter->value(); if (gs.time > lfoPeriod || 0x03 == enableState) { // ENABLING = 0x03 - gs.time = 0; + gs.time = -1; } // coef of the slope // a = (y2 - y1) / (x2 - x1) // 1 / ( 1 - 2 * stepfrac) - float a; - if( stepFrac != 0.5f ){ - a = 1.0f / (1.0f - stepFrac * 2.0f); - } else { - a = 0.0001f; - } + float a = 1.0f / (1.0f - stepFrac * 2.0f); // define the increasing of gain when only one output channel is used float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; + // merging of tests for // size of a segment of slope float u = ( 0.5f - stepFrac ) / 2.0f; + /* + qDebug() << "stepFrac" << stepFrac + << "| position :" << (roundf(position * 100.0f) / 100.0f) + << "| time :" << gs.time + << "| period :" << lfoPeriod + // << "| a :" << a // coef of slope between 1 and -1 + // << "| q :" << quarter // current quarter in the trigo circle + // << "| q+ :" << floorf((quarter+1.0f)/2.0f) + << "| vol_coef :" << frac + << "| pos_coef :" << frac + // << "| lawCoef :" << lawCoef + << "| enableState :" << enableState + << "| numSamples :" << numSamples; + */ + // todo (jclaveau) : stereo SampleUtil::mixStereoToMono(pOutput, pInput, numSamples); @@ -137,19 +136,15 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // math below should result in a simple copy of delay buf to pOutput. // todo (jclaveau) : ramping if period changes // todo (jclaveau) : ramping if curve is square - - CSAMPLE maxDiff = 0.0f; - CSAMPLE maxDiff2 = 0.0f; - CSAMPLE frac; for (unsigned int i = 0; i + 1 < numSamples; i += 2) { + gs.time++; CSAMPLE periodFraction = CSAMPLE(gs.time) / lfoPeriod; // current quarter in the trigonometric circle float quarter = floorf(periodFraction * 4.0f); - // part of the period fraction being steps (not in the slope) - CSAMPLE stepsFractionPart = floorf((quarter+1.0f)/2.0f) * stepFrac; + CSAMPLE stepFracCoef = floorf((quarter+1.0f)/2.0f) * stepFrac; // float inInterval = fmod( periodFraction, (lfoPeriod / 2.0) ); float inInterval = fmod( periodFraction, 0.5f ); @@ -160,63 +155,20 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, position = quarter < 2.0f ? 0.25f : 0.75f; } else { // in the slope (linear function) - position = (periodFraction - stepsFractionPart) * a; + position = (periodFraction - stepFracCoef) * a; } // set the curve between 0 and 1 - frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; - - // - if (oldFrac != -1.0f) { - float diff = frac - oldFrac; - - if( fabs(diff) >= rampingTreshold ){ - frac = oldFrac + (diff / fabs(diff) * rampingTreshold); - } - - if( fabs(diff) > maxDiff ){ - maxDiff = fabs(diff); - } - } - - oldFrac = frac; + CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; pOutput[i] = pOutput[i] * (1 - depth) + pOutput[i] * frac * lawCoef * depth; - pOutput[i+1] = pOutput[i+1] * (1 - depth) + pOutput[i+1] * (1.0f - frac) * lawCoef * depth; - - gs.time++; } - /** / - qDebug() << "stepFrac" << stepFrac - << "| time :" << gs.time - // << "| period :" << lfoPeriod - << "| a :" << a // coef of slope between 1 and -1 - // << "| lawCoef :" << lawCoef - // << "| enableState :" << enableState - // << "| numSamples :" << numSamples - ; - - /**/ - - - qDebug() - // < < "| position :" << (roundf(position * 100.0f) / 100.0f) - << "| frac :" << frac - // << "| maxDiff :" << maxDiff - // << "| maxDiff2 :" << maxDiff2 - << "| time :" << gs.time - << "| rampingTreshold :" << rampingTreshold - ; - // << "| a :" << a // coef of slope between 1 and -1 - // << "| numSamples :" << numSamples; - - /**/ // qDebug() << "pOutput[1]" << pOutput[1] << "| pOutput[2]" << pOutput[2]; } diff --git a/src/effects/native/paneffect.h b/src/effects/native/paneffect.h index 38468269c2f..bb675add747 100644 --- a/src/effects/native/paneffect.h +++ b/src/effects/native/paneffect.h @@ -13,10 +13,21 @@ struct PanGroupState { PanGroupState() { + delay_buf = SampleUtil::alloc(MAX_BUFFER_LEN); + SampleUtil::applyGain(delay_buf, 0, MAX_BUFFER_LEN); + prev_delay_time = 0.0; + prev_delay_samples = 0; + write_position = 0; + time = 0; } ~PanGroupState() { + SampleUtil::free(delay_buf); } + CSAMPLE* delay_buf; + double prev_delay_time; + int prev_delay_samples; + int write_position; unsigned int time; }; @@ -38,6 +49,7 @@ class PanEffect : public GroupEffectProcessor { const GroupFeatureState& groupFeatures); private: + // int getDelaySamples(double delay_time, const unsigned int sampleRate) const; QString debugString() const { return getId(); @@ -46,10 +58,7 @@ class PanEffect : public GroupEffectProcessor { EngineEffectParameter* m_pDepthParameter; EngineEffectParameter* m_pStrengthParameter; EngineEffectParameter* m_pPeriodParameter; - EngineEffectParameter* m_pRampingParameter; - - CSAMPLE oldFrac; - + DISALLOW_COPY_AND_ASSIGN(PanEffect); }; From 37d9a824db7bef9bbeea18ce7f647ca8891dde55 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Wed, 31 Dec 2014 00:24:25 +0100 Subject: [PATCH 010/481] ramping + cleaning --- src/effects/native/paneffect.cpp | 129 +++++++++++++++++++++---------- src/effects/native/paneffect.h | 17 +--- 2 files changed, 92 insertions(+), 54 deletions(-) diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp index bc6618eb015..8a461edd724 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/paneffect.cpp @@ -21,17 +21,6 @@ EffectManifest PanEffect::getManifest() { manifest.setVersion("1.0"); manifest.setDescription(QObject::tr("Simple Pan")); - EffectManifestParameter* depth = manifest.addParameter(); - depth->setId("depth"); - depth->setName(QObject::tr("Depth")); - depth->setDescription("Controls the intensity of the effect."); - depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); - depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); - depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - depth->setMinimum(0.0); - depth->setMaximum(1.0); - depth->setDefault(1.0); - EffectManifestParameter* strength = manifest.addParameter(); strength->setId("strength"); strength->setName(QObject::tr("Strength")); @@ -43,7 +32,7 @@ EffectManifest PanEffect::getManifest() { strength->setMinimum(0.0); strength->setMaximum(0.5); // strength->setDefault(0.25); - strength->setDefault(0.0); + strength->setDefault(0.5); EffectManifestParameter* period = manifest.addParameter(); period->setId("period"); @@ -57,6 +46,30 @@ EffectManifest PanEffect::getManifest() { // period->setDefault(250.0); period->setDefault(50.0); + EffectManifestParameter* ramping = manifest.addParameter(); + ramping->setId("ramping_treshold"); + ramping->setName(QObject::tr("Ramping")); + ramping->setDescription(""); + ramping->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + ramping->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + ramping->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + ramping->setMinimum(0.000000000001f); + ramping->setMaximum(0.015978f); + // ramping->setDefault(250.0); + ramping->setDefault(0.003f); + // 0.00387852 => other good value + + EffectManifestParameter* depth = manifest.addParameter(); + depth->setId("depth"); + depth->setName(QObject::tr("Depth")); + depth->setDescription("Controls the intensity of the effect."); + depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + depth->setMinimum(0.0); + depth->setMaximum(1.0); + depth->setDefault(1.0); + return manifest; } @@ -64,19 +77,16 @@ PanEffect::PanEffect(EngineEffect* pEffect, const EffectManifest& manifest) : m_pDepthParameter(pEffect->getParameterById("depth")), m_pStrengthParameter(pEffect->getParameterById("strength")), - m_pPeriodParameter(pEffect->getParameterById("period")) + m_pPeriodParameter(pEffect->getParameterById("period")), + m_pRampingParameter(pEffect->getParameterById("ramping_treshold")) { + oldFrac = -1.0f; Q_UNUSED(manifest); } PanEffect::~PanEffect() { - //qDebug() << debugString() << "destroyed"; } -// todo : ramping signal carré -// todo : - - void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, const CSAMPLE* pInput, CSAMPLE* pOutput, const unsigned int numSamples, @@ -94,41 +104,31 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, return; // DISABLED = 0x00 } - CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()) * numSamples / 2; + CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()) * (float)numSamples / 2.0f; CSAMPLE stepFrac = m_pStrengthParameter->value(); CSAMPLE depth = m_pDepthParameter->value(); + float rampingTreshold = m_pRampingParameter->value(); if (gs.time > lfoPeriod || 0x03 == enableState) { // ENABLING = 0x03 - gs.time = -1; + gs.time = 0; } // coef of the slope // a = (y2 - y1) / (x2 - x1) // 1 / ( 1 - 2 * stepfrac) - float a = 1.0f / (1.0f - stepFrac * 2.0f); + float a; + if( stepFrac != 0.5f ){ + a = 1.0f / (1.0f - stepFrac * 2.0f); + } else { + a = 0.0001f; + } // define the increasing of gain when only one output channel is used float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; - // merging of tests for // size of a segment of slope float u = ( 0.5f - stepFrac ) / 2.0f; - /* - qDebug() << "stepFrac" << stepFrac - << "| position :" << (roundf(position * 100.0f) / 100.0f) - << "| time :" << gs.time - << "| period :" << lfoPeriod - // << "| a :" << a // coef of slope between 1 and -1 - // << "| q :" << quarter // current quarter in the trigo circle - // << "| q+ :" << floorf((quarter+1.0f)/2.0f) - << "| vol_coef :" << frac - << "| pos_coef :" << frac - // << "| lawCoef :" << lawCoef - << "| enableState :" << enableState - << "| numSamples :" << numSamples; - */ - // todo (jclaveau) : stereo SampleUtil::mixStereoToMono(pOutput, pInput, numSamples); @@ -136,15 +136,19 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // math below should result in a simple copy of delay buf to pOutput. // todo (jclaveau) : ramping if period changes // todo (jclaveau) : ramping if curve is square + + CSAMPLE maxDiff = 0.0f; + CSAMPLE maxDiff2 = 0.0f; + CSAMPLE frac; for (unsigned int i = 0; i + 1 < numSamples; i += 2) { - gs.time++; CSAMPLE periodFraction = CSAMPLE(gs.time) / lfoPeriod; // current quarter in the trigonometric circle float quarter = floorf(periodFraction * 4.0f); - CSAMPLE stepFracCoef = floorf((quarter+1.0f)/2.0f) * stepFrac; + // part of the period fraction being steps (not in the slope) + CSAMPLE stepsFractionPart = floorf((quarter+1.0f)/2.0f) * stepFrac; // float inInterval = fmod( periodFraction, (lfoPeriod / 2.0) ); float inInterval = fmod( periodFraction, 0.5f ); @@ -155,20 +159,63 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, position = quarter < 2.0f ? 0.25f : 0.75f; } else { // in the slope (linear function) - position = (periodFraction - stepFracCoef) * a; + position = (periodFraction - stepsFractionPart) * a; } // set the curve between 0 and 1 - CSAMPLE frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; + frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; + + // + if (oldFrac != -1.0f) { + float diff = frac - oldFrac; + + if( fabs(diff) >= rampingTreshold ){ + frac = oldFrac + (diff / fabs(diff) * rampingTreshold); + } + + if( fabs(diff) > maxDiff ){ + maxDiff = fabs(diff); + } + } + + oldFrac = frac; pOutput[i] = pOutput[i] * (1 - depth) + pOutput[i] * frac * lawCoef * depth; + pOutput[i+1] = pOutput[i+1] * (1 - depth) + pOutput[i+1] * (1.0f - frac) * lawCoef * depth; + + gs.time++; } + /** / + qDebug() << "stepFrac" << stepFrac + << "| time :" << gs.time + // << "| period :" << lfoPeriod + << "| a :" << a // coef of slope between 1 and -1 + // << "| lawCoef :" << lawCoef + // << "| enableState :" << enableState + // << "| numSamples :" << numSamples + ; + + /**/ + + + qDebug() + // < < "| position :" << (roundf(position * 100.0f) / 100.0f) + << "| frac :" << frac + // << "| maxDiff :" << maxDiff + // << "| maxDiff2 :" << maxDiff2 + << "| time :" << gs.time + << "| rampingTreshold :" << rampingTreshold + ; + // << "| a :" << a // coef of slope between 1 and -1 + // << "| numSamples :" << numSamples; + + /**/ // qDebug() << "pOutput[1]" << pOutput[1] << "| pOutput[2]" << pOutput[2]; } diff --git a/src/effects/native/paneffect.h b/src/effects/native/paneffect.h index bb675add747..38468269c2f 100644 --- a/src/effects/native/paneffect.h +++ b/src/effects/native/paneffect.h @@ -13,21 +13,10 @@ struct PanGroupState { PanGroupState() { - delay_buf = SampleUtil::alloc(MAX_BUFFER_LEN); - SampleUtil::applyGain(delay_buf, 0, MAX_BUFFER_LEN); - prev_delay_time = 0.0; - prev_delay_samples = 0; - write_position = 0; - time = 0; } ~PanGroupState() { - SampleUtil::free(delay_buf); } - CSAMPLE* delay_buf; - double prev_delay_time; - int prev_delay_samples; - int write_position; unsigned int time; }; @@ -49,7 +38,6 @@ class PanEffect : public GroupEffectProcessor { const GroupFeatureState& groupFeatures); private: - // int getDelaySamples(double delay_time, const unsigned int sampleRate) const; QString debugString() const { return getId(); @@ -58,7 +46,10 @@ class PanEffect : public GroupEffectProcessor { EngineEffectParameter* m_pDepthParameter; EngineEffectParameter* m_pStrengthParameter; EngineEffectParameter* m_pPeriodParameter; - + EngineEffectParameter* m_pRampingParameter; + + CSAMPLE oldFrac; + DISALLOW_COPY_AND_ASSIGN(PanEffect); }; From 291e7dffe6fa5f846b70d1a64ab44b9dafe56207 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 24 Nov 2014 19:11:06 +0100 Subject: [PATCH 011/481] Delete obsolete include directives --- src/basetrackplayer.cpp | 1 - src/effects/effectchain.cpp | 1 - src/effects/effectchainslot.cpp | 1 - src/effects/native/bessel4lvmixeqeffect.h | 1 - src/engine/effects/engineeffectrack.cpp | 1 - src/engine/enginesidechaincompressor.cpp | 1 - src/library/browse/browsetablemodel.cpp | 1 - src/library/coverartcache.cpp | 1 - src/test/coverartutils_test.cpp | 1 - src/vinylcontrol/vinylcontrolxwax.cpp | 1 - 10 files changed, 10 deletions(-) diff --git a/src/basetrackplayer.cpp b/src/basetrackplayer.cpp index b23f60bbf4d..56837041b0e 100644 --- a/src/basetrackplayer.cpp +++ b/src/basetrackplayer.cpp @@ -9,7 +9,6 @@ #include "engine/enginebuffer.h" #include "engine/enginedeck.h" #include "engine/enginemaster.h" -#include "soundsourceproxy.h" #include "track/beatgrid.h" #include "waveform/renderers/waveformwidgetrenderer.h" #include "analyserqueue.h" diff --git a/src/effects/effectchain.cpp b/src/effects/effectchain.cpp index 9b310a6ed3f..3a4614e55fe 100644 --- a/src/effects/effectchain.cpp +++ b/src/effects/effectchain.cpp @@ -4,7 +4,6 @@ #include "engine/effects/message.h" #include "engine/effects/engineeffectrack.h" #include "engine/effects/engineeffectchain.h" -#include "sampleutil.h" #include "xmlparse.h" EffectChain::EffectChain(EffectsManager* pEffectsManager, const QString& id, diff --git a/src/effects/effectchainslot.cpp b/src/effects/effectchainslot.cpp index 66f2f02a42c..5bae506bea5 100644 --- a/src/effects/effectchainslot.cpp +++ b/src/effects/effectchainslot.cpp @@ -1,7 +1,6 @@ #include "effects/effectchainslot.h" #include "effects/effectrack.h" -#include "sampleutil.h" #include "controlpotmeter.h" #include "controlpushbutton.h" #include "util/math.h" diff --git a/src/effects/native/bessel4lvmixeqeffect.h b/src/effects/native/bessel4lvmixeqeffect.h index a23562008c0..931a6b966e9 100644 --- a/src/effects/native/bessel4lvmixeqeffect.h +++ b/src/effects/native/bessel4lvmixeqeffect.h @@ -13,7 +13,6 @@ #include "util.h" #include "util/types.h" #include "util/defs.h" -#include "sampleutil.h" #include "lvmixeqbase.h" class Bessel4LVMixEQEffectGroupState : diff --git a/src/engine/effects/engineeffectrack.cpp b/src/engine/effects/engineeffectrack.cpp index cdc666685f9..2fb6466fa17 100644 --- a/src/engine/effects/engineeffectrack.cpp +++ b/src/engine/effects/engineeffectrack.cpp @@ -1,7 +1,6 @@ #include "engine/effects/engineeffectrack.h" #include "engine/effects/engineeffectchain.h" -#include "sampleutil.h" EngineEffectRack::EngineEffectRack(int iRackNumber) : m_iRackNumber(iRackNumber) { diff --git a/src/engine/enginesidechaincompressor.cpp b/src/engine/enginesidechaincompressor.cpp index 432fa5ed6ba..4f9b6fc634e 100644 --- a/src/engine/enginesidechaincompressor.cpp +++ b/src/engine/enginesidechaincompressor.cpp @@ -1,7 +1,6 @@ #include #include "engine/enginesidechaincompressor.h" -#include "sampleutil.h" EngineSideChainCompressor::EngineSideChainCompressor(const char* group) : m_compressRatio(0.0), diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp index ff713cb92c5..6d02481364e 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/browse/browsetablemodel.cpp @@ -7,7 +7,6 @@ #include "library/browse/browsetablemodel.h" #include "library/browse/browsethread.h" -#include "soundsourceproxy.h" #include "playerinfo.h" #include "controlobject.h" #include "library/dao/trackdao.h" diff --git a/src/library/coverartcache.cpp b/src/library/coverartcache.cpp index 098b567a364..4083d3a8a7f 100644 --- a/src/library/coverartcache.cpp +++ b/src/library/coverartcache.cpp @@ -6,7 +6,6 @@ #include "library/coverartcache.h" #include "library/coverartutils.h" -#include "soundsourceproxy.h" // Large cover art wastes space in our cache when we typicaly won't show them at // their full size. If no width is specified, this is the maximum width cap. diff --git a/src/test/coverartutils_test.cpp b/src/test/coverartutils_test.cpp index 57e04d7bd54..19cf1db71d1 100644 --- a/src/test/coverartutils_test.cpp +++ b/src/test/coverartutils_test.cpp @@ -6,7 +6,6 @@ #include "library/coverartutils.h" #include "library/trackcollection.h" #include "test/mixxxtest.h" -#include "soundsourceproxy.h" // first inherit from MixxxTest to construct a QApplication to be able to // construct the default QPixmap in CoverArtCache diff --git a/src/vinylcontrol/vinylcontrolxwax.cpp b/src/vinylcontrol/vinylcontrolxwax.cpp index 17429b2b4b0..0a736c483eb 100644 --- a/src/vinylcontrol/vinylcontrolxwax.cpp +++ b/src/vinylcontrol/vinylcontrolxwax.cpp @@ -27,7 +27,6 @@ #include "controlobjectthread.h" #include "controlobjectslave.h" #include "controlobject.h" -#include "sampleutil.h" #include "util/math.h" #include "util/defs.h" From bc9defa8c146f12bbd80383b7ba9681bd8127abb Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 1 Dec 2014 09:23:33 +0100 Subject: [PATCH 012/481] New SoundSource/AudioSource API This is the first step towards a better/cleaner SoundSource API. The operations for reading audio data have been moved into the new abstract base class AudioSource. Currently SoundSource inherits from AudioSource for technical reasons. This is too much much coupling that needs to be broken up. I have already some ideas on how to resolve it, but first this work needs to be integrated. During the refactoring I've noticed, that the quality and maturity of the audio decoders for different file types greatly varies: Good/mature: - FLAC - Ogg - Opus - WavPack - libsndfile Mediocre: - MP3 Ugly/inefficient: - M4A - ModPlug Untested AudioSource API: - Apple CoreAudio - Windows Media Foundation Unfinished AudioSource API (just a hack): - FFmpeg Especially the code for the M4A decoder should be revised and cleaned up as soon as possible, followed by a review of the MP3 decoder. Reading audio data must work as stable and efficient as possible. This has priority over the improvement of the internal software architecture! --- build/depends.py | 1 + plugins/soundsourcem4a/SConscript | 1 + plugins/soundsourcem4a/m4a/mp4-mixxx.cpp | 213 ++---- plugins/soundsourcem4a/soundsourcem4a.cpp | 130 ++-- plugins/soundsourcem4a/soundsourcem4a.h | 41 +- plugins/soundsourcemediafoundation/SConscript | 2 +- .../soundsourcemediafoundation.cpp | 337 +++++----- .../soundsourcemediafoundation.h | 54 +- plugins/soundsourcewv/SConscript | 1 + plugins/soundsourcewv/soundsourcewv.cpp | 180 ++--- plugins/soundsourcewv/soundsourcewv.h | 73 +- src/analyserqueue.cpp | 66 +- src/analyserqueue.h | 7 +- src/audiosource.cpp | 65 ++ src/audiosource.h | 149 +++++ src/cachingreader.cpp | 145 ++-- src/cachingreader.h | 10 +- src/cachingreaderworker.cpp | 76 +-- src/cachingreaderworker.h | 38 +- src/engine/enginebufferscalest.cpp | 1 - src/musicbrainz/chromaprinter.cpp | 155 +++-- src/musicbrainz/chromaprinter.h | 11 +- src/soundsource.cpp | 117 ++-- src/soundsource.h | 115 ++-- src/soundsourcecoreaudio.cpp | 127 ++-- src/soundsourcecoreaudio.h | 24 +- src/soundsourceffmpeg.cpp | 110 ++-- src/soundsourceffmpeg.h | 33 +- src/soundsourceflac.cpp | 333 +++++----- src/soundsourceflac.h | 65 +- src/soundsourcemodplug.cpp | 203 +++--- src/soundsourcemodplug.h | 61 +- src/soundsourcemp3.cpp | 623 +++++++----------- src/soundsourcemp3.h | 53 +- src/soundsourceoggvorbis.cpp | 283 +++----- src/soundsourceoggvorbis.h | 54 +- src/soundsourceopus.cpp | 260 +++----- src/soundsourceopus.h | 46 +- src/soundsourceproxy.cpp | 8 +- src/soundsourcesndfile.cpp | 191 ++---- src/soundsourcesndfile.h | 48 +- src/soundsourcetaglib.cpp | 10 +- src/trackinfoobject.cpp | 11 +- 43 files changed, 2058 insertions(+), 2473 deletions(-) create mode 100644 src/audiosource.cpp create mode 100644 src/audiosource.h diff --git a/build/depends.py b/build/depends.py index aa22c25dd93..f1d2032d6c4 100644 --- a/build/depends.py +++ b/build/depends.py @@ -656,6 +656,7 @@ def sources(self, build): "errordialoghandler.cpp", "upgrade.cpp", + "audiosource.cpp", "soundsource.cpp", "soundsourcetaglib.cpp", diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index 96df0470988..515fa1d7b5a 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -14,6 +14,7 @@ m4a_sources = [ "soundsourcem4a.cpp", # MP4/M4A Support through FAAD/libmp4v2 "soundsourcetaglib.cpp", # TagLib dependencies "soundsource.cpp", # required to subclass SoundSource + "audiosource.cpp", # required to subclass AudioSource "sampleutil.cpp", # utility functions ] diff --git a/plugins/soundsourcem4a/m4a/mp4-mixxx.cpp b/plugins/soundsourcem4a/m4a/mp4-mixxx.cpp index 0ab967af34b..9d5882eabec 100644 --- a/plugins/soundsourcem4a/m4a/mp4-mixxx.cpp +++ b/plugins/soundsourcem4a/m4a/mp4-mixxx.cpp @@ -6,11 +6,21 @@ // // g++ $(pkg-config --cflags QtCore) $(pkg-config --libs-only-l QtCore) -lmp4v2 -lfaad -o mp4-mixxx mp4-mixxx.cpp // -#include -#include +#include "util/types.h" -#include "util/math.h" -#include "util/assert.h" +#include + +namespace +{ + // AAC: "... each block decodes to 1024 time-domain samples." + std::size_t kFramesPerBlock = 1024; + + // 32-bit float + std::size_t kBytesPerSample = sizeof(CSAMPLE); + + // Only mono or stereo channels are supported + std::size_t kMaxChannels = 2; +} /* * Copyright 2006 dnk @@ -150,7 +160,7 @@ static int mp4_open(struct input_plugin_data *ip_data) priv->overflow_buf_len = 0; priv->overflow_buf = NULL; - priv->sample_buf_len = 4096; + priv->sample_buf_len = kFramesPerBlock * kBytesPerSample * kMaxChannels; priv->sample_buf = new char[priv->sample_buf_len]; priv->sample_buf_frame = -1; @@ -159,7 +169,7 @@ static int mp4_open(struct input_plugin_data *ip_data) priv->decoder = faacDecOpen(); /* set decoder config */ neaac_cfg = faacDecGetCurrentConfiguration(priv->decoder); - neaac_cfg->outputFormat = FAAD_FMT_16BIT; /* force 16 bit audio */ + neaac_cfg->outputFormat = FAAD_FMT_FLOAT; /* force 32-bit float */ neaac_cfg->downMatrix = 1; /* 5.1 -> stereo */ neaac_cfg->defObjectType = LC; //qDebug() << "Decoder Config" << neaac_cfg->defObjectType @@ -232,10 +242,12 @@ static int mp4_open(struct input_plugin_data *ip_data) return -IP_ERROR_FILE_FORMAT; } -static int mp4_close(struct input_plugin_data *ip_data) -{ - struct mp4_private *priv; +static int mp4_close(struct input_plugin_data *ip_data) { + if ((0 == ip_data) || (0 == ip_data->private_ipd)) { + return 0; // nothing to do + } + struct mp4_private *priv; priv = (mp4_private*) ip_data->private_ipd; if (priv->mp4.handle) @@ -255,6 +267,8 @@ static int mp4_close(struct input_plugin_data *ip_data) delete priv; ip_data->private_ipd = NULL; + mp4_init(ip_data); + return 0; } @@ -336,8 +350,7 @@ static int decode_one_frame(struct input_plugin_data *ip_data, void *buffer, int priv->sample_buf_frame = this_frame; priv->mp4.sample++; - /* 16-bit samples */ - bytes = frame_info.samples * 2; + bytes = frame_info.samples * kBytesPerSample; if (bytes > count) { /* decoded too much; keep overflow. */ @@ -383,53 +396,60 @@ static int mp4_read(struct input_plugin_data *ip_data, char *buffer, int count) return rc; } -static int mp4_total_samples(struct input_plugin_data *ip_data) { +static int mp4_channels(struct input_plugin_data *ip_data) { + struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; + return priv->channels; +} + +static int mp4_sample_rate(struct input_plugin_data *ip_data) { + struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; + return priv->sample_rate; +} + +static int mp4_total_frames(struct input_plugin_data *ip_data) { struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; - return priv->channels * priv->mp4.num_samples * 1024; + return priv->mp4.num_samples * kFramesPerBlock; } static int mp4_current_sample(struct input_plugin_data *ip_data) { struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; - int frame_length = priv->channels * 1024; if (priv->overflow_buf_len == 0) { - return priv->mp4.sample * frame_length - priv->overflow_buf_len; + return priv->mp4.sample; + } else { + // -1 because if overflow buf is filled then mp4.sample is incremented, and + // the samples in the overflow buf are for sample - 1 + int samples_per_block = kFramesPerBlock * priv->channels; + Q_ASSERT(0 == (priv->overflow_buf_len % kBytesPerSample)); + int overflow_samples = samples_per_block - (priv->overflow_buf_len / kBytesPerSample); + return (priv->mp4.sample - 1) + overflow_samples; } - // rryan 9/2009 This is equivalent to the current sample. The full expression - // is (priv->mp4.sample - 1) * frame_length + (frame_length - - // priv->overflow_buf_len); but the frame_length lone terms drop out. +} - // -1 because if overflow buf is filled then mp4.sample is incremented, and - // the samples in the overflow buf are for sample - 1 - return (priv->mp4.sample - 1) * frame_length - priv->overflow_buf_len; +static int mp4_current_frame(struct input_plugin_data *ip_data) { + struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; + int current_sample = mp4_current_sample(ip_data); + return current_sample / priv->channels; } -static int mp4_seek_sample(struct input_plugin_data *ip_data, int sample) +static int mp4_seek_frame(struct input_plugin_data *ip_data, int frame) { struct mp4_private *priv; priv = (mp4_private*) ip_data->private_ipd; - // Ignore the seek if the sample is invalid. - DEBUG_ASSERT_AND_HANDLE(sample >= 0) { - return mp4_current_sample(ip_data); - } - // The first frame is samples 0 through 2047. The first sample of the second - // frame is 2048. 2048 / 2048 = 1, so frame_for_sample will be 2 on the - // 2048'th sample. The frame_offset_samples is how many samples into the frame - // the sample'th sample is. For x in (0,2047), the frame offset is x. For x in - // (2048,4095) the offset is x-2048 and so on. sample % 2048 is therefore - // suitable for calculating the offset. - unsigned int frame_for_sample = 1 + (sample / (2 * 1024)); - unsigned int frame_offset_samples = sample % (2 * 1024); - unsigned int frame_offset_bytes = frame_offset_samples * 2; - - //qDebug() << "Seeking to" << frame_for_sample << ":" << frame_offset; + Q_ASSERT(frame >= 0); + unsigned int block_for_frame = 1 + (frame / kFramesPerBlock); + unsigned int block_offset_frames = frame % kFramesPerBlock; + unsigned int block_offset_samples = block_offset_frames * priv->channels; + unsigned int frame_offset_bytes = block_offset_samples * kBytesPerSample; + + //qDebug() << "Seeking to" << block_for_frame << ":" << frame_offset; // Invalid sample requested -- return the current position. - if (frame_for_sample < 1 || frame_for_sample > priv->mp4.num_samples) - return mp4_current_sample(ip_data); + if (block_for_frame < 1 || block_for_frame > priv->mp4.num_samples) + return mp4_current_frame(ip_data); // We don't have the current frame decoded -- decode it. - if (priv->sample_buf_frame != frame_for_sample) { + if (priv->sample_buf_frame != block_for_frame) { // We might have to 'prime the pump' if this isn't the first frame. The // decoder has internal state that it builds as it plays, and just seeking @@ -438,7 +458,7 @@ static int mp4_seek_sample(struct input_plugin_data *ip_data, int sample) // artifacts. Figure out how many frames we need to go backward -- 1 seems // to work. const unsigned int how_many_backwards = 1; - int start_frame = math_max(frame_for_sample - how_many_backwards, 1U); + int start_frame = math_max(block_for_frame - how_many_backwards, 1U); priv->mp4.sample = start_frame; // rryan 9/2009 -- the documentation is sketchy on this, but I think that @@ -453,10 +473,10 @@ static int mp4_seek_sample(struct input_plugin_data *ip_data, int sample) do { result = decode_one_frame(ip_data, 0, 0); if (result < 0) qDebug() << "SEEK_ERROR"; - } while (result == -2 || priv->mp4.sample <= frame_for_sample); + } while (result == -2 || priv->mp4.sample <= block_for_frame); if (result == -1 || result == 0) { - return mp4_current_sample(ip_data); + return mp4_current_frame(ip_data); } } else { qDebug() << "Seek within frame"; @@ -469,101 +489,9 @@ static int mp4_seek_sample(struct input_plugin_data *ip_data, int sample) priv->overflow_buf += frame_offset_bytes; priv->overflow_buf_len -= frame_offset_bytes; - return mp4_current_sample(ip_data); + return mp4_current_frame(ip_data); } -static int mp4_seek(struct input_plugin_data *ip_data, double offset) -{ - struct mp4_private *priv; - MP4SampleId sample; - uint32_t scale; - - priv = (mp4_private*) ip_data->private_ipd; - - scale = MP4GetTrackTimeScale(priv->mp4.handle, priv->mp4.track); - if (scale == 0) - return -IP_ERROR_INTERNAL; - - sample = MP4GetSampleIdFromTime(priv->mp4.handle, priv->mp4.track, - (MP4Timestamp)(offset * (double)scale), 0); - if (sample == MP4_INVALID_SAMPLE_ID) - return -IP_ERROR_INTERNAL; - - qDebug() << "seeking from sample" << priv->mp4.sample << "to sample" << sample; - priv->mp4.sample = sample; - priv->overflow_buf_len = 0; - - return priv->mp4.sample; -} - -/* commented because we use TagLib now ??? -- bkgood -static int mp4_read_comments(struct input_plugin_data *ip_data, - struct keyval **comments) -{ - struct mp4_private *priv; - uint16_t meta_num, meta_total; - uint8_t val; - uint8_t *ustr; - uint32_t size; - char *str; - GROWING_KEYVALS(c); - - priv = ip_data->private; - - MP4GetMetadata* provides malloced pointers, and the data - * is in UTF-8 (or at least it should be). - if (MP4GetMetadataArtist(priv->mp4.handle, &str)) - comments_add(&c, "artist", str); - if (MP4GetMetadataAlbum(priv->mp4.handle, &str)) - comments_add(&c, "album", str); - if (MP4GetMetadataName(priv->mp4.handle, &str)) - comments_add(&c, "title", str); - if (MP4GetMetadataGenre(priv->mp4.handle, &str)) - comments_add(&c, "genre", str); - if (MP4GetMetadataYear(priv->mp4.handle, &str)) - comments_add(&c, "date", str); - - if (MP4GetMetadataCompilation(priv->mp4.handle, &val)) - comments_add_const(&c, "compilation", val ? "yes" : "no"); -#if 0 - if (MP4GetBytesProperty(priv->mp4.handle, "moov.udta.meta.ilst.aART.data", &ustr, &size)) { - char *xstr; - - What's this? - * This is the result from lack of documentation. - * It's supposed to return just a string, but it - * returns an additional 16 bytes of junk at the - * beginning. Could be a bug. Could be intentional. - * Hopefully this works around it: - - if (ustr[0] == 0 && size > 16) { - ustr += 16; - size -= 16; - } - xstr = xmalloc(size + 1); - memcpy(xstr, ustr, size); - xstr[size] = 0; - comments_add(&c, "albumartist", xstr); - free(xstr); - } -#endif - if (MP4GetMetadataTrack(priv->mp4.handle, &meta_num, &meta_total)) { - char buf[6]; - snprintf(buf, 6, "%u", meta_num); - comments_add_const(&c, "tracknumber", buf); - } - if (MP4GetMetadataDisk(priv->mp4.handle, &meta_num, &meta_total)) { - char buf[6]; - snprintf(buf, 6, "%u", meta_num); - comments_add_const(&c, "discnumber", buf); - } - - comments_terminate(&c); - *comments = c.comments; - return 0; -} -*/ - static int mp4_duration(struct input_plugin_data *ip_data) { struct mp4_private *priv; @@ -580,16 +508,3 @@ static int mp4_duration(struct input_plugin_data *ip_data) return duration / scale; } -/* -const struct input_plugin_ops ip_ops = { - .open = mp4_open, - .close = mp4_close, - .read = mp4_read, - .seek = mp4_seek, - .read_comments = mp4_read_comments, - .duration = mp4_duration -}; - -const char * const ip_extensions[] = { "mp4", "m4a", "m4b", NULL }; -const char * const ip_mime_types[] = { "audio/mp4", "audio/mp4a-latm", NULL }; -*/ diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 8932bacfefd..e6da7f104f5 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -16,10 +16,9 @@ #include "soundsourcem4a.h" #include "soundsourcetaglib.h" -#include "sampleutil.h" -#include "util/assert.h" #include + #include #ifdef __MP4V2__ @@ -38,39 +37,23 @@ namespace Mixxx { SoundSourceM4A::SoundSourceM4A(QString qFileName) - : SoundSource(qFileName) { - - // Initialize variables to invalid values in case loading fails. - mp4file = MP4_INVALID_FILE_HANDLE; - filelength = 0; - setType("m4a"); + : Super(qFileName, "m4a") + , trackId(0) { mp4_init(&ipd); } SoundSourceM4A::~SoundSourceM4A() { - if (ipd.filename) { - delete [] ipd.filename; - ipd.filename = NULL; - } - - if (mp4file != MP4_INVALID_FILE_HANDLE) { - mp4_close(&ipd); - mp4file = MP4_INVALID_FILE_HANDLE; - } + delete[] ipd.filename; + mp4_close(&ipd); } Result SoundSourceM4A::open() { //Initialize the FAAD2 decoder... - initializeDecoder(); - - //qDebug() << "SSM4A: channels:" << getChannels() - // << "filelength:" << filelength - // << "Sample Rate:" << m_iSampleRate; - return OK; + return initializeDecoder(); } -int SoundSourceM4A::initializeDecoder() +Result SoundSourceM4A::initializeDecoder() { // Copy QString to char[] buffer for mp4_open to read from later const QByteArray qbaFileName(getFilename().toLocal8Bit()); @@ -91,90 +74,53 @@ int SoundSourceM4A::initializeDecoder() } // mp4_open succeeded -> populate variables - mp4_private* mp = (struct mp4_private*)ipd.private_ipd; - DEBUG_ASSERT_AND_HANDLE(mp) { - mp4_close(&ipd); + int channels = mp4_channels(&ipd); + if (2 < channels) { + qWarning() << "SSM4A::initializeDecoder failed" + << getFilename() << "unsupported number of channels" << channels; return ERR; } - mp4file = mp->mp4.handle; - filelength = mp4_total_samples(&ipd); - setSampleRate(mp->sample_rate); - setChannels(mp->channels); - - return OK; -} - -long SoundSourceM4A::seek(long filepos){ - // Abort if file did not load. - if (filelength == 0) - return 0; + setChannelCount(channels); + setFrameRate(mp4_sample_rate(&ipd)); + setFrameCount(mp4_total_frames(&ipd)); - //qDebug() << "SSM4A::seek()" << filepos; + setDuration(mp4_duration(&ipd)); - // qDebug() << "MP4SEEK: seek time:" << filepos / (getChannels() * m_iSampleRate) ; + qDebug() << "#channels" << getChannelCount() + << "frame rate" << getFrameRate() + << "#frames" << getFrameCount() + << "duration" << getDuration(); - int position = mp4_seek_sample(&ipd, filepos); - //int position = mp4_seek(&ipd, filepos / (getChannels() * m_iSampleRate)); - return position; + return OK; } -unsigned SoundSourceM4A::read(volatile unsigned long size, const SAMPLE* destination) { - // Abort if file did not load. - if (filelength == 0) - return 0; - - //qDebug() << "SSM4A::read()" << size; +AudioSource::diff_type SoundSourceM4A::seekFrame(diff_type frameIndex) { + return mp4_seek_frame(&ipd, frameIndex); +} - // We want to read a total of "size" samples, and the mp4_read() +AudioSource::size_type SoundSourceM4A::readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) { + // We want to read a total of "frameCount" frames, and the mp4_read() // function wants to know how many bytes we want to decode. One - // sample is 16-bits = 2 bytes here, so we multiply size by channels to - // get the number of bytes we want to decode. - - int total_bytes_to_decode = size * getChannels(); - int total_bytes_decoded = 0; - int num_bytes_req = 4096; - char* buffer = (char*)destination; - SAMPLE * as_buffer = (SAMPLE*) destination; //pointer for mono->stereo filling. + // sample is 32-bits = 4 bytes here. One block contains 1024 + // frames with samples for each channel. + const size_type bytes_per_block = kFramesPerBlock * getChannelCount() * sizeof(sample_type); + const size_type total_samples_to_decode = frames2samples(frameCount); + const size_type total_bytes_to_decode = total_samples_to_decode * sizeof(sample_type); + size_type total_bytes_decoded = 0; + char* byteBuffer = reinterpret_cast(sampleBuffer); do { - if (total_bytes_decoded + num_bytes_req > total_bytes_to_decode) - num_bytes_req = total_bytes_to_decode - total_bytes_decoded; - - // (char *)&destination[total_bytes_decoded/2], - int numRead = mp4_read(&ipd, - buffer, - num_bytes_req); - if(numRead <= 0) { + const size_type num_bytes_req = math_min(bytes_per_block, total_bytes_to_decode - total_bytes_decoded); + const int numRead = mp4_read(&ipd, byteBuffer, num_bytes_req); + if (numRead <= 0) { //qDebug() << "SSM4A::read: EOF"; break; } - buffer += numRead; + byteBuffer += numRead; total_bytes_decoded += numRead; } while (total_bytes_decoded < total_bytes_to_decode); - - // At this point *destination should be filled. If mono : double all samples - // (L => R) - if (getChannels() == 1) { - SampleUtil::doubleMonoToDualMono(as_buffer, total_bytes_decoded / 2); - } - - // Tell us about it only if we end up decoding a different value - // then what we expect. - - if (total_bytes_decoded % (size * 2)) { - qDebug() << "SSM4A::read : total_bytes_decoded:" - << total_bytes_decoded - << "size:" - << size; - } - - //There are two bytes in a 16-bit sample, so divide by 2. - return total_bytes_decoded / 2; -} - -inline long unsigned SoundSourceM4A::length(){ - return filelength; - //return getChannels() * mp4_duration(&ipd) * m_iSampleRate; + const size_type total_samples_decoded = total_bytes_decoded / sizeof(sample_type); + return samples2frames(total_samples_decoded); } Result SoundSourceM4A::parseHeader(){ diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index f3e592eba9b..de02d86add3 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -17,6 +17,12 @@ #ifndef SOUNDSOURCEM4A_H #define SOUNDSOURCEM4A_H +#include "soundsource.h" + +#include "m4a/ip.h" + +#include "defs_version.h" + #ifdef __MP4V2__ #include #else @@ -24,11 +30,9 @@ #endif #include + #include -#include "soundsource.h" -#include "defs_version.h" -#include "m4a/ip.h" -#include "util/defs.h" +#include #include @@ -39,25 +43,32 @@ #define MY_EXPORT #endif + namespace Mixxx { class SoundSourceM4A : public SoundSource { + typedef SoundSource Super; + public: + static QList supportedFileExtensions(); + explicit SoundSourceM4A(QString qFileName); ~SoundSourceM4A(); - Result open(); - long seek(long); - int initializeDecoder(); - unsigned read(unsigned long size, const SAMPLE*); - unsigned long length(); - Result parseHeader(); - QImage parseCoverArt(); - static QList supportedFileExtensions(); + + Result parseHeader() /*override*/; + + QImage parseCoverArt() /*override*/; + + Result open() /*override*/; + + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + private: - int trackId; - unsigned long filelength; - MP4FileHandle mp4file; + Result initializeDecoder(); + input_plugin_data ipd; + int trackId; }; extern "C" MY_EXPORT const char* getMixxxVersion() diff --git a/plugins/soundsourcemediafoundation/SConscript b/plugins/soundsourcemediafoundation/SConscript index 0fc749c542a..24af5ea17fc 100644 --- a/plugins/soundsourcemediafoundation/SConscript +++ b/plugins/soundsourcemediafoundation/SConscript @@ -18,7 +18,7 @@ if int(build.flags['mediafoundation']): else: env["LINKFLAGS"].remove("/subsystem:windows,5.01") ssmediafoundation_bin = env.SharedLibrary('soundsourcemediafoundation', - ['soundsource.cpp', 'soundsourcetaglib.cpp', 'soundsourcemediafoundation.cpp'], + ['soundsourcemediafoundation.cpp', 'soundsourcetaglib.cpp', 'soundsource.cpp', 'audiosource.cpp', 'sampleutil.cpp'], LINKCOM = [env['LINKCOM'], 'mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;1']) Return("ssmediafoundation_bin") diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 2016ca406ef..ec023090923 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -32,17 +32,47 @@ #include "soundsourcemediafoundation.h" #include "soundsourcetaglib.h" -const int kBitsPerSample = 16; const int kNumChannels = 2; const int kSampleRate = 44100; -const int kLeftoverSize = 4096; // in int16's, this seems to be the size MF AAC +const int kLeftoverSize = 4096; // in sample_type's, this seems to be the size MF AAC +const int kBitsPerSampleForBitrate = 16; // for bitrate calculation // decoder likes to give const static bool sDebug = false; +namespace { + +/** + * Convert a 100ns Media Foundation value to a number of seconds. + */ +inline qreal secondsFromMF(qint64 mf) { + return static_cast(mf) / 1e7; +} + +/** + * Convert a number of seconds to a 100ns Media Foundation value. + */ +inline qint64 mfFromSeconds(qreal sec) { + return sec * 1e7; +} + +/** + * Convert a 100ns Media Foundation value to a frame offset. + */ +inline qint64 frameFromMF(qint64 mf) { + return static_cast(mf) * kSampleRate / 1e7; +} + +/** + * Convert a frame offset to a 100ns Media Foundation value. + */ +inline qint64 mfFromFrame(qint64 frame) { + return static_cast(frame) / kSampleRate * 1e7; +} +} + /** Microsoft examples use this snippet often. */ -template static void safeRelease(T **ppT) -{ +template static void safeRelease(T **ppT) { if (*ppT) { (*ppT)->Release(); *ppT = NULL; @@ -50,35 +80,32 @@ template static void safeRelease(T **ppT) } SoundSourceMediaFoundation::SoundSourceMediaFoundation(QString filename) - : SoundSource(filename) - , m_pReader(NULL) - , m_pAudioType(NULL) - , m_wcFilename(NULL) - , m_nextFrame(0) - , m_leftoverBuffer(NULL) - , m_leftoverBufferSize(0) - , m_leftoverBufferLength(0) - , m_leftoverBufferPosition(0) - , m_mfDuration(0) - , m_iCurrentPosition(0) - , m_dead(false) - , m_seeking(false) -{ + : Super(filename, "m4a"), m_pReader(NULL), m_pAudioType(NULL), m_wcFilename( + NULL), m_nextFrame(0), m_leftoverBuffer(NULL), m_leftoverBufferSize( + 0), m_leftoverBufferLength(0), m_leftoverBufferPosition(0), m_mfDuration( + 0), m_iCurrentPosition(0), m_dead(false), m_seeking(false) { // these are always the same, might as well just stick them here // -bkgood - setType("m4a"); + // AudioSource properties + setChannelCount(kNumChannels); + setFrameRate(kSampleRate); + // SoundSource properties setChannels(kNumChannels); setSampleRate(kSampleRate); + // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for + // presentation descriptors, one of which MFSourceReader is not. + // Therefore, we calculate it ourselves, assuming 16 bits per sample + setBitrate(frames2samples(kBitsPerSampleForBitrate * kSampleRate) / 1000); + // http://social.msdn.microsoft.com/Forums/en/netfxbcl/thread/35c6a451-3507-40c8-9d1c-8d4edde7c0cc // gives maximum path + file length as 248 + 260, using that -bkgood m_wcFilename = new wchar_t[248 + 260]; } -SoundSourceMediaFoundation::~SoundSourceMediaFoundation() -{ - delete [] m_wcFilename; - delete [] m_leftoverBuffer; +SoundSourceMediaFoundation::~SoundSourceMediaFoundation() { + delete[] m_wcFilename; + delete[] m_leftoverBuffer; safeRelease(&m_pReader); safeRelease(&m_pAudioType); @@ -86,8 +113,7 @@ SoundSourceMediaFoundation::~SoundSourceMediaFoundation() CoUninitialize(); } -Result SoundSourceMediaFoundation::open() -{ +Result SoundSourceMediaFoundation::open() { if (sDebug) { qDebug() << "open()" << getFilename(); } @@ -99,7 +125,8 @@ Result SoundSourceMediaFoundation::open() HRESULT hr(S_OK); // Initialize the COM library. - hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + hr = CoInitializeEx(NULL, + COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if (FAILED(hr)) { qWarning() << "SSMF: failed to initialize COM"; return ERR; @@ -132,18 +159,19 @@ Result SoundSourceMediaFoundation::open() //Seek to position 0, which forces us to skip over all the header frames. //This makes sure we're ready to just let the Analyser rip and it'll //get the number of samples it expects (ie. no header frames). - seek(0); + seekFrame(0); return OK; } -long SoundSourceMediaFoundation::seek(long filepos) -{ - if (sDebug) { qDebug() << "seek()" << filepos; } +Mixxx::AudioSource::diff_type SoundSourceMediaFoundation::seekFrame( + diff_type frameIndex) { + if (sDebug) { + qDebug() << "seekFrame()" << frameIndex; + } PROPVARIANT prop; HRESULT hr(S_OK); - qint64 seekTarget(filepos / kNumChannels); - qint64 mfSeekTarget(mfFromFrame(seekTarget) - 1); + qint64 mfSeekTarget(mfFromFrame(frameIndex) - 1); // minus 1 here seems to make our seeking work properly, otherwise we will // (more often than not, maybe always) seek a bit too far (although not // enough for our calculatedFrameFromMF <= nextFrame assertion in ::read). @@ -157,7 +185,6 @@ long SoundSourceMediaFoundation::seek(long filepos) // this doesn't fail, see MS's implementation hr = InitPropVariantFromInt64(mfSeekTarget < 0 ? 0 : mfSeekTarget, &prop); - hr = m_pReader->Flush(MF_SOURCE_READER_FIRST_AUDIO_STREAM); if (FAILED(hr)) { qWarning() << "SSMF: failed to flush before seek"; @@ -168,41 +195,41 @@ long SoundSourceMediaFoundation::seek(long filepos) if (FAILED(hr)) { // nothing we can do here as we can't fail (no facility to other than // crashing mixxx) - qWarning() << "SSMF: failed to seek" << ( - hr == MF_E_INVALIDREQUEST ? "Sample requests still pending" : ""); + qWarning() << "SSMF: failed to seek" + << (hr == MF_E_INVALIDREQUEST ? + "Sample requests still pending" : ""); } else { - result = filepos; + result = frameIndex; } PropVariantClear(&prop); // record the next frame so that we can make sure we're there the next // time we get a buffer from MFSourceReader - m_nextFrame = seekTarget; + m_nextFrame = frameIndex; m_seeking = true; m_iCurrentPosition = result; return result; } -unsigned int SoundSourceMediaFoundation::read(unsigned long size, - const SAMPLE *destination) -{ - if (sDebug) { qDebug() << "read()" << size; } - SAMPLE *destBuffer(const_cast(destination)); - size_t framesRequested(size / kNumChannels); - size_t framesNeeded(framesRequested); +Mixxx::AudioSource::size_type SoundSourceMediaFoundation::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + if (sDebug) { + qDebug() << "read()" << frameCount; + } + size_type framesNeeded(frameCount); // first, copy frames from leftover buffer IF the leftover buffer is at // the correct frame if (m_leftoverBufferLength > 0 && m_leftoverBufferPosition == m_nextFrame) { - copyFrames(destBuffer, &framesNeeded, m_leftoverBuffer, - m_leftoverBufferLength); + copyFrames(sampleBuffer, &framesNeeded, m_leftoverBuffer, + m_leftoverBufferLength); if (m_leftoverBufferLength > 0) { if (framesNeeded != 0) { qWarning() << __FILE__ << __LINE__ - << "WARNING: Expected frames needed to be 0. Abandoning this file."; + << "WARNING: Expected frames needed to be 0. Abandoning this file."; m_dead = true; } - m_leftoverBufferPosition += framesRequested; + m_leftoverBufferPosition += frameCount; } } else { // leftoverBuffer already empty or in the wrong position, clear it @@ -216,27 +243,28 @@ unsigned int SoundSourceMediaFoundation::read(unsigned long size, IMFSample *pSample(NULL); bool error(false); // set to true to break after releasing - hr = m_pReader->ReadSample( - MF_SOURCE_READER_FIRST_AUDIO_STREAM, // [in] DWORD dwStreamIndex, - 0, // [in] DWORD dwControlFlags, - NULL, // [out] DWORD *pdwActualStreamIndex, - &dwFlags, // [out] DWORD *pdwStreamFlags, - ×tamp, // [out] LONGLONG *pllTimestamp, - &pSample); // [out] IMFSample **ppSample + hr = m_pReader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, // [in] DWORD dwStreamIndex, + 0, // [in] DWORD dwControlFlags, + NULL, // [out] DWORD *pdwActualStreamIndex, + &dwFlags, // [out] DWORD *pdwStreamFlags, + ×tamp, // [out] LONGLONG *pllTimestamp, + &pSample); // [out] IMFSample **ppSample if (FAILED(hr)) { - if (sDebug) { qDebug() << "ReadSample failed."; } + if (sDebug) { + qDebug() << "ReadSample failed."; + } break; } if (sDebug) { - qDebug() << "ReadSample timestamp:" << timestamp - << "frame:" << frameFromMF(timestamp) - << "dwflags:" << dwFlags; + qDebug() << "ReadSample timestamp:" << timestamp << "frame:" + << frameFromMF(timestamp) << "dwflags:" << dwFlags; } if (dwFlags & MF_SOURCE_READERF_ERROR) { // our source reader is now dead, according to the docs - qWarning() << "SSMF: ReadSample set ERROR, SourceReader is now dead"; + qWarning() + << "SSMF: ReadSample set ERROR, SourceReader is now dead"; m_dead = true; break; } else if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { @@ -260,28 +288,27 @@ unsigned int SoundSourceMediaFoundation::read(unsigned long size, error = true; goto releaseSample; } - qint16 *buffer(NULL); - size_t bufferLength(0); - hr = pMBuffer->Lock(reinterpret_cast(&buffer), NULL, - reinterpret_cast(&bufferLength)); + sample_type *buffer(NULL); + DWORD bufferLengthInBytes(0); + hr = pMBuffer->Lock(reinterpret_cast(&buffer), NULL, &bufferLengthInBytes); if (FAILED(hr)) { error = true; goto releaseMBuffer; } - bufferLength /= (kBitsPerSample / 8 * kNumChannels); // now in frames + size_type bufferLength = samples2frames(bufferLengthInBytes / sizeof(buffer[0])); if (m_seeking) { qint64 bufferPosition(frameFromMF(timestamp)); if (sDebug) { - qDebug() << "While seeking to " - << m_nextFrame << "WMF put us at" << bufferPosition; + qDebug() << "While seeking to " << m_nextFrame + << "WMF put us at" << bufferPosition; } if (m_nextFrame < bufferPosition) { // Uh oh. We are farther forward than our seek target. Emit // silence? We can't seek backwards here. - SAMPLE* pBufferCurpos = destBuffer + - (size - framesNeeded * kNumChannels); + sample_type* pBufferCurpos = sampleBuffer + + frames2samples(frameCount - framesNeeded); qint64 offshootFrames = bufferPosition - m_nextFrame; // If we can correct this immediately, write zeros and adjust @@ -289,12 +316,11 @@ unsigned int SoundSourceMediaFoundation::read(unsigned long size, if (offshootFrames <= framesNeeded) { qWarning() << __FILE__ << __LINE__ - << "Working around inaccurate seeking. Writing silence for" - << offshootFrames << "frames"; + << "Working around inaccurate seeking. Writing silence for" + << offshootFrames << "frames"; // Set offshootFrames * kNumChannels samples to zero. memset(pBufferCurpos, 0, - sizeof(*pBufferCurpos) * offshootFrames * - kNumChannels); + frames2samples(sizeof(*pBufferCurpos) * offshootFrames)); // Now m_nextFrame == bufferPosition m_nextFrame += offshootFrames; framesNeeded -= offshootFrames; @@ -306,14 +332,14 @@ unsigned int SoundSourceMediaFoundation::read(unsigned long size, m_seeking = false; m_nextFrame = bufferPosition; qWarning() << __FILE__ << __LINE__ - << "Seek offshoot is too drastic. Cutting losses and pretending the current decoded audio buffer is the right seek point."; + << "Seek offshoot is too drastic. Cutting losses and pretending the current decoded audio buffer is the right seek point."; } } - if (m_nextFrame >= bufferPosition && - m_nextFrame < bufferPosition + bufferLength) { + if (m_nextFrame >= bufferPosition + && m_nextFrame < bufferPosition + bufferLength) { // m_nextFrame is in this buffer. - buffer += (m_nextFrame - bufferPosition) * kNumChannels; + buffer += frames2samples(m_nextFrame - bufferPosition); bufferLength -= m_nextFrame - bufferPosition; m_seeking = false; } else { @@ -324,58 +350,53 @@ unsigned int SoundSourceMediaFoundation::read(unsigned long size, // If the bufferLength is larger than the leftover buffer, re-allocate // it with 2x the space. - if (bufferLength * kNumChannels > m_leftoverBufferSize) { + if (frames2samples(bufferLength) > m_leftoverBufferSize) { int newSize = m_leftoverBufferSize; - while (newSize < bufferLength * kNumChannels) { + while (newSize < frames2samples(bufferLength)) { newSize *= 2; } - qint16* newBuffer = new qint16[newSize]; + sample_type* newBuffer = new sample_type[newSize]; memcpy(newBuffer, m_leftoverBuffer, - sizeof(m_leftoverBuffer[0]) * m_leftoverBufferSize); - delete [] m_leftoverBuffer; + sizeof(m_leftoverBuffer[0]) * m_leftoverBufferSize); + delete[] m_leftoverBuffer; m_leftoverBuffer = newBuffer; m_leftoverBufferSize = newSize; } - copyFrames(destBuffer + (size - framesNeeded * kNumChannels), - &framesNeeded, buffer, bufferLength); + copyFrames( + sampleBuffer + frames2samples(frameCount - framesNeeded), + &framesNeeded, + buffer, bufferLength); -releaseRawBuffer: - hr = pMBuffer->Unlock(); + releaseRawBuffer: hr = pMBuffer->Unlock(); // I'm ignoring this, MSDN for IMFMediaBuffer::Unlock stipulates // nothing about the state of the instance if this fails so might as // well just let it be released. //if (FAILED(hr)) break; -releaseMBuffer: - safeRelease(&pMBuffer); -releaseSample: - safeRelease(&pSample); - if (error) break; + releaseMBuffer: safeRelease(&pMBuffer); + releaseSample: safeRelease(&pSample); + if (error) + break; } - m_nextFrame += framesRequested - framesNeeded; + size_type framesRead = frameCount - framesNeeded; + m_iCurrentPosition += framesRead; + m_nextFrame += framesRead; if (m_leftoverBufferLength > 0) { if (framesNeeded != 0) { qWarning() << __FILE__ << __LINE__ - << "WARNING: Expected frames needed to be 0. Abandoning this file."; + << "WARNING: Expected frames needed to be 0. Abandoning this file."; m_dead = true; } m_leftoverBufferPosition = m_nextFrame; } - long samples_read = size - framesNeeded * kNumChannels; - m_iCurrentPosition += samples_read; - if (sDebug) { qDebug() << "read()" << size << "returning" << samples_read; } - return samples_read; -} - -inline unsigned long SoundSourceMediaFoundation::length() -{ - unsigned long len(secondsFromMF(m_mfDuration) * kSampleRate * kNumChannels); - return len % kNumChannels == 0 ? len : len + 1; + if (sDebug) { + qDebug() << "read()" << frameCount << "returning" << framesRead; + } + return framesRead; } -Result SoundSourceMediaFoundation::parseHeader() -{ +Result SoundSourceMediaFoundation::parseHeader() { // Must be toLocal8Bit since Windows fopen does not do UTF-8 TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); @@ -400,7 +421,6 @@ Result SoundSourceMediaFoundation::parseHeader() } QImage SoundSourceMediaFoundation::parseCoverArt() { - setType("m4a"); TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { @@ -411,15 +431,13 @@ QImage SoundSourceMediaFoundation::parseCoverArt() { } // static -QList SoundSourceMediaFoundation::supportedFileExtensions() -{ +QList SoundSourceMediaFoundation::supportedFileExtensions() { QList list; list.push_back("m4a"); list.push_back("mp4"); return list; } - //------------------------------------------------------------------- // configureAudioStream // @@ -428,14 +446,13 @@ QList SoundSourceMediaFoundation::supportedFileExtensions() //------------------------------------------------------------------- /** Cobbled together from: - http://msdn.microsoft.com/en-us/library/dd757929(v=vs.85).aspx - and http://msdn.microsoft.com/en-us/library/dd317928(VS.85).aspx - -- Albert - If anything in here fails, just bail. I'm not going to decode HRESULTS. - -- Bill - */ -bool SoundSourceMediaFoundation::configureAudioStream() -{ + http://msdn.microsoft.com/en-us/library/dd757929(v=vs.85).aspx + and http://msdn.microsoft.com/en-us/library/dd317928(VS.85).aspx + -- Albert + If anything in here fails, just bail. I'm not going to decode HRESULTS. + -- Bill + */ +bool SoundSourceMediaFoundation::configureAudioStream() { HRESULT hr(S_OK); // deselect all streams, we only want the first @@ -445,7 +462,8 @@ bool SoundSourceMediaFoundation::configureAudioStream() return false; } - hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, true); + hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + true); if (FAILED(hr)) { qWarning() << "SSMF: failed to select first audio stream"; return false; @@ -463,7 +481,7 @@ bool SoundSourceMediaFoundation::configureAudioStream() return false; } - hr = m_pAudioType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM); + hr = m_pAudioType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_Float); if (FAILED(hr)) { qWarning() << "SSMF: failed to set subtype"; return false; @@ -490,14 +508,14 @@ bool SoundSourceMediaFoundation::configureAudioStream() // MSDN for this attribute says that if bps is 8, samples are unsigned. // Otherwise, they're signed (so they're signed for us as 16 bps). Why // chose to hide this rather useful tidbit here is beyond me -bkgood - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, kBitsPerSample); + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, sizeof(m_leftoverBuffer[0]) * 8); if (FAILED(hr)) { qWarning() << "SSMF: failed to set bits per sample"; return false; } hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, - kNumChannels * (kBitsPerSample / 8)); + frames2samples(sizeof(m_leftoverBuffer[0]))); if (FAILED(hr)) { qWarning() << "SSMF: failed to set block alignment"; return false; @@ -517,9 +535,8 @@ bool SoundSourceMediaFoundation::configureAudioStream() // Set this type on the source reader. The source reader will // load the necessary decoder. - hr = m_pReader->SetCurrentMediaType( - MF_SOURCE_READER_FIRST_AUDIO_STREAM, - NULL, m_pAudioType); + hr = m_pReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + NULL, m_pAudioType); // the reader has the media type now, free our reference so we can use our // pointer for other purposes. Do this before checking for failure so we @@ -531,48 +548,41 @@ bool SoundSourceMediaFoundation::configureAudioStream() } // Get the complete uncompressed format. - hr = m_pReader->GetCurrentMediaType( - MF_SOURCE_READER_FIRST_AUDIO_STREAM, - &m_pAudioType); + hr = m_pReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + &m_pAudioType); if (FAILED(hr)) { qWarning() << "SSMF: failed to retrieve completed media type"; return false; } // Ensure the stream is selected. - hr = m_pReader->SetStreamSelection( - MF_SOURCE_READER_FIRST_AUDIO_STREAM, - true); + hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + true); if (FAILED(hr)) { qWarning() << "SSMF: failed to select first audio stream (again)"; return false; } - // this may not be safe on all platforms as m_leftoverBufferSize is a - // size_t and this function is writing a uint32. However, on 32-bit - // Windows 7, size_t is defined as uint which is 32-bits, so we're safe - // for all supported platforms -bkgood UINT32 leftoverBufferSize = 0; hr = m_pAudioType->GetUINT32(MF_MT_SAMPLE_SIZE, &leftoverBufferSize); if (FAILED(hr)) { qWarning() << "SSMF: failed to get buffer size"; return false; } - m_leftoverBufferSize = static_cast(leftoverBufferSize); - m_leftoverBufferSize /= 2; // convert size in bytes to size in int16s - m_leftoverBuffer = new qint16[m_leftoverBufferSize]; + m_leftoverBufferSize = leftoverBufferSize; + m_leftoverBufferSize /= sizeof(sample_type); // convert size in bytes to sizeof(sample_type) + m_leftoverBuffer = new sample_type[m_leftoverBufferSize]; return true; } -bool SoundSourceMediaFoundation::readProperties() -{ +bool SoundSourceMediaFoundation::readProperties() { PROPVARIANT prop; HRESULT hr = S_OK; //Get the duration, provided as a 64-bit integer of 100-nanosecond units hr = m_pReader->GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE, - MF_PD_DURATION, &prop); + MF_PD_DURATION, &prop); if (FAILED(hr)) { qWarning() << "SSMF: error getting duration"; return false; @@ -585,11 +595,6 @@ bool SoundSourceMediaFoundation::readProperties() qDebug() << "SSMF: Duration:" << getDuration(); PropVariantClear(&prop); - // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for - // presentation descriptors, one of which MFSourceReader is not. - // Therefore, we calculate it ourselves. - setBitrate(kBitsPerSample * kSampleRate * kNumChannels); - return true; } @@ -598,16 +603,14 @@ bool SoundSourceMediaFoundation::readProperties() * is moved to the beginning of m_leftoverBuffer, so empty it first (possibly * with this method). If src and dest overlap, I'll hurt you. */ -void SoundSourceMediaFoundation::copyFrames( - qint16 *dest, size_t *destFrames, const qint16 *src, size_t srcFrames) -{ +void SoundSourceMediaFoundation::copyFrames(sample_type *dest, size_t *destFrames, + const sample_type *src, size_t srcFrames) { if (srcFrames > *destFrames) { int samplesToCopy(*destFrames * kNumChannels); memcpy(dest, src, samplesToCopy * sizeof(*src)); srcFrames -= *destFrames; - memmove(m_leftoverBuffer, - src + samplesToCopy, - srcFrames * kNumChannels * sizeof(*src)); + memmove(m_leftoverBuffer, src + samplesToCopy, + srcFrames * kNumChannels * sizeof(*src)); *destFrames = 0; m_leftoverBufferLength = srcFrames; } else { @@ -619,35 +622,3 @@ void SoundSourceMediaFoundation::copyFrames( } } } - -/** - * Convert a 100ns Media Foundation value to a number of seconds. - */ -inline qreal SoundSourceMediaFoundation::secondsFromMF(qint64 mf) -{ - return static_cast(mf) / 1e7; -} - -/** - * Convert a number of seconds to a 100ns Media Foundation value. - */ -inline qint64 SoundSourceMediaFoundation::mfFromSeconds(qreal sec) -{ - return sec * 1e7; -} - -/** - * Convert a 100ns Media Foundation value to a frame offset. - */ -inline qint64 SoundSourceMediaFoundation::frameFromMF(qint64 mf) -{ - return static_cast(mf) * kSampleRate / 1e7; -} - -/** - * Convert a frame offset to a 100ns Media Foundation value. - */ -inline qint64 SoundSourceMediaFoundation::mfFromFrame(qint64 frame) -{ - return static_cast(frame) / kSampleRate * 1e7; -} diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 96794568f76..49a5f1f1e22 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -38,31 +38,34 @@ class IMFMediaType; class IMFMediaSource; class SoundSourceMediaFoundation : public Mixxx::SoundSource { - public: + typedef SoundSource Super; + +public: + static QList supportedFileExtensions(); + explicit SoundSourceMediaFoundation(QString filename); ~SoundSourceMediaFoundation(); - Result open(); - long seek(long filepos); - unsigned read(unsigned long size, const SAMPLE *buffer); - inline long unsigned length(); + Result parseHeader(); QImage parseCoverArt(); - static QList supportedFileExtensions(); - private: + Result open(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; + +private: bool configureAudioStream(); bool readProperties(); - void copyFrames(qint16 *dest, size_t *destFrames, const qint16 *src, - size_t srcFrames); - static inline qreal secondsFromMF(qint64 mf); - static inline qint64 mfFromSeconds(qreal sec); - static inline qint64 frameFromMF(qint64 mf); - static inline qint64 mfFromFrame(qint64 frame); + void copyFrames(sample_type *dest, size_t *destFrames, const sample_type *src, + size_t srcFrames); + IMFSourceReader *m_pReader; IMFMediaType *m_pAudioType; wchar_t *m_wcFilename; int m_nextFrame; - qint16 *m_leftoverBuffer; + sample_type *m_leftoverBuffer; size_t m_leftoverBufferSize; size_t m_leftoverBufferLength; int m_leftoverBufferPosition; @@ -72,28 +75,23 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSource { bool m_seeking; }; -extern "C" MY_EXPORT const char* getMixxxVersion() -{ +extern "C" MY_EXPORT const char* getMixxxVersion() { return VERSION; } -extern "C" MY_EXPORT int getSoundSourceAPIVersion() -{ +extern "C" MY_EXPORT int getSoundSourceAPIVersion() { return MIXXX_SOUNDSOURCE_API_VERSION; } -extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString filename) -{ +extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString filename) { return new SoundSourceMediaFoundation(filename); } -extern "C" MY_EXPORT char** supportedFileExtensions() -{ +extern "C" MY_EXPORT char** supportedFileExtensions() { QList exts = SoundSourceMediaFoundation::supportedFileExtensions(); //Convert to C string array. - char** c_exts = (char**)malloc((exts.count() + 1) * sizeof(char*)); - for (int i = 0; i < exts.count(); i++) - { + char** c_exts = (char**) malloc((exts.count() + 1) * sizeof(char*)); + for (int i = 0; i < exts.count(); i++) { QByteArray qba = exts[i].toUtf8(); c_exts[i] = strdup(qba.constData()); qDebug() << c_exts[i]; @@ -103,9 +101,9 @@ extern "C" MY_EXPORT char** supportedFileExtensions() return c_exts; } -extern "C" MY_EXPORT void freeFileExtensions(char **exts) -{ - for (int i(0); exts[i]; ++i) free(exts[i]); +extern "C" MY_EXPORT void freeFileExtensions(char **exts) { + for (int i(0); exts[i]; ++i) + free(exts[i]); free(exts); } diff --git a/plugins/soundsourcewv/SConscript b/plugins/soundsourcewv/SConscript index 984e3bddc5a..c6c68a560fb 100644 --- a/plugins/soundsourcewv/SConscript +++ b/plugins/soundsourcewv/SConscript @@ -14,6 +14,7 @@ wv_sources = [ "soundsourcewv.cpp", # Wavpack support "soundsourcetaglib.cpp", # TagLib dependencies "soundsource.cpp", # required to subclass SoundSource + "audiosource.cpp", # required to subclass AudioSource "sampleutil.cpp", # utility functions ] diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 301e8ee0889..f1debe22427 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -1,125 +1,80 @@ -//soundsourcewv.cpp : sound source proxy for .wv (WavPack files) -//created by fenugrec -//great help from rryan & others on #mixxx -//format_samples adapted from cmus (Peter Lemenkov) - -#include - #include "soundsourcewv.h" #include "soundsourcetaglib.h" #include "sampleutil.h" #include -namespace Mixxx { - -SoundSourceWV::SoundSourceWV(QString qFilename) - : SoundSource(qFilename), - filewvc(NULL), - Bps(0), - filelength(0) { - setType("wv"); -} +namespace Mixxx { -SoundSourceWV::~SoundSourceWV(){ - if (filewvc) { - WavpackCloseFile(filewvc); - filewvc=NULL; - } -} - -QList SoundSourceWV::supportedFileExtensions() -{ +QList SoundSourceWV::supportedFileExtensions() { QList list; list.push_back("wv"); return list; } +SoundSourceWV::SoundSourceWV(QString qFilename) + : Super(qFilename, "wv"), m_wpc(NULL), m_sampleScale(0.0f) { +} + +SoundSourceWV::~SoundSourceWV() { + if (m_wpc) { + WavpackCloseFile(m_wpc); + } +} -Result SoundSourceWV::open() -{ +Result SoundSourceWV::open() { QByteArray qBAFilename(getFilename().toLocal8Bit()); char msg[80]; //hold possible error message - filewvc = WavpackOpenFileInput(qBAFilename.constData(), msg, OPEN_2CH_MAX | OPEN_WVC,0); - if (!filewvc) { - qDebug() << "SSWV::open: failed to open file : "<< msg; + m_wpc = WavpackOpenFileInput(qBAFilename.constData(), msg, + OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); + if (!m_wpc) { + qDebug() << "SSWV::open: failed to open file : " << msg; return ERR; } - if (WavpackGetMode(filewvc) & MODE_FLOAT) { - qDebug() << "SSWV::open: cannot load 32bit float files"; - WavpackCloseFile(filewvc); - filewvc=NULL; - return ERR; - } - // wavpack_open succeeded -> populate variables - filelength = WavpackGetNumSamples(filewvc); - setSampleRate(WavpackGetSampleRate(filewvc)); - setChannels(WavpackGetReducedChannels(filewvc)); - Bps=WavpackGetBytesPerSample(filewvc); - qDebug () << "SSWV::open: opened filewvc with filelength: "<2) { - qDebug() << "SSWV::open: warning: input file has > 2 bytes per sample, will be truncated to 16bits"; + + setChannelCount(WavpackGetReducedChannels(m_wpc)); + setFrameRate(WavpackGetSampleRate(m_wpc)); + setFrameCount(WavpackGetNumSamples(m_wpc)); + + if (WavpackGetMode(m_wpc) & MODE_FLOAT) { + m_sampleScale = 1.0f; + } else { + const int bitsPerSample = WavpackGetBitsPerSample(m_wpc); + const uint32_t maxSampleValue = uint32_t(1) << (bitsPerSample - 1); + m_sampleScale = 1.0f / (0.5f + sample_type(maxSampleValue)); } + return OK; } - -long SoundSourceWV::seek(long filepos){ - if (WavpackSeekSample(filewvc,filepos>>1) != true) { - qDebug() << "SSWV::seek : could not seek to sample #" << (filepos>>1); - return 0; +AudioSource::diff_type SoundSourceWV::seekFrame(diff_type frameIndex) { + if (WavpackSeekSample(m_wpc, frameIndex) == TRUE) { + return frameIndex; + } else { + qDebug() << "SSWV::seek : could not seek to frame #" << frameIndex; + return WavpackGetSampleIndex(m_wpc); } - return filepos; } - -unsigned SoundSourceWV::read(volatile unsigned long size, const SAMPLE* destination){ - //SAMPLE is "short int" => 16bits. [size] is timesamps*2 (because L+R) - SAMPLE * dest = (SAMPLE*) destination; - unsigned long sampsread=0; - unsigned long timesamps, tsdone; - - //tempbuffer is fixed size : WV_BUF_LENGTH of uint32 - while (sampsread != size) { - timesamps=(size-sampsread)>>1; //timesamps still remaining - if (timesamps > (WV_BUF_LENGTH / getChannels())) { //if requested size requires more than one buffer filling - timesamps=(WV_BUF_LENGTH / getChannels()); //tempbuffer must hold (timesamps * channels) samples - qDebug() << "SSWV::read : performance warning, size requested > buffer size !"; - } - - tsdone=WavpackUnpackSamples(filewvc, tempbuffer, timesamps); //fill temp buffer with timesamps*4bytes*channels - //data is right justified, format_samples() fixes that. - - SoundSourceWV::format_samples(Bps, (char *) (dest + (sampsread>>1) * getChannels()), tempbuffer, tsdone*getChannels()); - //this will unpack the 4byte/sample - //output of wUnpackSamples(), sign-extending or truncating to output 16bit / sample. - //specifying dest+sampsread should resume the conversion where it was left if size requested - //required multiple reads (size req. > fixed buffer size) - - sampsread = sampsread + (tsdone<<1); - if (tsdone!=timesamps) { - qDebug () << "SSWV::read : WavpackUnpackSamples read "<(sampleBuffer), frameCount); + if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { + // signed integer -> float + const size_type sampleCount = frames2samples(unpackCount); + for (size_type i = 0; i < sampleCount; ++i) { + const int32_t sampleValue = + reinterpret_cast(sampleBuffer)[i]; + sampleBuffer[i] = SampleUtil::clampSample( + sampleValue * m_sampleScale); } - - } - - if (getChannels() == 1) { //if MONO : expand array to double it's size; see ssov.cpp - SampleUtil::doubleMonoToDualMono(dest, sampsread / 2); } - - return sampsread; -} - - -inline long unsigned SoundSourceWV::length(){ - //filelength is # of timesamps. - return filelength<<1; + return unpackCount; } - Result SoundSourceWV::parseHeader() { const QByteArray qBAFilename(getFilename().toLocal8Bit()); TagLib::WavPack::File f(qBAFilename.constData()); @@ -154,43 +109,4 @@ QImage SoundSourceWV::parseCoverArt() { } } -void SoundSourceWV::format_samples(int Bps, char *dst, int32_t *src, uint32_t count) -{ - //this handles converting the fixed 32bit per sample produced by UnpackSamples - //to 16 bps, by truncating (24/32) or sign-extending (8) - //could eventually be asm-optimized.. - int32_t temp; - - switch (Bps) { - case 1: - while (count--) { - *dst++ = (char) 0; //left shift the 8 bit sample - *dst++ = (char) *src++ ;//+ 128; //only works with u8int ? - } - break; - case 2: - while (count--) { - *dst++ = (char) (temp = *src++); //low byte - *dst++ = (char) (temp >> 8); //high byte - } - break; - case 3: //modified to truncate to 16bits - while (count--) { - *dst++ = (char) (temp = (*src++) >> 8); - *dst++ = (char) (temp >> 8); - } - break; - case 4: //also truncates - while (count--) { - *dst++ = (char) (temp = (*src++) >> 16); - *dst++ = (char) (temp >> 8); - //*dst++ = (char) (temp >> 16); - //*dst++ = (char) (temp >> 24); - } - break; - } - - return; -} - } // namespace Mixxx diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index a013e42a01c..321bbfb0f94 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -1,15 +1,8 @@ -//soundsourcewv.h -// wavpack sound proxy for mixxx. -// fenugrec 12/2009 - - #ifndef SOUNDSOURCEWV_H #define SOUNDSOURCEWV_H -#include -#include "soundsource.h" #include "defs_version.h" -#include "util/defs.h" +#include "soundsource.h" #include "wavpack/wavpack.h" @@ -23,48 +16,48 @@ namespace Mixxx { -class SoundSourceWV : public SoundSource { - public: - explicit SoundSourceWV(QString qFilename); - ~SoundSourceWV(); - Result open(); - long seek(long); - unsigned read(unsigned long size, const SAMPLE*); - inline long unsigned length(); - Result parseHeader(); - QImage parseCoverArt(); - static QList supportedFileExtensions(); - private: - WavpackContext * filewvc; //works as a file handle to access the wv file. - int Bps; - unsigned long filelength; - int32_t tempbuffer[WV_BUF_LENGTH]; //hax ! legacy from cmus. this is 64k*4bytes. - void format_samples(int, char *, int32_t *, uint32_t); -}; +class SoundSourceWV: public SoundSource { + typedef SoundSource Super; + +public: + static QList supportedFileExtensions(); + + explicit SoundSourceWV(QString qFilename); + ~SoundSourceWV(); + Result parseHeader(); + QImage parseCoverArt(); + + Result open(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; + +private: + WavpackContext* m_wpc; + + sample_type m_sampleScale; +}; -extern "C" MY_EXPORT const char* getMixxxVersion() -{ +extern "C" MY_EXPORT const char* getMixxxVersion() { return VERSION; } -extern "C" MY_EXPORT int getSoundSourceAPIVersion() -{ +extern "C" MY_EXPORT int getSoundSourceAPIVersion() { return MIXXX_SOUNDSOURCE_API_VERSION; } -extern "C" MY_EXPORT SoundSource* getSoundSource(QString filename) -{ +extern "C" MY_EXPORT SoundSource* getSoundSource(QString filename) { return new SoundSourceWV(filename); } -extern "C" MY_EXPORT char** supportedFileExtensions() -{ +extern "C" MY_EXPORT char** supportedFileExtensions() { QList exts = SoundSourceWV::supportedFileExtensions(); //Convert to C string array. - char** c_exts = (char**)malloc((exts.count() + 1) * sizeof(char*)); - for (int i = 0; i < exts.count(); i++) - { + char** c_exts = (char**) malloc((exts.count() + 1) * sizeof(char*)); + for (int i = 0; i < exts.count(); i++) { QByteArray qba = exts[i].toUtf8(); c_exts[i] = strdup(qba.constData()); qDebug() << c_exts[i]; @@ -74,9 +67,9 @@ extern "C" MY_EXPORT char** supportedFileExtensions() return c_exts; } -extern "C" MY_EXPORT void freeFileExtensions(char **exts) -{ - for (int i(0); exts[i]; ++i) free(exts[i]); +extern "C" MY_EXPORT void freeFileExtensions(char **exts) { + for (int i(0); exts[i]; ++i) + free(exts[i]); free(exts); } diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 7d69c07271f..8accc6cbc32 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -8,7 +8,6 @@ #include "analyserqueue.h" #include "soundsourceproxy.h" #include "playerinfo.h" -#include "sampleutil.h" #include "util/timer.h" #include "library/trackcollection.h" #include "analyserwaveform.h" @@ -26,17 +25,21 @@ // 100 for 10% step after finalize #define FINALIZE_PERCENT 1 -// We need to use a smaller block size becuase on Linux, the AnalyserQueue -// can starve the CPU of its resources, resulting in xruns.. A block size of -// 8192 seems to do fine. -const int kAnalysisBlockSize = 8192; +namespace +{ + const Mixxx::AudioSource::size_type kAnalysisChannels = 2; // stereo + + // We need to use a smaller block size becuase on Linux, the AnalyserQueue + // can starve the CPU of its resources, resulting in xruns.. A block size of + // 8192 seems to do fine. + const int kAnalysisBlockSize = 8192; +} AnalyserQueue::AnalyserQueue(TrackCollection* pTrackCollection) : m_aq(), m_exit(false), m_aiCheckPriorities(false), - m_pSamplesPCM(new SAMPLE[kAnalysisBlockSize]), - m_pSamples(new CSAMPLE[kAnalysisBlockSize]), + m_pStereoSamples(new Mixxx::AudioSource::sample_type[kAnalysisBlockSize]), m_tioq(), m_qm(), m_qwait(), @@ -59,8 +62,7 @@ AnalyserQueue::~AnalyserQueue() { } //qDebug() << "AnalyserQueue::~AnalyserQueue()"; - delete [] m_pSamplesPCM; - delete [] m_pSamples; + delete [] m_pStereoSamples; } void AnalyserQueue::addAnalyser(Analyser* an) { @@ -159,61 +161,61 @@ TrackPointer AnalyserQueue::dequeueNextBlocking() { } // This is called from the AnalyserQueue thread -bool AnalyserQueue::doAnalysis(TrackPointer tio, const Mixxx::SoundSourcePointer& pSoundSource) { - int totalSamples = pSoundSource->length(); - //qDebug() << tio->getFilename() << " has " << totalSamples << " samples."; - int processedSamples = 0; +bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudioSource) { + const Mixxx::AudioSource::size_type totalFrames = pAudioSource->getFrameCount(); + const Mixxx::AudioSource::size_type frameCount = kAnalysisBlockSize / kAnalysisChannels; + Mixxx::AudioSource::size_type processedFrames = 0; + Mixxx::AudioSource::size_type readFrames = 0; QTime progressUpdateInhibitTimer; progressUpdateInhibitTimer.start(); // Inhibit Updates for 60 milliseconds - int read = 0; bool dieflag = false; bool cancelled = false; int progress; // progress in 0 ... 100 do { ScopedTimer t("AnalyserQueue::doAnalysis block"); - read = pSoundSource->read(kAnalysisBlockSize, m_pSamplesPCM); + + readFrames = pAudioSource->readStereoFrameSamplesInterleaved(frameCount, m_pStereoSamples); + const Mixxx::AudioSource::size_type readStereoFrameSamplesInterleaved = readFrames * kAnalysisChannels; // To compare apples to apples, let's only look at blocks that are the // full block size. - if (read != kAnalysisBlockSize) { + if (readFrames != frameCount) { t.cancel(); } // Safety net in case something later barfs on 0 sample input - if (read == 0) { + if (frameCount == 0) { t.cancel(); break; } // If we get more samples than length, ask the analysers to process // up to the number we promised, then stop reading - AD - if (read + processedSamples > totalSamples) { - qDebug() << "While processing track of length " << totalSamples << " actually got " - << read + processedSamples << " samples, truncating analysis at expected length"; - read = totalSamples - processedSamples; + if (readFrames + processedFrames > totalFrames) { + qDebug() << "While processing track of length " << totalFrames << " actually got " + << (readFrames + processedFrames) << " frames, truncating analysis at expected length"; + readFrames = totalFrames - processedFrames; dieflag = true; } - SampleUtil::convertS16ToFloat32(m_pSamples, m_pSamplesPCM, read); - QListIterator it(m_aq); while (it.hasNext()) { Analyser* an = it.next(); //qDebug() << typeid(*an).name() << ".process()"; - an->process(m_pSamples, read); + an->process(m_pStereoSamples, readStereoFrameSamplesInterleaved); //qDebug() << "Done " << typeid(*an).name() << ".process()"; } // emit progress updates // During the doAnalysis function it goes only to 100% - FINALIZE_PERCENT // because the finalise functions will take also some time - processedSamples += read; + processedFrames += readFrames; //fp div here prevents insane signed overflow - progress = (int)(((float)processedSamples)/totalSamples * + progress = (int)(((float)processedFrames)/totalFrames * (1000 - FINALIZE_PERCENT)); if (m_progressInfo.track_progress != progress) { @@ -250,7 +252,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, const Mixxx::SoundSourcePointer if (dieflag || cancelled) { t.cancel(); } - } while(read == kAnalysisBlockSize && !dieflag); + } while(!dieflag && (frameCount == readFrames)); return !cancelled; //don't return !dieflag or we might reanalyze over and over } @@ -300,14 +302,14 @@ void AnalyserQueue::run() { // Get the audio SoundSourceProxy soundSourceProxy(nextTrack); - Mixxx::SoundSourcePointer pSoundSource(soundSourceProxy.open()); - if (pSoundSource.isNull()) { + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); + if (pAudioSource.isNull()) { qWarning() << "Failed to open file for analyzing:" << nextTrack->getLocation(); continue; } - int iNumSamples = pSoundSource->length(); - int iSampleRate = pSoundSource->getSampleRate(); + int iNumSamples = pAudioSource->getFrameCount() * kAnalysisChannels; + int iSampleRate = pAudioSource->getFrameRate(); if (iNumSamples == 0 || iSampleRate == 0) { qWarning() << "Skipping invalid file:" << nextTrack->getLocation(); @@ -329,7 +331,7 @@ void AnalyserQueue::run() { if (processTrack) { emitUpdateProgress(nextTrack, 0); - bool completed = doAnalysis(nextTrack, pSoundSource); + bool completed = doAnalysis(nextTrack, pAudioSource); if (!completed) { //This track was cancelled QListIterator itf(m_aq); diff --git a/src/analyserqueue.h b/src/analyserqueue.h index 9b742ed1be8..cb36c5a9508 100644 --- a/src/analyserqueue.h +++ b/src/analyserqueue.h @@ -9,8 +9,8 @@ #include "configobject.h" #include "analyser.h" -#include "soundsource.h" #include "trackinfoobject.h" +#include "audiosource.h" class TrackCollection; @@ -58,13 +58,12 @@ class AnalyserQueue : public QThread { bool isLoadedTrackWaiting(TrackPointer tio); TrackPointer dequeueNextBlocking(); - bool doAnalysis(TrackPointer tio, const Mixxx::SoundSourcePointer& pSoundSource); + bool doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudioSource); void emitUpdateProgress(TrackPointer tio, int progress); bool m_exit; QAtomicInt m_aiCheckPriorities; - SAMPLE* m_pSamplesPCM; - CSAMPLE* m_pSamples; + Mixxx::AudioSource::sample_type* m_pStereoSamples; // The processing queue and associated mutex QQueue m_tioq; diff --git a/src/audiosource.cpp b/src/audiosource.cpp new file mode 100644 index 00000000000..b6d18b1416a --- /dev/null +++ b/src/audiosource.cpp @@ -0,0 +1,65 @@ +#include "audiosource.h" + +#include "sampleutil.h" + +#include + +namespace Mixxx { + +/*static*/const AudioSource::sample_type AudioSource::kSampleValueZero = + CSAMPLE_ZERO; +/*static*/const AudioSource::sample_type AudioSource::kSampleValuePeak = + CSAMPLE_PEAK; + +AudioSource::AudioSource() + : m_channelCount(kChannelCountDefault), m_frameRate(kFrameRateDefault), m_frameCount( + kFrameCountDefault) { +} + +AudioSource::~AudioSource() { +} + +void AudioSource::setChannelCount(size_type channelCount) { + m_channelCount = channelCount; +} +void AudioSource::setFrameRate(size_type frameRate) { + m_frameRate = frameRate; +} +void AudioSource::setFrameCount(size_type frameCount) { + m_frameCount = frameCount; +} + +void AudioSource::reset() { + m_channelCount = kChannelCountDefault; + m_frameRate = kFrameRateDefault; + m_frameCount = kFrameCountDefault; +} + +AudioSource::size_type AudioSource::readStereoFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + switch (getChannelCount()) { + case 1: // mono channel + { + const AudioSource::size_type readCount = readFrameSamplesInterleaved( + frameCount, sampleBuffer); + SampleUtil::doubleMonoToDualMono(sampleBuffer, readCount); + return readCount; + } + case 2: // stereo channel(s) + { + return readFrameSamplesInterleaved(frameCount, sampleBuffer); + } + default: // multiple channels + { + typedef std::vector SampleBuffer; + SampleBuffer tempBuffer(frames2samples(frameCount)); + const AudioSource::size_type readCount = readFrameSamplesInterleaved( + frameCount, &tempBuffer[0]); + SampleUtil::copyMultiToStereo(sampleBuffer, &tempBuffer[0], readCount, + getChannelCount()); + return readCount; + } + } +} + +} diff --git a/src/audiosource.h b/src/audiosource.h new file mode 100644 index 00000000000..5734e5d2a86 --- /dev/null +++ b/src/audiosource.h @@ -0,0 +1,149 @@ +#ifndef MIXXX_AUDIOSOURCE_H +#define MIXXX_AUDIOSOURCE_H + +#include "util/types.h" // CSAMPLE + +#include + +#include // size_t / diff_t + +namespace Mixxx { + +// Common interface and base class for audio sources. +// +// Both the number of channels and the frame rate must +// be constant and are not allowed to change over time. +// +// The length of audio data is measured in frames. A frame +// is a tuple that contains one sample for each channel. +class AudioSource { +public: + typedef std::size_t size_type; + typedef std::ptrdiff_t diff_type; + typedef CSAMPLE sample_type; + + static const size_type kChannelCountZero = 0; + static const size_type kChannelCountMono = 1; + static const size_type kChannelCountStereo = 2; + static const size_type kChannelCountDefault = kChannelCountZero; + + static const size_type kFrameRateZero = 0; + static const size_type kFrameRateDefault = kFrameRateZero; + + static const size_type kFrameCountZero = 0; + static const size_type kFrameCountDefault = kFrameCountZero; + + static const sample_type kSampleValueZero; + static const sample_type kSampleValuePeak; + + virtual ~AudioSource(); + + // Returns the number of channels. The number of channels + // must be constant over time. + inline size_type getChannelCount() const { + return m_channelCount; + } + inline bool isChannelCountValid() const { + return kChannelCountZero < getChannelCount(); + } + inline bool isChannelCountMono() const { + return kChannelCountMono == getChannelCount(); + } + inline bool isChannelCountStereo() const { + return kChannelCountStereo == getChannelCount(); + } + + // Returns the number of frames per second. This equals + // the number samples for each channel per second, which + // must be uniform among all channels. The frame rate + // must be constant over time. + inline size_type getFrameRate() const { + return m_frameRate; + } + inline bool isFrameRateValid() const { + return kFrameRateZero < getFrameRate(); + } + + // Returns the total number of frames. + inline size_type getFrameCount() const { + return m_frameCount; + } + inline bool isFrameCountEmpty() const { + return kFrameCountZero >= getFrameCount(); + } + + // #frames -> #samples + template + inline T frames2samples(T frameCount) const { + Q_ASSERT(isChannelCountValid()); + return frameCount * getChannelCount(); + } + + // #samples -> #frames + template + inline T samples2frames(T sampleCount) const { + Q_ASSERT(isChannelCountValid()); + Q_ASSERT(0 == (sampleCount % getChannelCount())); + return sampleCount / getChannelCount(); + } + + // Adjusts the current frame seek index: + // - Index of first frame: frameIndex = 0 + // - Index of last frame: frameIndex = totalFrames() - 1 + // - The seek position in seconds is frameIndex / frameRate() + // Returns the actual current frame index which may differ from the + // requested index if the source does not support accurate seeking. + virtual diff_type seekFrame(diff_type frameIndex) = 0; + + // Fills the buffer with samples from each channel starting + // at the current frame seek position. + // + // The required size of the sampleBuffer is sampleCount = + // frames2samples(frameCount). The samples in the sampleBuffer + // are stored as consecutive frames with the samples for each + // channel interleaved. + // + // Returns the actual number of frames that have been read which + // may be lower than the requested number of frames. The current + // frame seek position is moved forward to the next unread frame. + virtual size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) = 0; + + // Utility function for explicitly reading stereo (= 2 channels) + // frames from an AudioSource. This is commonly used in Mixxx! + // + // If this source provides only a single channel (mono) the samples + // of that channel will be doubled. If the source provides more + // than 2 channels only the first 2 channels will be read. The + // minimum required capacity of the sampleBuffer is frameCount * 2. + // + // Returns the actual number of frames that have been read which + // may be lower than the requested number of frames. The current + // frame seek position is moved forward to the next unread frame. + // + // Derived classes may provide an optimized version that doesn't + // require any post-processing as done by this default implementation. + // Especially down-mixing multiple channels to stereo is inefficient! + virtual size_type readStereoFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer); + +protected: + AudioSource(); + + void setChannelCount(size_type channelCount); + void setFrameRate(size_type frameRate); + void setFrameCount(size_type frameCount); + + void reset(); + +private: + size_type m_channelCount; + size_type m_frameRate; + size_type m_frameCount; +}; + +typedef QSharedPointer AudioSourcePointer; + +} // namespace Mixxx + +#endif // MIXXX_AUDIOSOURCE_H diff --git a/src/cachingreader.cpp b/src/cachingreader.cpp index b27545525c0..2b20a163902 100644 --- a/src/cachingreader.cpp +++ b/src/cachingreader.cpp @@ -6,7 +6,6 @@ #include "cachingreader.h" #include "trackinfoobject.h" -#include "soundsourceproxy.h" #include "sampleutil.h" #include "util/counter.h" #include "util/math.h" @@ -25,7 +24,7 @@ CachingReader::CachingReader(QString group, m_mruChunk(NULL), m_lruChunk(NULL), m_pRawMemoryBuffer(NULL), - m_iTrackNumSamplesCallbackSafe(0) { + m_iTrackNumFramesCallbackSafe(0) { int rawMemoryBufferLength = CachingReaderWorker::kSamplesPerChunk * maximumChunksInMemory; m_pRawMemoryBuffer = new CSAMPLE[rawMemoryBufferLength]; @@ -39,8 +38,8 @@ CachingReader::CachingReader(QString group, for (int i=0; i < maximumChunksInMemory; i++) { Chunk* c = new Chunk; c->chunk_number = -1; - c->length = 0; - c->data = bufferStart; + c->frameCount = 0; + c->stereoSamples = bufferStart; c->next_lru = NULL; c->prev_lru = NULL; @@ -154,7 +153,7 @@ void CachingReader::freeChunk(Chunk* pChunk) { m_mruChunk = removeFromLRUList(pChunk, m_mruChunk); pChunk->chunk_number = -1; - pChunk->length = 0; + pChunk->frameCount = 0; m_freeChunks.push_back(pChunk); } @@ -171,7 +170,7 @@ void CachingReader::freeAllChunks() { } if (!m_freeChunks.contains(c)) { c->chunk_number = -1; - c->length = 0; + c->frameCount = 0; c->next_lru = NULL; c->prev_lru = NULL; m_freeChunks.push_back(c); @@ -239,7 +238,7 @@ void CachingReader::process() { } else if (status.status == TRACK_LOADED) { freeAllChunks(); m_readerStatus = status.status; - m_iTrackNumSamplesCallbackSafe = status.trackNumSamples; + m_iTrackNumFramesCallbackSafe = status.trackFrameCount; } else if (status.status == CHUNK_READ_SUCCESS) { Chunk* pChunk = status.chunk; if (pChunk == NULL) { @@ -299,7 +298,7 @@ void CachingReader::process() { int CachingReader::read(int sample, int num_samples, CSAMPLE* buffer) { // Check for bad inputs - if (sample % 2 != 0 || num_samples < 0 || !buffer) { + if (sample % CachingReaderWorker::kChunkChannels != 0 || num_samples % CachingReaderWorker::kChunkChannels != 0 || num_samples < 0 || !buffer) { QString temp = QString("Sample = %1").arg(sample); qDebug() << "CachingReader::read() invalid arguments sample:" << sample << "num_samples:" << num_samples << "buffer:" << buffer; @@ -316,37 +315,37 @@ int CachingReader::read(int sample, int num_samples, CSAMPLE* buffer) { // Process messages from the reader thread. process(); + int samples_remaining = num_samples; + + // TODO: is it possible to move this code out of caching reader // and into enginebuffer? It doesn't quite make sense here, although // it makes preroll completely transparent to the rest of the code - //if we're in preroll... - int zerosWritten = 0; if (sample < 0) { - if (sample + num_samples <= 0) { - //everything is zeros, easy - memset(buffer, 0, sizeof(*buffer) * num_samples); - return num_samples; - } else { - //some of the buffer is zeros, some is from the file - memset(buffer, 0, sizeof(*buffer) * (0 - sample)); - buffer += (0 - sample); - num_samples = sample + num_samples; - zerosWritten = (0 - sample); - sample = 0; - //continue processing the rest of the chunks normally + int zero_samples = math_min(-sample, samples_remaining); + Q_ASSERT(0 <= zero_samples); + Q_ASSERT(zero_samples <= samples_remaining); + SampleUtil::clear(buffer, zero_samples); + samples_remaining -= zero_samples; + if (samples_remaining == 0) { + return 0; } + buffer += zero_samples; + sample += zero_samples; + Q_ASSERT(0 <= sample); } - int start_sample = math_min(m_iTrackNumSamplesCallbackSafe, - sample); - int start_chunk = chunkForSample(start_sample); - int end_sample = math_min(m_iTrackNumSamplesCallbackSafe, - sample + num_samples - 1); - int end_chunk = chunkForSample(end_sample); + Q_ASSERT(0 == (sample % CachingReaderWorker::kChunkChannels)); + const int frame = sample / CachingReaderWorker::kChunkChannels; + Q_ASSERT(0 == (samples_remaining % CachingReaderWorker::kChunkChannels)); + int frames_remaining = samples_remaining / CachingReaderWorker::kChunkChannels; + const int start_frame = math_min(m_iTrackNumFramesCallbackSafe, frame); + const int start_chunk = chunkForFrame(start_frame); + const int end_frame = math_min(m_iTrackNumFramesCallbackSafe, frame + (frames_remaining - 1)); + const int end_chunk = chunkForFrame(end_frame); - int samples_remaining = num_samples; - int current_sample = sample; + int current_frame = frame; // Sanity checks if (start_chunk > end_chunk) { @@ -361,7 +360,7 @@ int CachingReader::read(int sample, int num_samples, CSAMPLE* buffer) { // If the chunk is not in cache, then we must return an error. if (current == NULL) { // qDebug() << "Couldn't get chunk " << chunk_num - // << " in read() of [" << sample << "," << sample + num_samples + // << " in read() of [" << sample << "," << (sample + samples_remaining) // << "] chunks " << start_chunk << "-" << end_chunk; // Something is wrong. Break out of the loop, that should fill the @@ -370,83 +369,75 @@ int CachingReader::read(int sample, int num_samples, CSAMPLE* buffer) { break; } - int chunk_start_sample = CachingReaderWorker::sampleForChunk(chunk_num); - int chunk_offset = current_sample - chunk_start_sample; - int chunk_remaining_samples = current->length - chunk_offset; + int chunk_start_frame = CachingReaderWorker::frameForChunk(chunk_num); + const int chunk_frame_offset = current_frame - chunk_start_frame; + int chunk_remaining_frames = current->frameCount - chunk_frame_offset; // More sanity checks - if (current_sample < chunk_start_sample || current_sample % 2 != 0) { + if (current_frame < chunk_start_frame) { qDebug() << "CachingReader::read() bad chunk parameters" - << "chunk_start_sample" << chunk_start_sample - << "current_sample" << current_sample; + << "chunk_start_frame" << chunk_start_frame + << "current_frame" << current_frame; break; } // If we're past the start_chunk then current_sample should be // chunk_start_sample. - if (start_chunk != chunk_num && chunk_start_sample != current_sample) { + if (start_chunk != chunk_num && chunk_start_frame != current_frame) { qDebug() << "CachingReader::read() bad chunk parameters" << "chunk_num" << chunk_num << "start_chunk" << start_chunk - << "chunk_start_sample" << chunk_start_sample - << "current_sample" << current_sample; + << "chunk_start_sample" << chunk_start_frame + << "current_sample" << current_frame; break; } - if (samples_remaining < 0) { + if (frames_remaining < 0) { qDebug() << "CachingReader::read() bad samples remaining" - << samples_remaining; + << frames_remaining; break; } // It is completely possible that chunk_remaining_samples is less than // zero. If the caller is trying to read from beyond the end of the // file, then this can happen. We should tolerate it. - if (chunk_remaining_samples < 0) { - chunk_remaining_samples = 0; + if (chunk_remaining_frames < 0) { + chunk_remaining_frames = 0; } - int samples_to_read = math_clamp(samples_remaining, 0, - chunk_remaining_samples); + const int frames_to_read = math_clamp(frames_remaining, 0, chunk_remaining_frames); // If we did not decide to read any samples from this chunk then that // means we have exhausted all the samples in the song. - if (samples_to_read == 0) { + if (frames_to_read == 0) { break; } // samples_to_read should be non-negative and even - if (samples_to_read < 0 || samples_to_read % 2 != 0) { - qDebug() << "CachingReader::read() samples_to_read invalid" - << samples_to_read; + if (frames_to_read < 0) { + qDebug() << "CachingReader::read() frames_to_read invalid" + << frames_to_read; break; } - // TODO(rryan) do a test and see if using memcpy is faster than gcc - // optimizing the for loop - CSAMPLE *data = current->data + chunk_offset; - memcpy(buffer, data, sizeof(*buffer) * samples_to_read); - // for (int i=0; i < samples_to_read; i++) { - // buffer[i] = data[i]; - // } - + const int chunk_sample_offset = chunk_frame_offset * CachingReaderWorker::kChunkChannels; + const int samples_to_read = frames_to_read * CachingReaderWorker::kChunkChannels; + SampleUtil::copy(buffer, current->stereoSamples + chunk_sample_offset, samples_to_read); buffer += samples_to_read; - current_sample += samples_to_read; samples_remaining -= samples_to_read; + current_frame += frames_to_read; + frames_remaining -= frames_to_read; } // If we didn't supply all the samples requested, that probably means we're // at the end of the file, or something is wrong. Provide zeroes and pretend // all is well. The caller can't be bothered to check how long the file is. - // TODO(XXX) memset - for (int i=0; i& hintList) { @@ -465,7 +456,7 @@ void CachingReader::hintAndMaybeWake(const QVector& hintList) { // constant. 2048 is a pretty good number of samples because 25ms // latency corresponds to 1102.5 mono samples and we need double // that for stereo samples. - const int default_samples = 2048; + const int default_samples = 1024 * CachingReaderWorker::kChunkChannels; QSet chunksToFreshen; while (iterator.hasNext()) { @@ -486,12 +477,16 @@ void CachingReader::hintAndMaybeWake(const QVector& hintList) { qDebug() << "ERROR: Negative hint length. Ignoring."; continue; } - int start_sample = math_clamp(hint.sample, 0, - m_iTrackNumSamplesCallbackSafe); - int start_chunk = chunkForSample(start_sample); - int end_sample = math_clamp(hint.sample + hint.length - 1, 0, - m_iTrackNumSamplesCallbackSafe); - int end_chunk = chunkForSample(end_sample); + Q_ASSERT(0 == (hint.sample % CachingReaderWorker::kChunkChannels)); + const int frame = hint.sample / CachingReaderWorker::kChunkChannels; + Q_ASSERT(0 == (hint.length % CachingReaderWorker::kChunkChannels)); + const int frame_count = hint.length / CachingReaderWorker::kChunkChannels; + const int start_frame = math_clamp(frame, 0, + m_iTrackNumFramesCallbackSafe); + const int start_chunk = chunkForFrame(start_frame); + int end_frame = math_clamp(frame + (frame_count - 1), 0, + m_iTrackNumFramesCallbackSafe); + const int end_chunk = chunkForFrame(end_frame); for (int current = start_chunk; current <= end_chunk; ++current) { chunksToFreshen.insert(current); diff --git a/src/cachingreader.h b/src/cachingreader.h index f945240f3d2..d76cdb0ff1f 100644 --- a/src/cachingreader.h +++ b/src/cachingreader.h @@ -54,7 +54,7 @@ class CachingReader : public QObject { // Read num_samples from the SoundSource starting with sample into // buffer. Returns the total number of samples actually written to buffer. - virtual int read(int sample, int num_samples, CSAMPLE* buffer); + virtual int read(int sample, int num_samples, Mixxx::AudioSource::sample_type* buffer); // Issue a list of hints, but check whether any of the hints request a chunk // that is not in the cache. If any hints do request a chunk not in cache, @@ -86,8 +86,8 @@ class CachingReader : public QObject { static Chunk* insertIntoLRUList(Chunk* chunk, Chunk* head); // Given a sample number, return the chunk number corresponding to it. - inline static int chunkForSample(int sample_number) { - return sample_number / CachingReaderWorker::kSamplesPerChunk; + inline static int chunkForFrame(int frame_number) { + return frame_number / CachingReaderWorker::kFramesPerChunk; } const ConfigObject* m_pConfig; @@ -131,9 +131,9 @@ class CachingReader : public QObject { Chunk* m_lruChunk; // The raw memory buffer which is divided up into chunks. - CSAMPLE* m_pRawMemoryBuffer; + Mixxx::AudioSource::sample_type* m_pRawMemoryBuffer; - int m_iTrackNumSamplesCallbackSafe; + int m_iTrackNumFramesCallbackSafe; CachingReaderWorker* m_pWorker; }; diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 52b5d6da1ee..8fe6254b75d 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -7,7 +7,6 @@ #include "cachingreaderworker.h" #include "trackinfoobject.h" #include "soundsourceproxy.h" -#include "sampleutil.h" #include "util/compatibility.h" #include "util/event.h" #include "util/math.h" @@ -22,8 +21,9 @@ // Must be divisible by 8, 4, and 2. Just pick a power of 2. #define CHUNK_LENGTH 65536 -const int CachingReaderWorker::kChunkLength = CHUNK_LENGTH; -const int CachingReaderWorker::kSamplesPerChunk = CHUNK_LENGTH / sizeof(CSAMPLE); +const Mixxx::AudioSource::size_type CachingReaderWorker::kChunkLength = CHUNK_LENGTH; +const Mixxx::AudioSource::size_type CachingReaderWorker::kSamplesPerChunk = CHUNK_LENGTH / sizeof(Mixxx::AudioSource::sample_type); +const Mixxx::AudioSource::size_type CachingReaderWorker::kFramesPerChunk = kSamplesPerChunk / kChunkChannels; CachingReaderWorker::CachingReaderWorker(QString group, @@ -33,14 +33,10 @@ CachingReaderWorker::CachingReaderWorker(QString group, m_tag(QString("CachingReaderWorker %1").arg(m_group)), m_pChunkReadRequestFIFO(pChunkReadRequestFIFO), m_pReaderStatusFIFO(pReaderStatusFIFO), - m_iTrackNumSamples(0), - m_pSample(NULL), m_stop(0) { - m_pSample = new SAMPLE[kSamplesPerChunk]; } CachingReaderWorker::~CachingReaderWorker() { - delete [] m_pSample; } void CachingReaderWorker::processChunkReadRequest(ChunkReadRequest* request, @@ -48,42 +44,38 @@ void CachingReaderWorker::processChunkReadRequest(ChunkReadRequest* request, int chunk_number = request->chunk->chunk_number; //qDebug() << "Processing ChunkReadRequest for" << chunk_number; update->chunk = request->chunk; - update->chunk->length = 0; + update->chunk->frameCount = 0; - if (!m_pCurrentSoundSource || chunk_number < 0) { + if (!m_pAudioSource || chunk_number < 0) { update->status = CHUNK_READ_INVALID; return; } // Stereo samples - int sample_position = sampleForChunk(chunk_number); - int samples_remaining = m_iTrackNumSamples - sample_position; - int samples_to_read = math_min(kSamplesPerChunk, samples_remaining); + Mixxx::AudioSource::size_type frame_position = frameForChunk(chunk_number); + Mixxx::AudioSource::size_type frames_remaining = m_pAudioSource->getFrameCount() - frame_position; + Mixxx::AudioSource::size_type frames_to_read = math_min(kFramesPerChunk, frames_remaining); // Bogus chunk number - if (samples_to_read <= 0) { + if (frames_to_read <= 0) { update->status = CHUNK_READ_EOF; return; } - m_pCurrentSoundSource->seek(sample_position); - int samples_read = m_pCurrentSoundSource->read(samples_to_read, - m_pSample); + frame_position = m_pAudioSource->seekFrame(frame_position); - // If we've run out of music, the SoundSource can return 0 samples. - // Remember that SoundSourc->getLength() (which is m_iTrackNumSamples) can - // lie to us about the length of the song! - if (samples_read <= 0) { + const Mixxx::AudioSource::size_type frames_read = m_pAudioSource->readStereoFrameSamplesInterleaved(frames_to_read, request->chunk->stereoSamples); + + // If we've run out of music, the AudioSource can return 0 frames/samples. + // Remember that AudioSource->getFrameCount() can lie to us about + // the length of the song! + if (frames_read <= 0) { update->status = CHUNK_READ_EOF; return; } - CSAMPLE* buffer = request->chunk->data; - //qDebug() << "Reading into " << buffer; - SampleUtil::convertS16ToFloat32(buffer, m_pSample, samples_read); - update->status = CHUNK_READ_SUCCESS; - update->chunk->length = samples_read; + update->chunk->frameCount = frames_read; } // WARNING: Always called from a different thread (GUI) @@ -123,20 +115,19 @@ void CachingReaderWorker::run() { namespace { - Mixxx::SoundSourcePointer openSoundSourceForReading(const TrackPointer& pTrack) { + Mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::SoundSourcePointer pSoundSource(soundSourceProxy.open()); - if (pSoundSource.isNull()) { + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); + if (pAudioSource.isNull()) { qWarning() << "Failed to open file:" << pTrack->getLocation(); - return Mixxx::SoundSourcePointer(); + return Mixxx::AudioSourcePointer(); } - if (pSoundSource->length() > 0) { - // successfully opened and readable - return pSoundSource; - } else { - qWarning() << "Invalid file:" << pTrack->getLocation(); - return Mixxx::SoundSourcePointer(); + if (pAudioSource->isFrameCountEmpty()) { + qWarning() << "File is empty:" << pTrack->getLocation(); + return Mixxx::AudioSourcePointer(); } + // successfully opened and readable + return pAudioSource; } } @@ -149,10 +140,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { ReaderStatusUpdate status; status.status = TRACK_NOT_LOADED; status.chunk = NULL; - status.trackNumSamples = 0; - - m_pCurrentSoundSource.clear(); - m_iTrackNumSamples = 0; + status.trackFrameCount = 0; QString filename = pTrack->getLocation(); @@ -166,8 +154,8 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { return; } - m_pCurrentSoundSource = openSoundSourceForReading(pTrack); - if (m_pCurrentSoundSource.isNull()) { + m_pAudioSource = openAudioSourceForReading(pTrack); + if (m_pAudioSource.isNull()) { // Must unlock before emitting to avoid deadlock qDebug() << m_group << "CachingReaderWorker::loadTrack() load failed for\"" << filename << "\", file invalid, unlocked reader lock"; @@ -177,7 +165,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { return; } - m_iTrackNumSamples = status.trackNumSamples = m_pCurrentSoundSource->length(); + status.trackFrameCount = m_pAudioSource->getFrameCount(); status.status = TRACK_LOADED; m_pReaderStatusFIFO->writeBlocking(&status, 1); @@ -190,8 +178,8 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { m_pReaderStatusFIFO->writeBlocking(&status, 1); } - // Emit that the track has been loaded - emit(trackLoaded(pTrack, m_pCurrentSoundSource->getSampleRate(), m_iTrackNumSamples)); + // Emit that the track is loaded. + emit(trackLoaded(pTrack, m_pAudioSource->getFrameRate(), m_pAudioSource->getFrameCount() * kChunkChannels)); } void CachingReaderWorker::quitWait() { diff --git a/src/cachingreaderworker.h b/src/cachingreaderworker.h index f2d14620d8a..9cb57c7d6e1 100644 --- a/src/cachingreaderworker.h +++ b/src/cachingreaderworker.h @@ -6,22 +6,23 @@ #include #include #include -#include -#include "soundsource.h" +#include "audiosource.h" #include "trackinfoobject.h" #include "engine/engineworker.h" #include "util/fifo.h" -#include "util/types.h" +// forward declaration(s) +class AudioSourceProxy; + // A Chunk is a section of audio that is being cached. The chunk_number can be // used to figure out the sample number of the first sample in data by using // sampleForChunk() typedef struct Chunk { int chunk_number; - int length; - CSAMPLE* data; + int frameCount; + Mixxx::AudioSource::sample_type* stereoSamples; Chunk* prev_lru; Chunk* next_lru; } Chunk; @@ -44,10 +45,11 @@ enum ReaderStatus { typedef struct ReaderStatusUpdate { ReaderStatus status; Chunk* chunk; - int trackNumSamples; - ReaderStatusUpdate() { - status = INVALID; - chunk = NULL; + int trackFrameCount; + ReaderStatusUpdate() + : status(INVALID) + , chunk(NULL) + , trackFrameCount(0) { } } ReaderStatusUpdate; @@ -71,12 +73,15 @@ class CachingReaderWorker : public EngineWorker { void quitWait(); // A Chunk is a memory-resident section of audio that has been cached. Each - // chunk holds a fixed number of samples given by kSamplesPerChunk. - const static int kChunkLength, kSamplesPerChunk; + // chunk holds a fixed number of stereo frames given by kFramesPerChunk. + static const Mixxx::AudioSource::size_type kChunkLength; + static const Mixxx::AudioSource::size_type kChunkChannels = 2; // stereo + static const Mixxx::AudioSource::size_type kFramesPerChunk; + static const Mixxx::AudioSource::size_type kSamplesPerChunk; // = kFramesPerChunk * kChunkChannels // Given a chunk number, return the start sample number for the chunk. - inline static int sampleForChunk(int chunk_number) { - return chunk_number * kSamplesPerChunk; + static Mixxx::AudioSource::size_type frameForChunk(Mixxx::AudioSource::size_type chunk_number) { + return chunk_number * kFramesPerChunk; } signals: @@ -108,12 +113,9 @@ class CachingReaderWorker : public EngineWorker { void processChunkReadRequest(ChunkReadRequest* request, ReaderStatusUpdate* update); - // The current sound source of the track loaded - Mixxx::SoundSourcePointer m_pCurrentSoundSource; - int m_iTrackNumSamples; + // The current audio source of the track loaded + Mixxx::AudioSourcePointer m_pAudioSource; - // Temporary buffer for reading from SoundSources - SAMPLE* m_pSample; QAtomicInt m_stop; }; diff --git a/src/engine/enginebufferscalest.cpp b/src/engine/enginebufferscalest.cpp index bf904c95ba8..6068d1103e4 100644 --- a/src/engine/enginebufferscalest.cpp +++ b/src/engine/enginebufferscalest.cpp @@ -38,7 +38,6 @@ EngineBufferScaleST::EngineBufferScaleST(ReadAheadManager *pReadAheadManager) m_dTempoOld(1.0), m_pReadAheadManager(pReadAheadManager) { m_pSoundTouch = new soundtouch::SoundTouch(); - m_pSoundTouch->setChannels(2); m_pSoundTouch->setRate(m_dRateOld); m_pSoundTouch->setTempo(m_dTempoOld); m_pSoundTouch->setPitch(1.0); diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index ef611676a7f..1bdd62f9421 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -1,83 +1,108 @@ -#include - -#include - #include "musicbrainz/chromaprinter.h" + #include "soundsourceproxy.h" -ChromaPrinter::ChromaPrinter(QObject* parent) - : QObject(parent) { -} +#include -QString ChromaPrinter::getFingerPrint(TrackPointer pTrack) { - SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::SoundSourcePointer pSoundSource(soundSourceProxy.open()); - if (pSoundSource.isNull()) { - qDebug() << "Skipping invalid file:" << pTrack->getLocation(); - return QString(); - } - if (0 >= pSoundSource->length()) { - qDebug() << "Skipping empty file:" << pTrack->getLocation(); - return QString(); - } - return calcFingerPrint(pSoundSource); -} +#include -QString ChromaPrinter::calcFingerPrint(const Mixxx::SoundSourcePointer& pSoundSource) { - // this is worth 2min of audio, multiply by 2 because we have 2 channels +namespace +{ + // this is worth 2min of audio // AcoustID only stores a fingerprint for the first two minutes of a song // on their server so we need only a fingerprint of the first two minutes // --kain88 July 2012 - unsigned long maxFingerprintSamples = 120 * 2 * pSoundSource->getSampleRate(); - unsigned long numFingerprintSamples = math_min(pSoundSource->length(), maxFingerprintSamples); + const Mixxx::AudioSource::size_type kFingerprintDuration = 120; // in seconds + const Mixxx::AudioSource::size_type kFingerprintChannels = 2; // stereo - SAMPLE *pData = new SAMPLE[numFingerprintSamples]; - QTime timerReadingFile; - timerReadingFile.start(); - unsigned int read = pSoundSource->read(numFingerprintSamples, pData); + QString calcFingerPrint(const Mixxx::AudioSourcePointer& pAudioSource) { - if (read!=numFingerprintSamples) { - qDebug() << "oh that's embarrasing I couldn't read the track"; - return QString(); - } - qDebug("reading file took: %d ms" , timerReadingFile.elapsed()); + Mixxx::AudioSource::size_type numFrames = + kFingerprintDuration * pAudioSource->getFrameRate(); + // check that the song is actually longer then the amount of audio we use + if (numFrames > pAudioSource->getFrameCount()) { + numFrames = pAudioSource->getFrameCount(); + } + + QTime timerReadingFile; + timerReadingFile.start(); + + const Mixxx::AudioSource::size_type numSamples = numFrames * kFingerprintChannels; + Mixxx::AudioSource::sample_type* sampleBuffer = new Mixxx::AudioSource::sample_type[numSamples]; + + const Mixxx::AudioSource::size_type readFrames = pAudioSource->readStereoFrameSamplesInterleaved(numFrames, sampleBuffer); - ChromaprintContext* ctx = chromaprint_new(CHROMAPRINT_ALGORITHM_DEFAULT); - // we have 2 channels in mixxx always - chromaprint_start(ctx, pSoundSource->getSampleRate(), 2); + const Mixxx::AudioSource::size_type readSamples = readFrames * kFingerprintChannels; + SAMPLE *pData = new SAMPLE[readSamples]; - QTime timerGeneratingFingerPrint; - timerGeneratingFingerPrint.start(); - int success = chromaprint_feed(ctx, pData, numFingerprintSamples); - if (!success) { - qDebug() << "could not generate fingerprint"; + for (Mixxx::AudioSource::size_type i = 0; i < readSamples; ++i) { + pData[i] = SAMPLE(sampleBuffer[i] * SAMPLE_MAX); + } + + delete[] sampleBuffer; + + if (readFrames != numFrames) { + qDebug() << "oh that's embarrasing I couldn't read the track"; + delete[] pData; + return QString(); + } + qDebug("reading file took: %d ms" , timerReadingFile.elapsed()); + + ChromaprintContext* ctx = chromaprint_new(CHROMAPRINT_ALGORITHM_DEFAULT); + // we have 2 channels in mixxx always + chromaprint_start(ctx, pAudioSource->getFrameRate(), kFingerprintChannels); + + QTime timerGeneratingFingerPrint; + timerGeneratingFingerPrint.start(); + int success = chromaprint_feed(ctx, pData, readSamples); delete [] pData; - return QString(); - } - chromaprint_finish(ctx); - - void* fprint = NULL; - int size = 0; - int ret = chromaprint_get_raw_fingerprint(ctx, &fprint, &size); - QByteArray fingerprint; - if (ret == 1) { - void* encoded = NULL; - int encoded_size = 0; - chromaprint_encode_fingerprint(fprint, size, - CHROMAPRINT_ALGORITHM_DEFAULT, - &encoded, - &encoded_size, 1); - - fingerprint.append(reinterpret_cast(encoded), encoded_size); - - chromaprint_dealloc(fprint); - chromaprint_dealloc(encoded); + chromaprint_finish(ctx); + if (!success) { + qDebug() << "could not generate fingerprint"; + chromaprint_free(ctx); + return QString(); + } + + void* fprint = NULL; + int size = 0; + int ret = chromaprint_get_raw_fingerprint(ctx, &fprint, &size); + QByteArray fingerprint; + if (ret == 1) { + void* encoded = NULL; + int encoded_size = 0; + chromaprint_encode_fingerprint(fprint, size, + CHROMAPRINT_ALGORITHM_DEFAULT, + &encoded, + &encoded_size, 1); + + fingerprint.append(reinterpret_cast(encoded), encoded_size); + + chromaprint_dealloc(fprint); + chromaprint_dealloc(encoded); + } + chromaprint_free(ctx); + + qDebug("generating fingerprint took: %d ms" , timerGeneratingFingerPrint.elapsed()); + + return fingerprint; } - chromaprint_free(ctx); - delete [] pData; +} - qDebug("generating fingerprint took: %d ms" , timerGeneratingFingerPrint.elapsed()); +ChromaPrinter::ChromaPrinter(QObject* parent) + : QObject(parent) { +} - return fingerprint; +QString ChromaPrinter::getFingerPrint(TrackPointer pTrack) { + SoundSourceProxy soundSourceProxy(pTrack); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); + if (pAudioSource.isNull()) { + qDebug() << "Skipping invalid file:" << pTrack->getLocation(); + return QString(); + } + if (pAudioSource->isFrameCountEmpty()) { + qDebug() << "Skipping empty file:" << pTrack->getLocation(); + return QString(); + } + return calcFingerPrint(pAudioSource); } diff --git a/src/musicbrainz/chromaprinter.h b/src/musicbrainz/chromaprinter.h index 3b1cd1d0d97..6bbfe0115f6 100644 --- a/src/musicbrainz/chromaprinter.h +++ b/src/musicbrainz/chromaprinter.h @@ -3,19 +3,14 @@ #include -#include "soundsource.h" #include "trackinfoobject.h" class ChromaPrinter: public QObject { Q_OBJECT - public: - ChromaPrinter(QObject* parent=NULL); - QString getFingerPrint(TrackPointer pTrack); - - private: - - QString calcFingerPrint(const Mixxx::SoundSourcePointer& pSoundSource); +public: + explicit ChromaPrinter(QObject* parent = NULL); + QString getFingerPrint(TrackPointer pTrack); }; #endif //CHROMAPRINTER_H diff --git a/src/soundsource.cpp b/src/soundsource.cpp index 120d41af893..8a0e021dca9 100644 --- a/src/soundsource.cpp +++ b/src/soundsource.cpp @@ -1,63 +1,58 @@ /*************************************************************************** - soundsource.cpp - description - ------------------- - begin : Wed Feb 20 2002 - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : -***************************************************************************/ + soundsource.cpp - description + ------------------- + begin : Wed Feb 20 2002 + copyright : (C) 2002 by Tue and Ken Haste Andersen + email : + ***************************************************************************/ /*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -***************************************************************************/ - -#include - + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ #include "soundsource.h" -#include "util/math.h" -namespace Mixxx -{ +namespace Mixxx { -namespace -{ - const float BPM_ZERO = 0.0f; - const float BPM_MAX = 300.0f; +namespace { +const float BPM_ZERO = 0.0f; +const float BPM_MAX = 300.0f; - float parseBpmString(const QString& sBpm) { - float bpm = sBpm.toFloat(); - while(bpm > BPM_MAX) { - bpm /= 10.0f; - } - return bpm; +float parseBpmString(const QString& sBpm) { + float bpm = sBpm.toFloat(); + while (bpm > BPM_MAX) { + bpm /= 10.0f; } + return bpm; +} - float parseReplayGainString(QString sReplayGain) { - QString ReplayGainstring = sReplayGain.remove(" dB"); - float fReplayGain = db2ratio(ReplayGainstring.toFloat()); - // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. - // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by setting value to 0 (i.e. rescan via analyserrg) - if (fReplayGain == 1.0f) { - fReplayGain = 0.0f; - } - return fReplayGain; +float parseReplayGainString(QString sReplayGain) { + QString ReplayGainstring = sReplayGain.remove(" dB"); + float fReplayGain = db2ratio(ReplayGainstring.toFloat()); + // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. + // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by setting value to 0 (i.e. rescan via analyserrg) + if (fReplayGain == 1.0f) { + fReplayGain = 0.0f; } + return fReplayGain; +} + +} +SoundSource::SoundSource(QString sFilename) + : m_sFilename(sFilename), m_sType( + m_sFilename.section(".", -1).toLower()), m_fReplayGain(0.0f), m_fBpm( + BPM_ZERO), m_iChannels(0), m_iSampleRate(0), m_iBitrate(0), m_iDuration(0) { } -SoundSource::SoundSource(QString qFilename) - : m_qFilename(qFilename), - m_iChannels(0), - m_iSampleRate(0), - m_fReplayGain(0.0f), - m_fBpm(BPM_ZERO), - m_iBitrate(0), - m_iDuration(0) { +SoundSource::SoundSource(QString sFilename, QString sType) + : m_sFilename(sFilename), m_sType(sType), m_fReplayGain(0.0f), m_fBpm( + BPM_ZERO), m_iChannels(0), m_iSampleRate(0), m_iBitrate(0), m_iDuration(0) { } SoundSource::~SoundSource() { @@ -76,4 +71,34 @@ void SoundSource::setReplayGainString(QString sReplayGain) { setReplayGain(parseReplayGainString(sReplayGain)); } +int SoundSource::getChannels() const { + if (isChannelCountValid()) { + // from AudioSource + return getChannelCount(); + } else { + // from metadata + return m_iChannels; + } +} + +int SoundSource::getSampleRate() const { + if (isFrameRateValid()) { + // from AudioSource + return getFrameRate(); + } else { + // from metadata + return m_iSampleRate; + } +} + +int SoundSource::getDuration() const { + if (isFrameRateValid() && !isFrameCountEmpty()) { + // from AudioSource + return getFrameCount() / getFrameRate(); + } else { + // from metadata + return m_iDuration; + } +} + } //namespace Mixxx diff --git a/src/soundsource.h b/src/soundsource.h index 4518a4d1757..895691be099 100644 --- a/src/soundsource.h +++ b/src/soundsource.h @@ -1,9 +1,9 @@ /*************************************************************************** - soundsource.h - description - ------------------- - begin : Wed Feb 20 2002 - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : + soundsource.h - description + ------------------- + begin : Wed Feb 20 2002 + copyright : (C) 2002 by Tue and Ken Haste Andersen + email : ***************************************************************************/ /*************************************************************************** @@ -18,59 +18,61 @@ #ifndef SOUNDSOURCE_H #define SOUNDSOURCE_H -#include "util/types.h" +#define MIXXX_SOUNDSOURCE_API_VERSION 7 +/** @note SoundSource API Version history: + 1 - Mixxx 1.8.0 Beta 2 + 2 - Mixxx 1.9.0 Pre (added key code) + 3 - Mixxx 1.10.0 Pre (added freeing function for extensions) + 4 - Mixxx 1.11.0 Pre (added composer field to SoundSource) + 5 - Mixxx 1.12.0 Pre (added album artist and grouping fields to SoundSource) + 6 - Mixxx 1.12.0 Pre (added cover art suppport) + 7 - Mixxx 1.13.0 New AudioSource API + */ + +#include "audiosource.h" #include "util/defs.h" #include #include -#include - -#define MIXXX_SOUNDSOURCE_API_VERSION 6 -/** @note SoundSource API Version history: - 1 - Mixxx 1.8.0 Beta 2 - 2 - Mixxx 1.9.0 Pre (added key code) - 3 - Mixxx 1.10.0 Pre (added freeing function for extensions) - 4 - Mixxx 1.11.0 Pre (added composer field to SoundSource) - 5 - Mixxx 1.12.0 Pre (added album artist and grouping fields to SoundSource) - 6 - Mixxx 1.13.0 (added cover art suppport) - */ /** Getter function to be declared by all SoundSource plugins */ namespace Mixxx { - class SoundSource; +class SoundSource; } + typedef Mixxx::SoundSource* (*getSoundSourceFunc)(QString filename); typedef char** (*getSupportedFileExtensionsFunc)(); typedef int (*getSoundSourceAPIVersionFunc)(); /* New in version 3 */ typedef void (*freeFileExtensionsFunc)(char** exts); +namespace Mixxx { /* - Base class for sound sources. -*/ -namespace Mixxx -{ -class SoundSource -{ + Base class for sound sources. + */ +class SoundSource: public AudioSource { public: virtual ~SoundSource(); - virtual Result open() = 0; - virtual long seek(long) = 0; - virtual unsigned read(unsigned long size, const SAMPLE*) = 0; - virtual long unsigned length() = 0; + inline const QString& getFilename() const { + return m_sFilename; + } + inline const QString& getType() const { + return m_sType; + } + + // Parses metadata before opening the SoundSource for reading. + // + // Only metadata that is quickly readable should be read. + // The implementation is free to set inaccurate estimated + // values here that are overwritten when the AudioSource is + // actually opened for reading. virtual Result parseHeader() = 0; // Returns the first cover art image embedded within the file (if any). virtual QImage parseCoverArt() = 0; - inline const QString& getType() const { - return m_sType; - } - inline const QString& getFilename() const { - return m_qFilename; - } inline const QString& getArtist() const { return m_sArtist; } @@ -113,9 +115,9 @@ class SoundSource inline int getBitrate() const { return m_iBitrate; } - inline int getDuration() const { - return m_iDuration; - } + int getChannels() const; + int getSampleRate() const; + int getDuration() const; inline void setArtist(QString artist) { m_sArtist = artist; @@ -158,11 +160,10 @@ class SoundSource m_fReplayGain = replayGain; } void setReplayGainString(QString sReplayGain); - inline void setChannels(int channels) { m_iChannels = channels; } - inline void setSampleRate(unsigned int sampleRate) { + inline void setSampleRate(int sampleRate) { m_iSampleRate = sampleRate; } inline void setBitrate(int bitrate) { @@ -172,24 +173,22 @@ class SoundSource m_iDuration = duration; } - inline int getChannels() const { - return m_iChannels; - } - inline unsigned int getSampleRate() const { - return m_iSampleRate; - } + /** + * Opens the SoundSource for reading audio data. + * + * When opening a SoundSource the corresponding + * AudioSource must be initialized. + */ + virtual Result open() = 0; protected: - explicit SoundSource(QString qFilename); - - inline void setType(QString type) { - m_sType = type; - } + explicit SoundSource(QString sFilename); + SoundSource(QString sFilename, QString sType); private: - const QString m_qFilename; + const QString m_sFilename; + const QString m_sType; - QString m_sType; QString m_sArtist; QString m_sTitle; QString m_sAlbum; @@ -205,12 +204,14 @@ class SoundSource // The following members need to be initialized // explicitly in the constructor! Otherwise their // value is undefined. - int m_iChannels; - unsigned int m_iSampleRate; float m_fReplayGain; - float m_fBpm; - int m_iBitrate; - int m_iDuration; + float m_fBpm; // beats / minute + + // Audio properties (from metadata) + int m_iChannels; // #channels + int m_iSampleRate; // Hz + int m_iBitrate; // kbit / s + int m_iDuration; // #seconds }; typedef QSharedPointer SoundSourcePointer; diff --git a/src/soundsourcecoreaudio.cpp b/src/soundsourcecoreaudio.cpp index 872a4e740bd..5f3988183f6 100644 --- a/src/soundsourcecoreaudio.cpp +++ b/src/soundsourcecoreaudio.cpp @@ -21,10 +21,14 @@ #include "soundsourcetaglib.h" #include "util/math.h" +namespace +{ + Mixxx::AudioSource::size_type kChannelCount = 2; +} + SoundSourceCoreAudio::SoundSourceCoreAudio(QString filename) - : SoundSource(filename), - m_samples(0), - m_headerFrames(0) { + : Super(filename) + , m_headerFrames(0) { } SoundSourceCoreAudio::~SoundSourceCoreAudio() { @@ -58,23 +62,17 @@ Result SoundSourceCoreAudio::open() { } // get the input file format - CAStreamBasicDescription inputFormat; - UInt32 size = sizeof(inputFormat); - m_inputFormat = inputFormat; - err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileDataFormat, &size, &inputFormat); + UInt32 inputFormatSize = sizeof(m_inputFormat); + err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileDataFormat, &inputFormatSize, &m_inputFormat); if (err != noErr) { qDebug() << "SSCA: Error getting file format (" << getFilename() << ")"; return ERR; } - //Debugging: - //printf ("Source File format: "); inputFormat.Print(); - //printf ("Dest File format: "); outputFormat.Print(); - // create the output format m_outputFormat = CAStreamBasicDescription( - inputFormat.mSampleRate, 2, - CAStreamBasicDescription::kPCMFormatInt16, true); + m_inputFormat.mSampleRate, kChannelCount, + CAStreamBasicDescription::kPCMFormatFloat32, true); // set the client format err = ExtAudioFileSetProperty(m_audioFile, kExtAudioFileProperty_ClientDataFormat, @@ -84,13 +82,10 @@ Result SoundSourceCoreAudio::open() { return ERR; } - setChannels(m_outputFormat.NumberChannels()); - //get the total length in frames of the audio file - copypasta: http://discussions.apple.com/thread.jspa?threadID=2364583&tstart=47 - UInt32 dataSize; - SInt64 totalFrameCount; - dataSize = sizeof(totalFrameCount); //XXX: This looks sketchy to me - Albert - err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileLengthFrames, &dataSize, &totalFrameCount); + SInt64 totalFrameCount; + UInt32 totalFrameCountSize = sizeof(totalFrameCount); + err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileLengthFrames, &totalFrameCountSize, &totalFrameCount); if (err != noErr) { qDebug() << "SSCA: Error getting number of frames"; return ERR; @@ -101,92 +96,68 @@ Result SoundSourceCoreAudio::open() { // AudioConverterRef acRef; - UInt32 acrsize=sizeof(AudioConverterRef); + UInt32 acrsize = sizeof(AudioConverterRef); err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_AudioConverter, &acrsize, &acRef); //_ThrowExceptionIfErr(@"kExtAudioFileProperty_AudioConverter", err); AudioConverterPrimeInfo primeInfo; - UInt32 piSize=sizeof(AudioConverterPrimeInfo); + UInt32 piSize = sizeof(AudioConverterPrimeInfo); memset(&primeInfo, 0, piSize); err = AudioConverterGetProperty(acRef, kAudioConverterPrimeInfo, &piSize, &primeInfo); if (err != kAudioConverterErr_PropertyNotSupported) { // Only if decompressing //_ThrowExceptionIfErr(@"kAudioConverterPrimeInfo", err); - m_headerFrames=primeInfo.leadingFrames; + m_headerFrames = primeInfo.leadingFrames; } - m_samples = (totalFrameCount/* - m_headerFrames*/) * getChannels(); - setDuration(m_samples / (inputFormat.mSampleRate * getChannels())); - setSampleRate(inputFormat.mSampleRate); - qDebug() << m_samples << totalFrameCount << getChannels(); + setChannelCount(m_outputFormat.NumberChannels()); + setFrameRate(m_inputFormat.mSampleRate); + setFrameCount(totalFrameCount/* - m_headerFrames*/); //Seek to position 0, which forces us to skip over all the header frames. //This makes sure we're ready to just let the Analyser rip and it'll //get the number of samples it expects (ie. no header frames). - seek(0); + seekFrame(0); return OK; } -long SoundSourceCoreAudio::seek(long filepos) { - // important division here, filepos is in audio samples (i.e. shorts) - // but libflac expects a number in time samples. I _think_ this should - // be hard-coded at two because *2 is the assumption the caller makes - // -- bkgood - OSStatus err = noErr; - SInt64 segmentStart = filepos / 2; - - err = ExtAudioFileSeek(m_audioFile, (SInt64)segmentStart+m_headerFrames); +Mixxx::AudioSource::diff_type SoundSourceCoreAudio::seekFrame(diff_type frameIndex) { + OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); //_ThrowExceptionIfErr(@"ExtAudioFileSeek", err); - //qDebug() << "SSCA: Seeking to" << segmentStart; - - //err = ExtAudioFileSeek(m_audioFile, filepos / 2); + //qDebug() << "SSCA: Seeking to" << frameIndex; if (err != noErr) { - qDebug() << "SSCA: Error seeking to" << filepos << " (file " << getFilename() << ")";// << GetMacOSStatusErrorString(err) << GetMacOSStatusCommentString(err); + qDebug() << "SSCA: Error seeking to" << frameIndex << " (file " << getFilename() << ")";// << GetMacOSStatusErrorString(err) << GetMacOSStatusCommentString(err); } - return filepos; + return frameIndex; } -unsigned int SoundSourceCoreAudio::read(unsigned long size, const SAMPLE *destination) { +Mixxx::AudioSource::size_type SoundSourceCoreAudio::readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) { //if (!m_decoder) return 0; - OSStatus err; - SAMPLE *destBuffer(const_cast(destination)); - UInt32 numFrames = 0;//(size / 2); /// m_outputFormat.mBytesPerFrame); - unsigned int totalFramesToRead = size/2; - unsigned int numFramesRead = 0; - unsigned int numFramesToRead = totalFramesToRead; + size_type numFramesRead = 0; - while (numFramesRead < totalFramesToRead) { //FIXME: Hardcoded 2 - numFramesToRead = totalFramesToRead - numFramesRead; + while (numFramesRead < frameCount) { + size_type numFramesToRead = frameCount - numFramesRead; AudioBufferList fillBufList; - fillBufList.mNumberBuffers = 1; //Decode a single track? - fillBufList.mBuffers[0].mNumberChannels = m_outputFormat.mChannelsPerFrame; - fillBufList.mBuffers[0].mDataByteSize = math_min(1024, numFramesToRead*4);//numFramesToRead*sizeof(*destBuffer); // 2 = num bytes per SAMPLE - fillBufList.mBuffers[0].mData = (void*)(&destBuffer[numFramesRead*2]); - - // client format is always linear PCM - so here we determine how many frames of lpcm - // we can read/write given our buffer size - numFrames = numFramesToRead; //This silly variable acts as both a parameter and return value. - err = ExtAudioFileRead (m_audioFile, &numFrames, &fillBufList); - //The actual number of frames read also comes back in numFrames. - //(It's both a parameter to a function and a return value. wat apple?) - //XThrowIfError (err, "ExtAudioFileRead"); - if (!numFrames) { - // this is our termination condition + fillBufList.mNumberBuffers = 1; + fillBufList.mBuffers[0].mNumberChannels = getChannelCount(); + fillBufList.mBuffers[0].mDataByteSize = frames2samples(numFramesToRead) * sizeof(sampleBuffer[0]); + fillBufList.mBuffers[0].mData = sampleBuffer + frames2samples(numFramesRead); + + UInt32 numFramesToReadInOut = numFramesToRead; // input/output parameter + OSStatus err = ExtAudioFileRead(m_audioFile, &numFramesToReadInOut, &fillBufList); + if (0 == numFramesToReadInOut) { + // EOF reached break; } - numFramesRead += numFrames; + numFramesRead += numFramesToReadInOut; } - return numFramesRead*2; -} - -inline unsigned long SoundSourceCoreAudio::length() { - return m_samples; + return numFramesRead; } Result SoundSourceCoreAudio::parseHeader() { - if (getFilename().endsWith(".m4a")) { - setType("m4a"); + if (getType() == "m4a") { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); if (!readFileHeader(this, f)) { return ERR; @@ -203,8 +174,7 @@ Result SoundSourceCoreAudio::parseHeader() { return ERR; } } - } else if (getFilename().endsWith(".mp3")) { - setType("mp3"); + } else if (getType() == "mp3") { TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); if (!readFileHeader(this, f)) { return ERR; @@ -226,8 +196,7 @@ Result SoundSourceCoreAudio::parseHeader() { } } } - } else if (getFilename().endsWith(".mp2")) { - setType("mp2"); + } else if (getType() == "mp2") { //TODO: MP2 metadata. Does anyone use mp2 files anymore? // Feels like 1995 again... return ERR; @@ -238,8 +207,7 @@ Result SoundSourceCoreAudio::parseHeader() { QImage SoundSourceCoreAudio::parseCoverArt() { QImage coverArt; - if (getFilename().endsWith(".m4a")) { - setType("m4a"); + if (getType() == "m4a") { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { @@ -247,8 +215,7 @@ QImage SoundSourceCoreAudio::parseCoverArt() { } else { return QImage(); } - } else if (getFilename().endsWith(".mp3")) { - setType("mp3"); + } else if (getType() == "mp3") { TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { diff --git a/src/soundsourcecoreaudio.h b/src/soundsourcecoreaudio.h index d4a6ce78f6f..daef90e3933 100644 --- a/src/soundsourcecoreaudio.h +++ b/src/soundsourcecoreaudio.h @@ -37,27 +37,33 @@ #include -#include "util/types.h" -#include "util/defs.h" #include "soundsource.h" +#include "util/defs.h" + class SoundSourceCoreAudio : public Mixxx::SoundSource { + typedef SoundSource Super; + public: + static QList supportedFileExtensions(); + explicit SoundSourceCoreAudio(QString filename); ~SoundSourceCoreAudio(); - Result open(); - long seek(long filepos); - unsigned read(unsigned long size, const SAMPLE *buffer); - inline long unsigned length(); + Result parseHeader(); QImage parseCoverArt(); - static QList supportedFileExtensions(); + + Result open(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; + private: - unsigned int m_samples; // total number of samples - SInt64 m_headerFrames; ExtAudioFileRef m_audioFile; CAStreamBasicDescription m_inputFormat; CAStreamBasicDescription m_outputFormat; + SInt64 m_headerFrames; }; diff --git a/src/soundsourceffmpeg.cpp b/src/soundsourceffmpeg.cpp index 0837e51fe13..7ca9f83726e 100644 --- a/src/soundsourceffmpeg.cpp +++ b/src/soundsourceffmpeg.cpp @@ -21,40 +21,34 @@ * * ***************************************************************************/ -#include "trackinfoobject.h" #include "soundsourceffmpeg.h" #include #include -static QMutex ffmpegmutex; +#include #define SOUNDSOURCEFFMPEG_CACHESIZE 1000 #define SOUNDSOURCEFFMPEG_POSDISTANCE ((1024 * 1000) / 8) SoundSourceFFmpeg::SoundSourceFFmpeg(QString filename) - : SoundSource(filename), - m_iAudioStream(-1), - m_filelength(-1), - m_pFormatCtx(NULL), - m_pIformat(NULL), - m_pCodecCtx(NULL), - m_pCodec(NULL), - m_pResample(NULL), - m_iCurrentMixxTs(0), - m_bIsSeeked(false), - m_lCacheBytePos(0), - m_lCacheStartByte(0), - m_lCacheEndByte(0), - m_lCacheLastPos(0), - m_lLastStoredPos(0), - m_lStoredSeekPoint(-1) { - m_SCache.clear(); - setType(filename.section(".",-1).toLower()); + : Mixxx::SoundSource(filename) + , m_pFormatCtx(NULL) + , m_iAudioStream(-1) + , m_pCodecCtx(NULL) + , m_pCodec(NULL) + , m_pResample(NULL) + , m_iCurrentMixxTs(0) + , m_bIsSeeked(false) + , m_lCacheBytePos(0) + , m_lCacheStartByte(0) + , m_lCacheEndByte(0) + , m_lCacheLastPos(0) + , m_lLastStoredPos(0) + , m_lStoredSeekPoint(-1) { } SoundSourceFFmpeg::~SoundSourceFFmpeg() { - struct ffmpegLocationObject *l_SRmJmp = NULL; clearCache(); if (m_pCodecCtx != NULL) { @@ -70,31 +64,10 @@ SoundSourceFFmpeg::~SoundSourceFFmpeg() { } while (m_SJumpPoints.size() > 0) { - l_SRmJmp = m_SJumpPoints[0]; + ffmpegLocationObject* l_SRmJmp = m_SJumpPoints[0]; m_SJumpPoints.remove(0); free(l_SRmJmp); } - -} - -AVCodecContext *SoundSourceFFmpeg::getCodecContext() { - return m_pCodecCtx; -} - -AVFormatContext *SoundSourceFFmpeg::getFormatContext() { - return m_pFormatCtx; -} - -int SoundSourceFFmpeg::getAudioStreamIndex() { - return m_iAudioStream; -} - -void SoundSourceFFmpeg::lock() { - ffmpegmutex.lock(); -} - -void SoundSourceFFmpeg::unlock() { - ffmpegmutex.unlock(); } bool SoundSourceFFmpeg::clearCache() { @@ -431,22 +404,23 @@ Result SoundSourceFFmpeg::open() { m_pResample = new EncoderFfmpegResample(m_pCodecCtx); m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_S16); - this->setChannels(m_pCodecCtx->channels); - this->setSampleRate(m_pCodecCtx->sample_rate); + this->setChannelCount(m_pCodecCtx->channels); + this->setFrameRate(m_pCodecCtx->sample_rate); + this->setFrameCount((m_pFormatCtx->duration * m_pCodecCtx->sample_rate) / AV_TIME_BASE); - qDebug() << "ffmpeg: Samplerate: " << this->getSampleRate() << ", Channels: " << - this->getChannels() << "\n"; - if (this->getChannels() > 2) { + qDebug() << "ffmpeg: Samplerate: " << this->getFrameRate() << ", Channels: " << + this->getChannelCount() << "\n"; + if (this->getChannelCount() > 2) { qDebug() << "ffmpeg: No support for more than 2 channels!"; return ERR; } - m_filelength = (long int) ((double)m_pFormatCtx->duration * 2 / AV_TIME_BASE * - this->getSampleRate()); return OK; } -long SoundSourceFFmpeg::seek(long filepos) { +Mixxx::AudioSource::diff_type SoundSourceFFmpeg::seekFrame(diff_type frameIndex) { + const diff_type filepos = frames2samples(frameIndex); + int ret = 0; qint64 i = 0; @@ -502,16 +476,15 @@ long SoundSourceFFmpeg::seek(long filepos) { m_bIsSeeked = TRUE; - return filepos; + return frameIndex; } -unsigned int SoundSourceFFmpeg::read(unsigned long size, - const SAMPLE * destination) { +unsigned int SoundSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { if (m_SCache.size() == 0) { // Make sure we allways start at begining and cache have some // material that we can consume. - seek(0); + seekFrame(0); m_bIsSeeked = FALSE; } @@ -530,6 +503,21 @@ unsigned int SoundSourceFFmpeg::read(unsigned long size, } +Mixxx::AudioSource::size_type SoundSourceFFmpeg::readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) { + // This is just a hack that simply reuses existing + // functionality. Sample data should be resampled + // directly into AV_SAMPLE_FMT_FLT instead of + // AV_SAMPLE_FMT_S16! + typedef std::vector TempBuffer; + TempBuffer tempBuffer(frames2samples(frameCount)); + const size_type readSamples = read(tempBuffer.size(), &tempBuffer[0]); + for (size_type i = 0; i < readSamples; ++i) { + sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / sample_type(SAMPLE_MAX); + } + return samples2frames(readSamples); +} + Result SoundSourceFFmpeg::parseHeader() { qDebug() << "ffmpeg: SoundSourceFFmpeg::parseHeader" << getFilename(); QByteArray qBAFilename = getFilename().toLocal8Bit(); @@ -630,11 +618,10 @@ Result SoundSourceFFmpeg::parseHeader() { } - this->setType(getFilename().section(".",-1).toLower()); - this->setDuration(FmtCtx->duration / AV_TIME_BASE); - this->setBitrate((int)(CodecCtx->bit_rate / 1000)); - this->setSampleRate(CodecCtx->sample_rate); this->setChannels(CodecCtx->channels); + this->setSampleRate(CodecCtx->sample_rate); + this->setBitrate(CodecCtx->bit_rate / 1000); + this->setDuration(FmtCtx->duration / AV_TIME_BASE); avcodec_close(CodecCtx); avformat_close_input(&FmtCtx); @@ -644,13 +631,10 @@ Result SoundSourceFFmpeg::parseHeader() { } QImage SoundSourceFFmpeg::parseCoverArt() { + // currently not implemented return QImage(); } -inline long unsigned SoundSourceFFmpeg::length() { - return m_filelength; -} - QList SoundSourceFFmpeg::supportedFileExtensions() { QList list; AVInputFormat *l_SInputFmt = NULL; diff --git a/src/soundsourceffmpeg.h b/src/soundsourceffmpeg.h index 0a381acfd9e..849eac7c664 100644 --- a/src/soundsourceffmpeg.h +++ b/src/soundsourceffmpeg.h @@ -64,36 +64,33 @@ struct ffmpegCacheObject { }; class SoundSourceFFmpeg : public Mixxx::SoundSource { + typedef SoundSource Super; + public: + static QList supportedFileExtensions(); + explicit SoundSourceFFmpeg(QString qFilename); ~SoundSourceFFmpeg(); - Result open(); - long seek(long); - unsigned int read(unsigned long size, const SAMPLE*); - Result parseHeader(); - QImage parseCoverArt(); - inline long unsigned length(); - bool readInput(); - static QList supportedFileExtensions(); - AVCodecContext *getCodecContext(); - AVFormatContext *getFormatContext(); - int getAudioStreamIndex(); + Result parseHeader() /*override*/; + QImage parseCoverArt() /*override*/; + + Result open() /*override*/; -protected: - void lock(); - void unlock(); + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; +private: bool readFramesToCache(unsigned int count, qint64 offset); bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); quint64 getSizeofCache(); bool clearCache(); -private: - int m_iAudioStream; - quint64 m_filelength; + unsigned int read(unsigned long size, SAMPLE*); + AVFormatContext *m_pFormatCtx; - AVInputFormat *m_pIformat; + int m_iAudioStream; AVCodecContext *m_pCodecCtx; AVCodec *m_pCodec; diff --git a/src/soundsourceflac.cpp b/src/soundsourceflac.cpp index ea235918fe6..3e4380523eb 100644 --- a/src/soundsourceflac.cpp +++ b/src/soundsourceflac.cpp @@ -15,47 +15,31 @@ #include "soundsourceflac.h" #include "soundsourcetaglib.h" +#include "sampleutil.h" +#include "util/math.h" #include #include -#include // memcpy - SoundSourceFLAC::SoundSourceFLAC(QString filename) - : Mixxx::SoundSource(filename) - , m_file(filename) - , m_decoder(NULL) - , m_samples(0) - , m_bps(0) - , m_minBlocksize(0) - , m_maxBlocksize(0) - , m_minFramesize(0) - , m_maxFramesize(0) - , m_flacBuffer(NULL) - , m_flacBufferLength(0) - , m_leftoverBuffer(NULL) - , m_leftoverBufferLength(0) { + : Super(filename, "flac"), m_file(filename), m_decoder(NULL), m_minBlocksize( + 0), m_maxBlocksize(0), m_minFramesize(0), m_maxFramesize(0), m_sampleScale( + kSampleValueZero), m_decodeSampleBufferReadOffset(0), m_decodeSampleBufferWriteOffset( + 0) { } SoundSourceFLAC::~SoundSourceFLAC() { - if (m_flacBuffer != NULL) { - delete [] m_flacBuffer; - m_flacBuffer = NULL; - } - if (m_leftoverBuffer != NULL) { - delete [] m_leftoverBuffer; - m_leftoverBuffer = NULL; - } - if (m_decoder) { - FLAC__stream_decoder_finish(m_decoder); - FLAC__stream_decoder_delete(m_decoder); // frees memory - m_decoder = NULL; - } + close(); } // soundsource overrides Result SoundSourceFLAC::open() { + if (NULL != m_decoder) { + qWarning() << "SSFLAC: Already open!"; + return ERR; + } + if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "SSFLAC: Could not read file!"; return ERR; @@ -64,93 +48,121 @@ Result SoundSourceFLAC::open() { m_decoder = FLAC__stream_decoder_new(); if (m_decoder == NULL) { qWarning() << "SSFLAC: decoder allocation failed!"; + close(); return ERR; } - FLAC__StreamDecoderInitStatus initStatus( - FLAC__stream_decoder_init_stream( - m_decoder, FLAC_read_cb, FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, - FLAC_eof_cb, FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, - (void*) this)); + FLAC__stream_decoder_set_md5_checking(m_decoder, FALSE); + const FLAC__StreamDecoderInitStatus initStatus( + FLAC__stream_decoder_init_stream(m_decoder, FLAC_read_cb, + FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, + FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { qWarning() << "SSFLAC: decoder init failed!"; - goto decoderError; + close(); + return ERR; } if (!FLAC__stream_decoder_process_until_end_of_metadata(m_decoder)) { qWarning() << "SSFLAC: process to end of meta failed!"; - qWarning() << "SSFLAC: decoder state: " << FLAC__stream_decoder_get_state(m_decoder); - goto decoderError; - } // now number of samples etc. should be populated - if (m_flacBuffer == NULL) { - // we want 2 samples per frame, see ::flacWrite code -- bkgood - m_flacBuffer = new FLAC__int16[m_maxBlocksize * 2 /*m_iChannels*/]; - } - if (m_leftoverBuffer == NULL) { - m_leftoverBuffer = new FLAC__int16[m_maxBlocksize * 2 /*m_iChannels*/]; + qWarning() << "SSFLAC: decoder state: " + << FLAC__stream_decoder_get_state(m_decoder); + close(); + return ERR; } -// qDebug() << "SSFLAC: Total samples: " << m_samples; -// qDebug() << "SSFLAC: Sampling rate: " << m_iSampleRate << " Hz"; -// qDebug() << "SSFLAC: Channels: " << m_iChannels; -// qDebug() << "SSFLAC: BPS: " << m_bps; + return OK; -decoderError: - FLAC__stream_decoder_finish(m_decoder); - FLAC__stream_decoder_delete(m_decoder); - m_decoder = NULL; +} - qWarning() << "SSFLAC: Decoder error at file" << getFilename(); - return ERR; +void SoundSourceFLAC::close() { + if (m_decoder) { + FLAC__stream_decoder_finish(m_decoder); + FLAC__stream_decoder_delete(m_decoder); // frees memory + m_decoder = NULL; + } + m_decodeSampleBuffer.clear(); + m_file.close(); + Super::reset(); } -long SoundSourceFLAC::seek(long filepos) { - if (!m_decoder) return 0; - // important division here, filepos is in audio samples (i.e. shorts) - // but libflac expects a number in time samples. I _think_ this should - // be hard-coded at two because *2 is the assumption the caller makes - // -- bkgood - bool result = FLAC__stream_decoder_seek_absolute(m_decoder, filepos / 2); - if (!result) +Mixxx::AudioSource::diff_type SoundSourceFLAC::seekFrame(diff_type frameIndex) { + // clear decode buffer before seeking + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + bool result = FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex); + if (!result) { qWarning() << "SSFLAC: Seeking error at file" << getFilename(); - m_leftoverBufferLength = 0; // clear internal buffer since we moved - return filepos; + } + return frameIndex; +} + +Mixxx::AudioSource::size_type SoundSourceFLAC::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); } -unsigned int SoundSourceFLAC::read(unsigned long size, const SAMPLE *destination) { - if (!m_decoder) return 0; - SAMPLE *destBuffer(const_cast(destination)); - unsigned int samplesWritten = 0; - unsigned int i = 0; - while (samplesWritten < size) { +Mixxx::AudioSource::size_type SoundSourceFLAC::readStereoFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); +} + +Mixxx::AudioSource::size_type SoundSourceFLAC::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer, + bool readStereoSamples) { + sample_type* outBuffer = sampleBuffer; + size_type framesRemaining = frameCount; + while (0 < framesRemaining) { + Q_ASSERT( + m_decodeSampleBufferReadOffset + <= m_decodeSampleBufferWriteOffset); // if our buffer from libflac is empty (either because we explicitly cleared // it or because we've simply used all the samples), ask for a new buffer - if (m_flacBufferLength == 0) { - i = 0; - if (!FLAC__stream_decoder_process_single(m_decoder)) { - qWarning() << "SSFLAC: decoder_process_single returned false (" << getFilename() << ")"; - break; - } else if (m_flacBufferLength == 0) { - // EOF + if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + if (FLAC__stream_decoder_process_single(m_decoder)) { + if (m_decodeSampleBufferReadOffset + >= m_decodeSampleBufferWriteOffset) { + // EOF + break; + } + } else { + qWarning() << "SSFLAC: decoder_process_single returned false (" + << getFilename() << ")"; break; } } - destBuffer[samplesWritten++] = m_flacBuffer[i++]; - --m_flacBufferLength; - } - if (m_flacBufferLength != 0) { - memcpy(m_leftoverBuffer, &m_flacBuffer[i], - m_flacBufferLength * sizeof(m_flacBuffer[0])); // safe because leftoverBuffer - // is as long as flacbuffer - memcpy(m_flacBuffer, m_leftoverBuffer, - m_flacBufferLength * sizeof(m_leftoverBuffer[0])); - // this whole if block could go away if this just used a ring buffer but I'd - // rather do that after I've gotten off the inital happiness of getting this right, - // if I see SIGSEGV one more time I'll pop -- bkgood + Q_ASSERT( + m_decodeSampleBufferReadOffset + <= m_decodeSampleBufferWriteOffset); + const size_type decodeBufferSamples = m_decodeSampleBufferWriteOffset + - m_decodeSampleBufferReadOffset; + const size_type decodeBufferFrames = samples2frames( + decodeBufferSamples); + const size_type framesToCopy = math_min(framesRemaining, decodeBufferFrames); + const size_type samplesToCopy = frames2samples(framesToCopy); + if (readStereoSamples && !isChannelCountStereo()) { + if (isChannelCountMono()) { + SampleUtil::copyMonoToDualMono(outBuffer, + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy); + } else { + SampleUtil::copyMultiToStereo(outBuffer, + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy, getChannelCount()); + } + outBuffer += framesToCopy * 2; // copied 2 samples per frame + } else { + SampleUtil::copy(outBuffer, + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + samplesToCopy); + outBuffer += samplesToCopy; + } + m_decodeSampleBufferReadOffset += samplesToCopy; + framesRemaining -= framesToCopy; + Q_ASSERT( + m_decodeSampleBufferReadOffset + <= m_decodeSampleBufferWriteOffset); } - return samplesWritten; -} - -inline unsigned long SoundSourceFLAC::length() { - // We only support 2 channels. - return m_samples * 2; /*m_iChannels*/ + return frameCount - framesRemaining; } Result SoundSourceFLAC::parseHeader() { @@ -208,31 +220,6 @@ QImage SoundSourceFLAC::parseCoverArt() { return coverArt; } -/** - * Shift needed to take our FLAC sample size to Mixxx's 16-bit samples. - * Shift right on negative, left on positive. - */ -inline int SoundSourceFLAC::getShift() const { - return 16 - m_bps; -} - -/** - * Shift a sample from FLAC as necessary to get a 16-bit value. - */ -inline FLAC__int16 SoundSourceFLAC::shift(FLAC__int32 sample) const { - // this is how libsndfile does this operation and is wonderfully - // straightforward. Just shift the sample left or right so that - // it fits in a 16-bit short. -- bkgood - int shift(getShift()); - if (shift == 0) { - return sample; - } else if (shift < 0) { - return sample >> abs(shift); - } else { - return sample << shift; - } -} - // static QList SoundSourceFLAC::supportedFileExtensions() { QList list; @@ -240,9 +227,9 @@ QList SoundSourceFLAC::supportedFileExtensions() { return list; } - // flac callback methods -FLAC__StreamDecoderReadStatus SoundSourceFLAC::flacRead(FLAC__byte buffer[], size_t *bytes) { +FLAC__StreamDecoderReadStatus SoundSourceFLAC::flacRead(FLAC__byte buffer[], + size_t *bytes) { *bytes = m_file.read((char*) buffer, *bytes); if (*bytes > 0) { return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; @@ -257,7 +244,8 @@ FLAC__StreamDecoderSeekStatus SoundSourceFLAC::flacSeek(FLAC__uint64 offset) { if (m_file.seek(offset)) { return FLAC__STREAM_DECODER_SEEK_STATUS_OK; } else { - qWarning() << "SSFLAC: An unrecoverable error occurred (" << getFilename() << ")"; + qWarning() << "SSFLAC: An unrecoverable error occurred (" + << getFilename() << ")"; return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR; } } @@ -270,7 +258,8 @@ FLAC__StreamDecoderTellStatus SoundSourceFLAC::flacTell(FLAC__uint64 *offset) { return FLAC__STREAM_DECODER_TELL_STATUS_OK; } -FLAC__StreamDecoderLengthStatus SoundSourceFLAC::flacLength(FLAC__uint64 *length) { +FLAC__StreamDecoderLengthStatus SoundSourceFLAC::flacLength( + FLAC__uint64 *length) { if (m_file.isSequential()) { return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; } @@ -286,41 +275,76 @@ FLAC__bool SoundSourceFLAC::flacEOF() { } FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( - const FLAC__Frame *frame, const FLAC__int32 *const buffer[]) { - unsigned int i(0); - m_flacBufferLength = 0; - if (frame->header.channels > 1) { - // stereo (or greater) - for (i = 0; i < frame->header.blocksize; ++i) { - m_flacBuffer[m_flacBufferLength++] = shift(buffer[0][i]); // left channel - m_flacBuffer[m_flacBufferLength++] = shift(buffer[1][i]); // right channel + const FLAC__Frame *frame, const FLAC__int32 * const buffer[]) { + if (getChannelCount() != frame->header.channels) { + qWarning() << "Invalid number of channels in FLAC frame header:" + << "expected" << getChannelCount() << "actual" + << frame->header.channels; + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; + } + Q_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + Q_ASSERT( + (m_decodeSampleBuffer.size() - m_decodeSampleBufferWriteOffset) + >= frames2samples(frame->header.blocksize)); + switch (getChannelCount()) { + case 1: { + // optimized code for 1 channel (mono) + Q_ASSERT(1 <= frame->header.channels); + for (unsigned i = 0; i < frame->header.blocksize; ++i) { + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[0][i] * m_sampleScale; } - } else { - // mono - for (i = 0; i < frame->header.blocksize; ++i) { - m_flacBuffer[m_flacBufferLength++] = shift(buffer[0][i]); // left channel - m_flacBuffer[m_flacBufferLength++] = shift(buffer[0][i]); // mono channel + break; + } + case 2: { + // optimized code for 2 channels (stereo) + Q_ASSERT(2 <= frame->header.channels); + for (unsigned i = 0; i < frame->header.blocksize; ++i) { + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[0][i] * m_sampleScale; + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[1][i] * m_sampleScale; + } + break; + } + default: { + // generic code for multiple channels + for (unsigned i = 0; i < frame->header.blocksize; ++i) { + for (unsigned j = 0; j < frame->header.channels; ++j) { + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[j][i] * m_sampleScale; + } } } - return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; // can't anticipate any errors here + } + Q_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; } void SoundSourceFLAC::flacMetadata(const FLAC__StreamMetadata *metadata) { switch (metadata->type) { case FLAC__METADATA_TYPE_STREAMINFO: - m_samples = metadata->data.stream_info.total_samples; - setChannels(metadata->data.stream_info.channels); - setSampleRate(metadata->data.stream_info.sample_rate); - m_bps = metadata->data.stream_info.bits_per_sample; + setChannelCount(metadata->data.stream_info.channels); + setFrameRate(metadata->data.stream_info.sample_rate); + setFrameCount(metadata->data.stream_info.total_samples); + m_sampleScale = kSampleValuePeak + / sample_type( + FLAC__int32(1) + << metadata->data.stream_info.bits_per_sample); + qDebug() << "FLAC file " << getFilename(); + qDebug() << getChannelCount() << " @ " << getFrameRate() << " Hz, " + << getFrameCount() << " total, " << "bit depth" + << " metadata->data.stream_info.bits_per_sample"; m_minBlocksize = metadata->data.stream_info.min_blocksize; m_maxBlocksize = metadata->data.stream_info.max_blocksize; m_minFramesize = metadata->data.stream_info.min_framesize; m_maxFramesize = metadata->data.stream_info.max_framesize; -// qDebug() << "FLAC file " << getFilename(); -// qDebug() << m_iChannels << " @ " << m_iSampleRate << " Hz, " << m_samples -// << " total, " << m_bps << " bps"; -// qDebug() << "Blocksize in [" << m_minBlocksize << ", " << m_maxBlocksize -// << "], Framesize in [" << m_minFramesize << ", " << m_maxFramesize << "]"; + qDebug() << "Blocksize in [" << m_minBlocksize << ", " << m_maxBlocksize + << "], Framesize in [" << m_minFramesize << ", " + << m_maxFramesize << "]"; + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + m_decodeSampleBuffer.resize(m_maxBlocksize * getChannelCount()); break; default: break; @@ -346,7 +370,7 @@ void SoundSourceFLAC::flacError(FLAC__StreamDecoderErrorStatus status) { break; } qWarning() << "SSFLAC got error" << error << "from libFLAC for file" - << getFilename(); + << getFilename(); // not much else to do here... whatever function that initiated whatever // decoder method resulted in this error will return an error, and the caller // will bail. libFLAC docs say to not close the decoder here -- bkgood @@ -354,8 +378,8 @@ void SoundSourceFLAC::flacError(FLAC__StreamDecoderErrorStatus status) { // begin callbacks (have to be regular functions because normal libFLAC isn't C++-aware) -FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder*, FLAC__byte buffer[], - size_t *bytes, void *client_data) { +FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder*, + FLAC__byte buffer[], size_t *bytes, void *client_data) { return ((SoundSourceFLAC*) client_data)->flacRead(buffer, bytes); } @@ -378,16 +402,19 @@ FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder*, void *client_data) { return ((SoundSourceFLAC*) client_data)->flacEOF(); } -FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder*, const FLAC__Frame *frame, - const FLAC__int32 *const buffer[], void *client_data) { +FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder*, + const FLAC__Frame *frame, const FLAC__int32 * const buffer[], + void *client_data) { return ((SoundSourceFLAC*) client_data)->flacWrite(frame, buffer); } -void FLAC_metadata_cb(const FLAC__StreamDecoder*, const FLAC__StreamMetadata *metadata, void *client_data) { +void FLAC_metadata_cb(const FLAC__StreamDecoder*, + const FLAC__StreamMetadata *metadata, void *client_data) { ((SoundSourceFLAC*) client_data)->flacMetadata(metadata); } -void FLAC_error_cb(const FLAC__StreamDecoder*, FLAC__StreamDecoderErrorStatus status, void *client_data) { +void FLAC_error_cb(const FLAC__StreamDecoder*, + FLAC__StreamDecoderErrorStatus status, void *client_data) { ((SoundSourceFLAC*) client_data)->flacError(status); } // end callbacks diff --git a/src/soundsourceflac.h b/src/soundsourceflac.h index 9231f6cefb0..2772bad92c1 100644 --- a/src/soundsourceflac.h +++ b/src/soundsourceflac.h @@ -18,26 +18,36 @@ #ifndef SOUNDSOURCEFLAC_H #define SOUNDSOURCEFLAC_H -#include -#include +#include "soundsource.h" +#include "util/defs.h" +#include "util/types.h" #include -#include "util/defs.h" -#include "util/types.h" -#include "soundsource.h" +#include +#include + +#include class SoundSourceFLAC : public Mixxx::SoundSource { + typedef SoundSource Super; + public: - ~SoundSourceFLAC(); - Result open(); - long seek(long filepos); - unsigned read(unsigned long size, const SAMPLE *buffer); - inline long unsigned length(); - Result parseHeader(); - QImage parseCoverArt(); static QList supportedFileExtensions(); + explicit SoundSourceFLAC(QString filename); + ~SoundSourceFLAC(); + + Result parseHeader() /*override*/; + + QImage parseCoverArt() /*override*/; + + Result open() /*override*/; + + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + // callback methods FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t *bytes); FLAC__StreamDecoderSeekStatus flacSeek(FLAC__uint64 offset); @@ -47,29 +57,30 @@ class SoundSourceFLAC : public Mixxx::SoundSource { FLAC__StreamDecoderWriteStatus flacWrite(const FLAC__Frame *frame, const FLAC__int32 *const buffer[]); void flacMetadata(const FLAC__StreamMetadata *metadata); void flacError(FLAC__StreamDecoderErrorStatus status); + private: - // these next two are inline but are defined in the cpp file because - // they should only be used there -- bkgood - inline int getShift() const; - inline FLAC__int16 shift(const FLAC__int32 sample) const; + void close(); + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); + QFile m_file; + FLAC__StreamDecoder *m_decoder; - unsigned int m_samples; // total number of samples - unsigned int m_bps; // bits per sample // misc bits about the flac format: // flac encodes from and decodes to LPCM in blocks, each block is made up of // subblocks (one for each chan) // flac stores in 'frames', each of which has a header and a certain number // of subframes (one for each channel) - unsigned int m_minBlocksize; // in time samples (audio samples = time samples * chanCount) - unsigned int m_maxBlocksize; - unsigned int m_minFramesize; - unsigned int m_maxFramesize; - FLAC__int16 *m_flacBuffer; // buffer for the write callback to write a single frame's samples - unsigned int m_flacBufferLength; - FLAC__int16 *m_leftoverBuffer; // buffer to place any samples which haven't been used - // at the end of a read call - unsigned int m_leftoverBufferLength; + size_type m_minBlocksize; // in time samples (audio samples = time samples * chanCount) + size_type m_maxBlocksize; + size_type m_minFramesize; + size_type m_maxFramesize; + sample_type m_sampleScale; + + typedef std::vector SampleBuffer; + std::vector m_decodeSampleBuffer; + SampleBuffer::size_type m_decodeSampleBufferReadOffset; + SampleBuffer::size_type m_decodeSampleBufferWriteOffset; }; // callbacks for libFLAC diff --git a/src/soundsourcemodplug.cpp b/src/soundsourcemodplug.cpp index 598ccb4cdeb..1ee473a426a 100644 --- a/src/soundsourcemodplug.cpp +++ b/src/soundsourcemodplug.cpp @@ -1,52 +1,24 @@ +#include "soundsourcemodplug.h" + #include #include #include #include -#include "soundsourcemodplug.h" - #include "util/timer.h" /* read files in 512k chunks */ -#define CHUNKSIZE (1 << 19) +#define CHUNKSIZE (1 << 18) // reserve some static space for settings... -ModPlug::ModPlug_Settings SoundSourceModPlug::s_settings; -int SoundSourceModPlug::s_bufferSizeLimit; - -SoundSourceModPlug::SoundSourceModPlug(QString qFilename) : - SoundSource(qFilename) +namespace { - m_opened = false; - m_fileLength = 0; - m_pModFile = 0; - - qDebug() << "[ModPlug] Loading ModPlug module " << getFilename(); - - // read module file to byte array - QFile modFile(getFilename()); - modFile.open(QIODevice::ReadOnly); - m_fileBuf = modFile.readAll(); - modFile.close(); - // get ModPlugFile descriptor for later access - m_pModFile = ModPlug::ModPlug_Load(m_fileBuf.constData(), m_fileBuf.length()); - - if (m_pModFile==NULL) { - qDebug() << "[ModPlug] Error while ModPlug_Load"; - } -} - -SoundSourceModPlug::~SoundSourceModPlug() -{ - if (m_pModFile) { - ModPlug::ModPlug_Unload(m_pModFile); - m_pModFile = NULL; - } + static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) + static ModPlug::ModPlug_Settings s_settings; // modplug decoder parameters } -QList SoundSourceModPlug::supportedFileExtensions() -{ +QList SoundSourceModPlug::supportedFileExtensions() { QList list; // ModPlug supports more formats but file name // extensions are not always present with modules. @@ -61,22 +33,66 @@ QList SoundSourceModPlug::supportedFileExtensions() } void SoundSourceModPlug::configure(unsigned int bufferSizeLimit, - const ModPlug::ModPlug_Settings &settings) -{ + const ModPlug::ModPlug_Settings &settings) { s_bufferSizeLimit = bufferSizeLimit; s_settings = settings; - ModPlug::ModPlug_SetSettings(&s_settings); } +namespace +{ + QString getTypeFromFilename(QString filename) { + const QString fileext(filename.section(".", -1).toLower()); + if (fileext == "mod") { + return "Protracker"; + } else if (fileext == "med") { + return "OctaMed"; + } else if (fileext == "okt") { + return "Oktalyzer"; + } else if (fileext == "s3m") { + return "Scream Tracker 3"; + } else if (fileext == "stm") { + return "Scream Tracker"; + } else if (fileext == "xm") { + return "FastTracker2"; + } else if (fileext == "it") { + return "Impulse Tracker"; + } else { + return "Module"; + } + } +} + +SoundSourceModPlug::SoundSourceModPlug(QString qFilename) + : Super(qFilename, getTypeFromFilename(qFilename)), m_pModFile(NULL), m_fileLength(0), m_seekPos(0) { + setChannelCount(2); // always stereo + setFrameRate(44100); // always 44.1kHz +} + +SoundSourceModPlug::~SoundSourceModPlug() { + if (m_pModFile) { + ModPlug::ModPlug_Unload(m_pModFile); + } +} + Result SoundSourceModPlug::open() { ScopedTimer t("SoundSourceModPlug::open()"); + qDebug() << "[ModPlug] Loading ModPlug module " << getFilename(); + + // read module file to byte array + QFile modFile(getFilename()); + modFile.open(QIODevice::ReadOnly); + m_fileBuf = modFile.readAll(); + modFile.close(); + // get ModPlugFile descriptor for later access + m_pModFile = ModPlug::ModPlug_Load(m_fileBuf.constData(), + m_fileBuf.length()); + if (m_pModFile == NULL) { // an error occured t.cancel(); - qDebug() << "[ModPlug] Could not load module file: " - << getFilename(); + qDebug() << "[ModPlug] Could not load module file: " << getFilename(); return ERR; } @@ -87,67 +103,61 @@ Result SoundSourceModPlug::open() { // * 44.1 (samples per millisecond) // + some more to accomodate short loops etc. // approximate and align with CHUNKSIZE yields: - // (((milliseconds << 2) >> 10 /* to seconds */) + // (((milliseconds << 1) >> 10 /* to seconds */) // div 11 /* samples to chunksize ratio */) // << 19 /* align to chunksize */ - int estimate = ((ModPlug::ModPlug_GetLength(m_pModFile) >> 8) / 11) << 19; + unsigned int estimate = ((ModPlug::ModPlug_GetLength(m_pModFile) >> 8) / 11) << 18; estimate = math_min(estimate, s_bufferSizeLimit); m_sampleBuf.reserve(estimate); qDebug() << "[ModPlug] Reserved " << m_sampleBuf.capacity() - << " bytes for samples"; + << " #samples"; // decode samples to sample buffer - int bytesRead = -1; + int samplesRead = -1; int currentSize = 0; - while((bytesRead != 0) && (m_sampleBuf.length() < s_bufferSizeLimit)) { + while ((samplesRead != 0) && (m_sampleBuf.size() < s_bufferSizeLimit)) { // reserve enough space in sample buffer m_sampleBuf.resize(currentSize + CHUNKSIZE); - bytesRead = ModPlug::ModPlug_Read(m_pModFile, - m_sampleBuf.data() + currentSize, - CHUNKSIZE); + samplesRead = ModPlug::ModPlug_Read(m_pModFile, + m_sampleBuf.data() + currentSize, + CHUNKSIZE * 2) / 2; // adapt to actual size - currentSize += bytesRead; - if (bytesRead != CHUNKSIZE) { + currentSize += samplesRead; + if (samplesRead != CHUNKSIZE) { m_sampleBuf.resize(currentSize); - bytesRead = 0; // we reached the end of the file + samplesRead = 0; // we reached the end of the file } } - qDebug() << "[ModPlug] Filled Sample buffer with " << m_sampleBuf.length() - << " bytes."; + qDebug() << "[ModPlug] Filled Sample buffer with " << m_sampleBuf.size() + << " samples."; qDebug() << "[ModPlug] Sample buffer has " - << m_sampleBuf.capacity() - m_sampleBuf.length() - << " bytes unused capacity."; - - // The sample buffer holds 44.1kHz 16bit integer stereo samples. - // We count the number of samples by dividing number of - // bytes in m_sampleBuf by 2 (bytes per sample). - m_fileLength = m_sampleBuf.length() >> 1; - setSampleRate(44100); // ModPlug always uses 44.1kHz - m_opened = true; + << m_sampleBuf.capacity() - m_sampleBuf.size() + << " samples unused capacity."; + + setFrameCount(samples2frames(m_sampleBuf.size())); m_seekPos = 0; + return OK; } -long SoundSourceModPlug::seek(long filePos) -{ - if (m_fileLength > 0) { - m_seekPos = math_min((unsigned long)filePos, m_fileLength); - return m_seekPos; - } - return 0; +Mixxx::AudioSource::diff_type SoundSourceModPlug::seekFrame( + diff_type frameIndex) { + return m_seekPos = frameIndex; } -unsigned SoundSourceModPlug::read(unsigned long size, - const SAMPLE* pDestination) -{ - unsigned maxLength = m_sampleBuf.length() >> 1; - unsigned copySamples = math_min(maxLength - m_seekPos, size); +Mixxx::AudioSource::size_type SoundSourceModPlug::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + const size_type maxFrames = samples2frames(m_sampleBuf.size()); + const size_type readFrames = math_min(maxFrames - m_seekPos, frameCount); - memcpy((unsigned char*) pDestination, - m_sampleBuf.constData() + (m_seekPos << 1), copySamples << 1); + const size_type readSamples = frames2samples(readFrames); + const size_type readOffset = frames2samples(m_seekPos); + for (size_type i = 0; i < readSamples; ++i) { + sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) / sample_type(SAMPLE_MAX); + } - m_seekPos += copySamples; - return copySamples; + m_seekPos += readFrames; + return readFrames; } Result SoundSourceModPlug::parseHeader() { @@ -157,42 +167,10 @@ Result SoundSourceModPlug::parseHeader() { return ERR; } - switch (ModPlug::ModPlug_GetModuleType(m_pModFile)) { - case NONE: - setType(QString("None")); - break; - case MOD: - setType(QString("Protracker")); - break; - case S3M: - setType(QString("Scream Tracker 3")); - break; - case XM: - setType(QString("FastTracker2")); - break; - case MED: - setType(QString("OctaMed")); - break; - case IT: - setType(QString("Impulse Tracker")); - break; - case STM: - setType(QString("Scream Tracker")); - break; - case OKT: - setType(QString("Oktalyzer")); - break; - default: - setType(QString("Module")); - break; - } - setComment(QString(ModPlug::ModPlug_GetMessage(m_pModFile))); setTitle(QString(ModPlug::ModPlug_GetName(m_pModFile))); setDuration(ModPlug::ModPlug_GetLength(m_pModFile) / 1000); setBitrate(8); // not really, but fill in something... - setSampleRate(44100); - setChannels(2); return OK; } @@ -201,8 +179,3 @@ QImage SoundSourceModPlug::parseCoverArt() { // modplug files -- kain88 (Oct 2014) return QImage(); } - -inline long unsigned SoundSourceModPlug::length() -{ - return m_fileLength; -} diff --git a/src/soundsourcemodplug.h b/src/soundsourcemodplug.h index c471a81c3fe..d0b98f06447 100644 --- a/src/soundsourcemodplug.h +++ b/src/soundsourcemodplug.h @@ -1,61 +1,56 @@ -// soundsourcemodplug.h - modplug tracker support -// created 2012 by Stefan Nuernberger - #ifndef SOUNDSOURCEMODPLUG_H #define SOUNDSOURCEMODPLUG_H -#include -#include -#include - #include "soundsource.h" -#include "util/math.h" namespace ModPlug { #include } +#include + // Class for reading tracker files using libmodplug. // The whole file is decoded at once and saved // in RAM to allow seeking and smooth operation in Mixxx. -class SoundSourceModPlug : public Mixxx::SoundSource -{ - public: - explicit SoundSourceModPlug(QString qFilename); - ~SoundSourceModPlug(); - Result open(); - long seek(long); - unsigned read(unsigned long size, const SAMPLE*); - inline long unsigned length(); - Result parseHeader(); - QImage parseCoverArt(); +class SoundSourceModPlug: public Mixxx::SoundSource { + typedef SoundSource Super; + +public: static QList supportedFileExtensions(); // apply settings for decoding static void configure(unsigned int bufferSizeLimit, - const ModPlug::ModPlug_Settings &settings); + const ModPlug::ModPlug_Settings &settings); - private: - static int s_bufferSizeLimit; // max track buffer length (bytes) - static ModPlug::ModPlug_Settings s_settings; // modplug decoder parameters + explicit SoundSourceModPlug(QString qFilename); + ~SoundSourceModPlug(); + + Result parseHeader() /*override*/; + QImage parseCoverArt() /*override*/; + + Result open() /*override*/; - bool m_opened; + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; + +private: + ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor unsigned long m_fileLength; // length of file in samples unsigned long m_seekPos; // current read position - ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor QByteArray m_fileBuf; // original module file data - QByteArray m_sampleBuf; // 16bit stereo samples, 44.1kHz + std::vector m_sampleBuf; // 16bit stereo samples, 44.1kHz // identification of modplug module type enum ModuleTypes { NONE = 0x00, - MOD = 0x01, - S3M = 0x02, - XM = 0x04, - MED = 0x08, - IT = 0x20, - STM = 0x100, - OKT = 0x8000 + MOD = 0x01, + S3M = 0x02, + XM = 0x04, + MED = 0x08, + IT = 0x20, + STM = 0x100, + OKT = 0x8000 }; }; diff --git a/src/soundsourcemp3.cpp b/src/soundsourcemp3.cpp index 37b01d92edc..b0d8803e696 100644 --- a/src/soundsourcemp3.cpp +++ b/src/soundsourcemp3.cpp @@ -23,54 +23,27 @@ #include -SoundSourceMp3::SoundSourceMp3(QString qFilename) : - Mixxx::SoundSource(qFilename), - m_file(qFilename) -{ - setType("mp3"); - inputbuf = NULL; - Stream = new mad_stream; - mad_stream_init(Stream); - Synth = new mad_synth; - mad_synth_init(Synth); - Frame = new mad_frame; - mad_frame_init(Frame); - - m_currentSeekFrameIndex = 0; - m_iAvgFrameSize = 0; - m_iChannels = 0; - rest = 0; - - bitrate = 0; - framecount = 0; - currentframe = 0; - pos = mad_timer_zero; - filelength = mad_timer_zero; - inputbuf_len = 0; +SoundSourceMp3::SoundSourceMp3(QString qFilename) + : Super(qFilename, "mp3") + , m_file(qFilename) + , inputbuf(NULL) + , inputbuf_len(0) + , framecount(0) + , currentframe(0) + , pos(mad_timer_zero) + , filelength(mad_timer_zero) + , units(MAD_UNITS_44100_HZ) + , m_madSynthOffset(0) + , m_currentSeekFrameIndex(0) + , m_iAvgFrameSize(0) { + mad_stream_init(&madStream); + mad_synth_init(&m_madSynth); + mad_frame_init(&madFrame); } SoundSourceMp3::~SoundSourceMp3() { - mad_stream_finish(Stream); - delete Stream; - - mad_frame_finish(Frame); - delete Frame; - - mad_synth_finish(Synth); - delete Synth; - - // Unmap inputbuf. - m_file.unmap(inputbuf); - inputbuf = NULL; - m_file.close(); - - //Free the pointers in our seek list, LIBERATE THEM!!! - for (int i = 0; i < m_qSeekList.count(); i++) - { - delete m_qSeekList[i]; - } - m_qSeekList.clear(); + close(); } QList SoundSourceMp3::supportedFileExtensions() @@ -92,31 +65,32 @@ Result SoundSourceMp3::open() { inputbuf = m_file.map(0, inputbuf_len); // Transfer it to the mad stream-buffer: - mad_stream_init(Stream); - mad_stream_options(Stream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(Stream, inputbuf, inputbuf_len); + mad_stream_init(&madStream); + mad_stream_options(&madStream, MAD_OPTION_IGNORECRC); + mad_stream_buffer(&madStream, inputbuf, inputbuf_len); /* Decode all the headers, and fill in stats: */ - mad_header Header; - mad_header_init(&Header); - filelength = mad_timer_zero; - bitrate = 0; + mad_header madHeader; + mad_header_init(&madHeader); + mad_timer_t filelength = mad_timer_zero; + int sumBitrate = 0; currentframe = 0; pos = mad_timer_zero; - while ((Stream->bufend - Stream->this_frame) > 0) + while ((madStream.bufend - madStream.this_frame) > 0) { - if (mad_header_decode (&Header, Stream) == -1) { - if (!MAD_RECOVERABLE (Stream->error)) + if (mad_header_decode (&madHeader, &madStream) == -1) { + if (!MAD_RECOVERABLE (madStream.error)) { break; - if (Stream->error == MAD_ERROR_LOSTSYNC) { + } + if (madStream.error == MAD_ERROR_LOSTSYNC) { // ignore LOSTSYNC due to ID3 tags - int tagsize = id3_tag_query (Stream->this_frame,Stream->bufend - Stream->this_frame); + int tagsize = id3_tag_query (madStream.this_frame, madStream.bufend - madStream.this_frame); if (tagsize > 0) { //qDebug() << "SSMP3::SSMP3() : skipping ID3 tag size " << tagsize; - mad_stream_skip (Stream, tagsize); + mad_stream_skip (&madStream, tagsize); continue; } } @@ -131,25 +105,60 @@ Result SoundSourceMp3::open() { // Grab data from madHeader + setChannelCount(MAD_NCHANNELS(&madHeader)); + // This warns us only when the reported sample rate changes. (and when // it is first set) - if (getSampleRate() == 0 && Header.samplerate > 0) { - setSampleRate(Header.samplerate); - } else if (getSampleRate() != Header.samplerate) { + if (getFrameRate() == 0 && madHeader.samplerate > 0) { + setFrameRate(madHeader.samplerate); + } else if (getFrameRate() != madHeader.samplerate) { qDebug() << "SSMP3: file has differing samplerate in some headers:" << getFilename() - << getSampleRate() << "vs" << Header.samplerate; + << getFrameRate() << "vs" << madHeader.samplerate; + } + + switch (getFrameRate()) + { + case 8000: + units = MAD_UNITS_8000_HZ; + break; + case 11025: + units = MAD_UNITS_11025_HZ; + break; + case 12000: + units = MAD_UNITS_12000_HZ; + break; + case 16000: + units = MAD_UNITS_16000_HZ; + break; + case 22050: + units = MAD_UNITS_22050_HZ; + break; + case 24000: + units = MAD_UNITS_24000_HZ; + break; + case 32000: + units = MAD_UNITS_32000_HZ; + break; + case 44100: + units = MAD_UNITS_44100_HZ; + break; + case 48000: + units = MAD_UNITS_48000_HZ; + break; + default: //By the MP3 specs, an MP3 _has_ to have one of the above samplerates... + qWarning() << "MP3 with invalid sample rate (" << getFrameRate() << "), defaulting to 44100"; + setFrameRate(44100); //Prevents division by zero errors. + units = MAD_UNITS_44100_HZ; } - setChannels(2); // always pretend to read 2 channels - m_iChannels = MAD_NCHANNELS(&Header); - mad_timer_add (&filelength, Header.duration); - bitrate += Header.bitrate; + mad_timer_add (&filelength, madHeader.duration); + sumBitrate += madHeader.bitrate; // Add frame to list of frames MadSeekFrameType * p = new MadSeekFrameType; - p->m_pStreamPos = (unsigned char *)Stream->this_frame; - p->pos = length(); + p->m_pStreamPos = (unsigned char *)madStream.this_frame; + p->pos = mad_timer_count(filelength, units); m_qSeekList.append(p); currentframe++; } @@ -163,42 +172,43 @@ Result SoundSourceMp3::open() { return ERR; } - // Find average frame size - m_iAvgFrameSize = (currentframe == 0) ? 0 : length()/currentframe; - // And average bitrate - bitrate = (currentframe == 0) ? 0 : bitrate / currentframe; framecount = currentframe; - currentframe = 0; + setFrameCount(mad_timer_count(filelength, units)); - //Recalculate the duration by using the average frame size. Our first guess at - //the duration of VBR MP3s in parseHeader() goes for speed over accuracy - //since it runs during a library scan. When we open() an MP3 for playback, - //we had to seek through the entire thing to build a seek table, so we've - //also counted the number of frames in it. We need that to better estimate - //the length of VBR MP3s. - if (getSampleRate() > 0 && m_iChannels > 0) //protect again divide by zero - { - //qDebug() << "SSMP3::open() - Setting duration to:" << framecount * m_iAvgFrameSize / getSampleRate() / m_iChannels; - setDuration(framecount * m_iAvgFrameSize / getSampleRate() / m_iChannels); + if (0 < framecount) { + m_iAvgFrameSize = getFrameCount() / framecount; + int avgBitrate = sumBitrate / framecount; + setBitrate(avgBitrate); + } else { + m_iAvgFrameSize = 0; } - //TODO: Emit metadata updated signal? -/* - qDebug() << "length = " << filelength.seconds << "d sec."; - qDebug() << "frames = " << framecount; - qDebug() << "bitrate = " << bitrate/1000; - qDebug() << "Size = " << length(); - */ - // Re-init buffer: - seek(0); + seekFrame(0); + currentframe = 0; return OK; } -bool SoundSourceMp3::isValid() const { - return framecount > 0; +void SoundSourceMp3::close() +{ + mad_stream_finish(&madStream); + mad_frame_finish(&madFrame); + mad_synth_finish(&m_madSynth); + + m_file.unmap(inputbuf); + inputbuf = 0; + inputbuf_len = 0; + + m_file.close(); + + //Free the pointers in our seek list, LIBERATE THEM!!! + for (int i = 0; i < m_qSeekList.count(); i++) + { + delete m_qSeekList[i]; + } + m_qSeekList.clear(); } MadSeekFrameType* SoundSourceMp3::getSeekFrame(long frameIndex) const { @@ -208,358 +218,201 @@ MadSeekFrameType* SoundSourceMp3::getSeekFrame(long frameIndex) const { return m_qSeekList.at(frameIndex); } -long SoundSourceMp3::seek(long filepos) { - // Ensure that we are seeking to an even filepos - if (filepos % 2 != 0) { - qDebug() << "SoundSourceMp3 got non-even seek target."; - filepos--; - } - - if (!isValid()) { - qDebug() << "SSMP3: Error wile seeking file " << getFilename(); - return 0; - } - - //qDebug() << "SEEK " << filepos; +namespace +{ + const int kSeekSyncFrameCount = 4; // required for synchronization +} +Mixxx::AudioSource::diff_type SoundSourceMp3::seekFrame(diff_type frameIndex) { MadSeekFrameType* cur = NULL; - - if (filepos == 0) { + if (frameIndex == 0) { // Seek to beginning of file // Re-init buffer: - mad_stream_finish(Stream); - mad_stream_init(Stream); - mad_stream_options(Stream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(Stream, (unsigned char *) inputbuf, inputbuf_len); - mad_frame_init(Frame); - mad_synth_init(Synth); - rest=-1; - + mad_stream_finish(&madStream); + mad_stream_init(&madStream); + mad_stream_options(&madStream, MAD_OPTION_IGNORECRC); + mad_stream_buffer(&madStream, (unsigned char *) inputbuf, inputbuf_len); + mad_frame_init(&madFrame); + mad_synth_init(&m_madSynth); + m_madSynthOffset = 0; m_currentSeekFrameIndex = 0; - //cur = getSeekFrame(0); - //frameIterator.toFront(); //Might not need to do this -- Albert June 19/2010 (during Qt3 purge) + cur = getSeekFrame(m_currentSeekFrameIndex); } else { - //qDebug() << "seek precise"; - // Perform precise seek accomplished by using a frame in the seek list - - // Find the frame to seek to in the list - /* - MadSeekFrameType *cur = m_qSeekList.last(); - int k=0; - while (cur!=0 && cur->pos>filepos) - { - cur = m_qSeekList.prev(); - ++k; - } - */ - - int framePos = findFrame(filepos); - if (framePos == 0 || framePos > filepos || m_currentSeekFrameIndex < 5) { - //qDebug() << "Problem finding good seek frame (wanted " << filepos << ", got " << framePos << "), starting from 0"; + int framePos = findFrame(frameIndex); + if (framePos == 0 || framePos > frameIndex || m_currentSeekFrameIndex <= kSeekSyncFrameCount) { + qDebug() << "Problem finding good seek frame (wanted " << frameIndex << ", got " << framePos << "), starting from 0"; // Re-init buffer: - mad_stream_finish(Stream); - mad_stream_init(Stream); - mad_stream_options(Stream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(Stream, (unsigned char *) inputbuf, inputbuf_len); - mad_frame_init(Frame); - mad_synth_init(Synth); - rest = -1; + mad_stream_finish(&madStream); + mad_stream_init(&madStream); + mad_stream_options(&madStream, MAD_OPTION_IGNORECRC); + mad_stream_buffer(&madStream, (unsigned char *) inputbuf, inputbuf_len); + mad_frame_init(&madFrame); + mad_synth_init(&m_madSynth); + m_madSynthOffset = 0; m_currentSeekFrameIndex = 0; cur = getSeekFrame(m_currentSeekFrameIndex); } else { //qDebug() << "frame pos " << cur->pos; // Start four frames before wanted frame to get in sync... - m_currentSeekFrameIndex -= 4; + m_currentSeekFrameIndex -= kSeekSyncFrameCount; cur = getSeekFrame(m_currentSeekFrameIndex); if (cur != NULL) { // Start from the new frame - mad_stream_finish(Stream); - mad_stream_init(Stream); - mad_stream_options(Stream, MAD_OPTION_IGNORECRC); + mad_stream_finish(&madStream); + mad_stream_init(&madStream); + mad_stream_options(&madStream, MAD_OPTION_IGNORECRC); // qDebug() << "mp3 restore " << cur->m_pStreamPos; - mad_stream_buffer(Stream, (const unsigned char *)cur->m_pStreamPos, + mad_stream_buffer(&madStream, (const unsigned char *)cur->m_pStreamPos, inputbuf_len-(long int)(cur->m_pStreamPos-(unsigned char *)inputbuf)); // Mute'ing is done here to eliminate potential pops/clicks from skipping // Rob Leslie explains why here: // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html - mad_synth_mute(Synth); - mad_frame_mute(Frame); + mad_synth_mute(&m_madSynth); + mad_frame_mute(&madFrame); // Decode the three frames before - mad_frame_decode(Frame, Stream); - mad_frame_decode(Frame, Stream); - mad_frame_decode(Frame, Stream); - mad_frame_decode(Frame, Stream); + mad_frame_decode(&madFrame, &madStream); + mad_frame_decode(&madFrame, &madStream); + mad_frame_decode(&madFrame, &madStream); + mad_frame_decode(&madFrame, &madStream); // this is also explained in the above mad-dev post - mad_synth_frame(Synth, Frame); + mad_synth_frame(&m_madSynth, &madFrame); // Set current position - rest = -1; + m_madSynthOffset = 0; m_currentSeekFrameIndex += 4; cur = getSeekFrame(m_currentSeekFrameIndex); } } - // Synthesize the samples from the frame which should be discard to reach the requested position - if (cur != NULL) //the "if" prevents crashes on bad files. - discard(filepos-cur->pos); - } -/* - else - { - qDebug() << "seek unprecise"; - // Perform seek which is can not be done precise because no frames is in the seek list - - int newpos = (int)(inputbuf_len * ((float)filepos/(float)length())); - // qDebug() << "Seek to " << filepos << " " << inputbuf_len << " " << newpos; - - // Go to an approximate position: - mad_stream_buffer(Stream, (unsigned char *) (inputbuf+newpos), inputbuf_len-newpos); - mad_synth_mute(Synth); - mad_frame_mute(Frame); - - // Decode a few (possible wrong) buffers: - int no = 0; - int succesfull = 0; - while ((no<10) && (succesfull<2)) - { - if (!mad_frame_decode(Frame, Stream)) - succesfull ++; - no ++; + if (cur != NULL) { //the "if" prevents crashes on bad files. + discardFrames(frameIndex - cur->pos); } - - // Discard the first synth: - mad_synth_frame(Synth, Frame); - - // Remaining samples in buffer are useless - rest = -1; - - // Reset seek frame list - m_qSeekList.clear(); - MadSeekFrameType *p = new MadSeekFrameType; - p->m_pStreamPos = (unsigned char*)Stream->this_frame; - p->pos = filepos; - m_qSeekList.append(p); - m_iSeekListMinPos = filepos; - m_iSeekListMaxPos = filepos; - m_iCurFramePos = filepos; } - */ - // Unfortunately we don't know the exact fileposition. The returned position is thus an + // Unfortunately we don't know the exact file position. The returned position is thus an // approximation only: - return filepos; -} - -inline long unsigned SoundSourceMp3::length() { - enum mad_units units; - - switch (getSampleRate()) - { - case 8000: - units = MAD_UNITS_8000_HZ; - break; - case 11025: - units = MAD_UNITS_11025_HZ; - break; - case 12000: - units = MAD_UNITS_12000_HZ; - break; - case 16000: - units = MAD_UNITS_16000_HZ; - break; - case 22050: - units = MAD_UNITS_22050_HZ; - break; - case 24000: - units = MAD_UNITS_24000_HZ; - break; - case 32000: - units = MAD_UNITS_32000_HZ; - break; - case 44100: - units = MAD_UNITS_44100_HZ; - break; - case 48000: - units = MAD_UNITS_48000_HZ; - break; - default: //By the MP3 specs, an MP3 _has_ to have one of the above samplerates... - units = MAD_UNITS_44100_HZ; - qWarning() << "MP3 with corrupt samplerate (" << getSampleRate() << "), defaulting to 44100"; - - setSampleRate(44100); //Prevents division by zero errors. - } - - return 2 * mad_timer_count(filelength, units); + return frameIndex; } /* decode the chosen number of samples and discard */ +Mixxx::AudioSource::size_type SoundSourceMp3::discardFrames(size_type frameCount) { + size_type framesDiscarded = 0; -unsigned long SoundSourceMp3::discard(unsigned long samples_wanted) { - unsigned long Total_samples_decoded = 0; - int no = 0; - - if (rest > 0) - Total_samples_decoded += 2*(Synth->pcm.length-rest); - - while (Total_samples_decoded < samples_wanted) { - if (mad_frame_decode(Frame, Stream)) { - if (MAD_RECOVERABLE(Stream->error)) { - continue; - } else if(Stream->error == MAD_ERROR_BUFLEN) { - break; - } else { - break; + while (framesDiscarded < frameCount) { + if (0 == m_madSynthOffset) { + if (mad_frame_decode(&madFrame, &madStream)) { + if (MAD_RECOVERABLE(madStream.error)) { + continue; + } else if(madStream.error == MAD_ERROR_BUFLEN) { + break; + } else { + break; + } } + mad_synth_frame(&m_madSynth, &madFrame); + } + size_type no = math_min(size_type(m_madSynth.pcm.length - m_madSynthOffset), frameCount - framesDiscarded); + m_madSynthOffset += no; + if (m_madSynthOffset >= m_madSynth.pcm.length) { + m_madSynthOffset = 0; } - mad_synth_frame(Synth, Frame); - no = math_min(Synth->pcm.length,(samples_wanted-Total_samples_decoded)/2); - Total_samples_decoded += 2*no; + framesDiscarded += no; } - if (Synth->pcm.length > no) - rest = no; - else - rest = -1; + return framesDiscarded; +} - return Total_samples_decoded; +Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); } -/* - read samples into , and return the number of - samples actually read. - */ -unsigned SoundSourceMp3::read(unsigned long samples_wanted, const SAMPLE * _destination) +Mixxx::AudioSource::size_type SoundSourceMp3::readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); +} + +namespace { - if (!isValid()) { - qDebug() << "SSMP3: Error while reading " << getFilename(); - return 0; - } + const Mixxx::AudioSource::sample_type kMadScale = Mixxx::AudioSource::kSampleValuePeak / Mixxx::AudioSource::sample_type(mad_fixed_t(1) << MAD_F_FRACBITS); - // Ensure that we are reading an even number of samples. Otherwise this function may - // go into an infinite loop - if (samples_wanted % 2 != 0) { - qDebug() << "SoundSourceMp3 got non-even samples_wanted"; - samples_wanted--; + inline + Mixxx::AudioSource::sample_type madScale(mad_fixed_t sample) { + return sample * kMadScale; } -// qDebug() << "frame list " << m_qSeekList.count(); - - SAMPLE * destination = (SAMPLE *)_destination; - unsigned Total_samples_decoded = 0; - int i; - - // If samples are left from previous read, then copy them to start of destination - // Make sure to take into account the case where there are more samples left over - // from the previous read than the client requested. - if (rest > 0) - { - for (i=rest; ipcm.length && Total_samples_decoded < samples_wanted; i++) - { - // Left channel - *(destination++) = madScale(Synth->pcm.samples[0][i]); - - /* Right channel. If the decoded stream is monophonic then - * the right output channel is the same as the left one. */ - if (m_iChannels>1) - *(destination++) = madScale(Synth->pcm.samples[1][i]); - else - *(destination++) = madScale(Synth->pcm.samples[0][i]); - - // This is safe because we have checked that samples_wanted is even. - Total_samples_decoded += 2; - - } +} - if(Total_samples_decoded >= samples_wanted) { - if(i < Synth->pcm.length) - rest = i; - else - rest = -1; - return Total_samples_decoded; - } - } +Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples) { + size_type framesRead = 0; + sample_type* sampleBufferPtr = sampleBuffer; // qDebug() << "Decoding"; - int no = 0; - unsigned int frames = 0; - while (Total_samples_decoded < samples_wanted) - { - // qDebug() << "no " << Total_samples_decoded; - if(mad_frame_decode(Frame,Stream)) - { - if(MAD_RECOVERABLE(Stream->error)) - { - if(Stream->error == MAD_ERROR_LOSTSYNC) { - // Ignore LOSTSYNC due to ID3 tags - int tagsize = id3_tag_query(Stream->this_frame, Stream->bufend - Stream->this_frame); - if(tagsize > 0) { - //qDebug() << "SSMP3::Read Skipping ID3 tag size: " << tagsize; - mad_stream_skip(Stream, tagsize); + while (framesRead < frameCount) { + // qDebug() << "no " << framesRead; + if (0 == m_madSynthOffset) { + if (mad_frame_decode(&madFrame, &madStream)) { + if (MAD_RECOVERABLE(madStream.error)) { + if (madStream.error == MAD_ERROR_LOSTSYNC) { + // Ignore LOSTSYNC due to ID3 tags + int tagsize = id3_tag_query(madStream.this_frame, madStream.bufend - madStream.this_frame); + if (tagsize > 0) { + //qDebug() << "SSMP3::Read Skipping ID3 tag size: " << tagsize; + mad_stream_skip(&madStream, tagsize); + } + continue; } + //qDebug() << "MAD: Recoverable frame level ERR (" << mad_stream_errorstr(Stream) << ")"; continue; + } else if (madStream.error == MAD_ERROR_BUFLEN) { + // qDebug() << "MAD: buflen ERR"; + break; + } else { + // qDebug() << "MAD: Unrecoverable frame level ERR (" << mad_stream_errorstr(Stream) << ")."; + break; } - //qDebug() << "MAD: Recoverable frame level ERR (" << mad_stream_errorstr(Stream) << ")"; - continue; - } else if(Stream->error==MAD_ERROR_BUFLEN) { - // qDebug() << "MAD: buflen ERR"; - break; - } else { - // qDebug() << "MAD: Unrecoverable frame level ERR (" << mad_stream_errorstr(Stream) << ")."; - break; } + /* Once decoded the frame is synthesized to PCM samples. No ERRs + * are reported by mad_synth_frame(); + */ + mad_synth_frame(&m_madSynth, &madFrame); + //qDebug() << "synthlen " << m_madSynth.pcm.length << ", remain " << (frameCount - framesRead); } - ++frames; - - /* Once decoded the frame is synthesized to PCM samples. No ERRs - * are reported by mad_synth_frame(); - */ - mad_synth_frame(Synth,Frame); - - // Number of channels in frame - //ch = MAD_NCHANNELS(&Frame->header); - - /* Synthesized samples must be converted from mad's fixed - * point number to the consumer format (16 bit). Integer samples - * are temporarily stored in a buffer that is flushed when - * full. - */ - - -// qDebug() << "synthlen " << Synth->pcm.length << ", remain " << (samples_wanted-Total_samples_decoded); - no = math_min(Synth->pcm.length,(samples_wanted-Total_samples_decoded)/2); - for (i=0; ipcm.samples[0][i]); - - /* Right channel. If the decoded stream is monophonic then - * the right output channel is the same as the left one. */ - if (m_iChannels==2) - *(destination++) = madScale(Synth->pcm.samples[1][i]); - else - *(destination++) = madScale(Synth->pcm.samples[0][i]); + size_type no = math_min(size_type(m_madSynth.pcm.length - m_madSynthOffset), frameCount - framesRead); + if (isChannelCountMono()) { + for (size_type i = 0; i < no; ++i) { + const sample_type sampleValue = madScale(m_madSynth.pcm.samples[0][m_madSynthOffset + i]); + *(sampleBufferPtr++) = sampleValue; + if (readStereoSamples) { + *(sampleBufferPtr++) = sampleValue; + } + } + } else if (isChannelCountStereo() || readStereoSamples) { + for (size_type i = 0; i < no; ++i) { + *(sampleBufferPtr++) = madScale(m_madSynth.pcm.samples[0][m_madSynthOffset + i]); + *(sampleBufferPtr++) = madScale(m_madSynth.pcm.samples[1][m_madSynthOffset + i]); + } + } else { + for (size_type i = 0; i < no; ++i) { + for (size_type j = 0; j < getChannelCount(); ++j) { + *(sampleBufferPtr++) = madScale(m_madSynth.pcm.samples[j][m_madSynthOffset + i]); + } + } } - Total_samples_decoded += 2*no; - - // qDebug() << "decoded: " << Total_samples_decoded << ", wanted: " << samples_wanted; + m_madSynthOffset += no; + if (m_madSynthOffset >= m_madSynth.pcm.length) { + m_madSynthOffset = 0; + } + framesRead += no; + //qDebug() << "decoded: " << framesRead << ", wanted: " << frameCount; } - - // If samples are still left in buffer, set rest to the index of the unused samples - if (Synth->pcm.length > no) - rest = no; - else - rest = -1; - - // qDebug() << "decoded " << Total_samples_decoded << " samples in " << frames << " frames, rest: " << rest << ", chan " << m_iChannels; - return Total_samples_decoded; + return framesRead; } Result SoundSourceMp3::parseHeader() { @@ -660,15 +513,3 @@ int SoundSourceMp3::findFrame(int pos) return 0; } } - -inline signed int SoundSourceMp3::madScale(mad_fixed_t sample) -{ - sample += (1L << (MAD_F_FRACBITS - 16)); - - if (sample >= MAD_F_ONE) - sample = MAD_F_ONE - 1; - else if (sample < -MAD_F_ONE) - sample = -MAD_F_ONE; - - return sample >> (MAD_F_FRACBITS + 1 - 16); -} diff --git a/src/soundsourcemp3.h b/src/soundsourcemp3.h index ec634883511..e7bd566be3b 100644 --- a/src/soundsourcemp3.h +++ b/src/soundsourcemp3.h @@ -36,8 +36,6 @@ #include #include -#define READLENGTH 5000 - /** Struct used to store mad frames for seeking */ typedef struct MadSeekFrameType { unsigned char *m_pStreamPos; @@ -50,47 +48,50 @@ typedef struct MadSeekFrameType { */ class SoundSourceMp3 : public Mixxx::SoundSource { + typedef SoundSource Super; + public: - ~SoundSourceMp3(); - Result open(); - long seek(long); - unsigned read(unsigned long size, const SAMPLE*); - unsigned long discard(unsigned long size); - /** Return the length of the file in samples. */ - inline long unsigned length(); - Result parseHeader(); - QImage parseCoverArt(); static QList supportedFileExtensions(); explicit SoundSourceMp3(QString qFilename); + ~SoundSourceMp3(); + + Result parseHeader() /*override*/; + QImage parseCoverArt() /*override*/; + + Result open() /*override*/; + + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + private: + void close(); + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); + /** Returns the position of the frame which was found. The found frame is set to * the current element in m_qSeekList */ int findFrame(int pos); - /** Scale the mad sample to be in 16 bit range. */ - inline signed int madScale (mad_fixed_t sample); + size_type discardFrames(size_type frameCount); MadSeekFrameType* getSeekFrame(long frameIndex) const; - // Returns true if the loaded file is valid and usable to read audio. - bool isValid() const; - QFile m_file; - int bitrate; + + unsigned char *inputbuf; + unsigned inputbuf_len; + int framecount; int currentframe; /** current play position. */ mad_timer_t pos; mad_timer_t filelength; - mad_stream *Stream; - mad_frame *Frame; - mad_synth *Synth; - unsigned inputbuf_len; - unsigned char *inputbuf; + enum mad_units units; + mad_stream madStream; + mad_frame madFrame; - /** Start index in Synth buffer of samples left over from previous call to read */ - int rest; - /** Number of channels in file */ - int m_iChannels; + mad_synth m_madSynth; + unsigned short m_madSynthOffset; // left overs from the previous read /** It is not possible to make a precise seek in an mp3 file without decoding the whole stream. * To have precise seek within a limited range from the current decode position, we keep track diff --git a/src/soundsourceoggvorbis.cpp b/src/soundsourceoggvorbis.cpp index 63c2913faf1..ff7d701093e 100644 --- a/src/soundsourceoggvorbis.cpp +++ b/src/soundsourceoggvorbis.cpp @@ -1,225 +1,148 @@ /*************************************************************************** - soundsourceoggvorbis.cpp - ogg vorbis decoder - ------------------- - copyright : (C) 2003 by Svein Magne Bang - email : -***************************************************************************/ + soundsourceoggvorbis.cpp - ogg vorbis decoder + ------------------- + copyright : (C) 2003 by Svein Magne Bang + email : + ***************************************************************************/ /*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -***************************************************************************/ + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ #include "soundsourceoggvorbis.h" #include "soundsourcetaglib.h" -#include "sampleutil.h" #include #include -#include - -#ifdef __WINDOWS__ -#include -#include -#elif __APPLE__ -#include -#elif __LINUX__ -#include -#endif - -inline int getByteOrder() { -#ifdef __LINUX__ - return __BYTE_ORDER == __LITTLE_ENDIAN ? 0 : 1; -#elif __APPLE__ - return CFByteOrderGetCurrent() == CFByteOrderLittleEndian ? 0 : 1; -#else - // Assume little endian. - // TODO(XXX) BSD. - return 0; -#endif - -} - -/* - Class for reading Ogg Vorbis - */ - SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) -: Mixxx::SoundSource(qFilename) -{ - filelength = 0; - setType("ogg"); + : Super(qFilename, "ogg") { + memset(&m_vf, 0, sizeof(m_vf)); } -SoundSourceOggVorbis::~SoundSourceOggVorbis() -{ - if (filelength > 0) { - ov_clear(&vf); +SoundSourceOggVorbis::~SoundSourceOggVorbis() { + if (0 != ov_clear(&m_vf)) { + qWarning() << "Failed to close OggVorbis file:" << getFilename(); } } Result SoundSourceOggVorbis::open() { const QByteArray qBAFilename(getFilename().toLocal8Bit()); -#ifdef __WINDOWS__ - if(ov_fopen(qBAFilename.constData(), &vf) < 0) { - qDebug() << "oggvorbis: Input does not appear to be an Ogg bitstream."; - filelength = 0; - return ERR; - } -#else - FILE *vorbisfile = fopen(qBAFilename.constData(), "r"); - if (!vorbisfile) { - qDebug() << "oggvorbis: cannot open" << getFilename(); + if (0 != ov_fopen(qBAFilename.constData(), &m_vf)) { + qWarning() << "Failed to open OggVorbis file:" << getFilename(); return ERR; } - if(ov_open(vorbisfile, &vf, NULL, 0) < 0) { - qDebug() << "oggvorbis: Input does not appear to be an Ogg bitstream."; - filelength = 0; + if (!ov_seekable(&m_vf)) { + qWarning() << "OggVorbis file is not seekable:" << getFilename(); + close(); return ERR; } -#endif // lookup the ogg's channels and samplerate - vorbis_info * vi = ov_info(&vf, -1); - - channels = vi->channels; - setChannels(channels); - setSampleRate(vi->rate); - - if (channels > 2) { - qDebug() << "oggvorbis: No support for more than 2 channels!"; - ov_clear(&vf); - filelength = 0; + const vorbis_info* vi = ov_info(&m_vf, -1); + if (!vi) { + qWarning() << "Failed to read OggVorbis file:" << getFilename(); + close(); return ERR; } + setChannelCount(vi->channels); + setFrameRate(vi->rate); - // ov_pcm_total returns the total number of frames in the ogg file. The - // frame is the channel-independent measure of samples. The total samples in - // the file is channels * ov_pcm_total. rryan 7/2009 I verified this by - // hand. a 30 second long 48khz mono ogg and a 48khz stereo ogg both report - // 1440000 for ov_pcm_total. - ogg_int64_t ret = ov_pcm_total(&vf, -1); - - if (ret >= 0) { - // We pretend that the file is stereo to the rest of the world. - filelength = ret * 2; - } - else //error - { - if (ret == OV_EINVAL) { - //The file is not seekable. Not sure if any action is needed. - qDebug() << "oggvorbis: file is not seekable " << getFilename(); - } + ogg_int64_t frameCount = ov_pcm_total(&m_vf, -1); + if (0 <= frameCount) { + setFrameCount(frameCount); + } else { + qWarning() << "Failed to read OggVorbis file:" << getFilename(); + close(); + return ERR; } return OK; } -/* - seek to - */ - -long SoundSourceOggVorbis::seek(long filepos) -{ - // In our speak, filepos is a sample in the file abstraction (i.e. it's - // stereo no matter what). filepos/2 is the frame we want to seek to. - if (filepos % 2 != 0) { - qDebug() << "SoundSourceOggVorbis got non-even seek target."; - filepos--; - } - - if (ov_seekable(&vf)) { - if (ov_pcm_seek(&vf, filepos/2) != 0) { - // This is totally common (i.e. you're at EOF). Let's not leave this - // qDebug on. - - // qDebug() << "ogg vorbis: Seek ERR on seekable."; - } - - // Even if an error occured, return them the current position because - // that's what we promised. (Double it because ov_pcm_tell returns - // frames and we pretend to the world that everything is stereo) - return ov_pcm_tell(&vf) * 2; - } else { - qDebug() << "ogg vorbis: Seek ERR at file " << getFilename(); - return 0; +void SoundSourceOggVorbis::close() { + if (0 != ov_clear(&m_vf)) { + qWarning() << "Failed to close OggVorbis file:" << getFilename(); } + Super::reset(); } - -/* - read samples into , and return the number of - samples actually read. - */ - -unsigned SoundSourceOggVorbis::read(volatile unsigned long size, const SAMPLE * destination) { - if (size % 2 != 0) { - qDebug() << "SoundSourceOggVorbis got non-even size in read."; - size--; +Mixxx::AudioSource::diff_type SoundSourceOggVorbis::seekFrame( + diff_type frameIndex) { + int seekResult = ov_pcm_seek(&m_vf, frameIndex); + if (0 != seekResult) { + qWarning() << "Failed to seek OggVorbis file:" << getFilename(); } + return ov_pcm_tell(&m_vf); +} - char *pRead = (char*) destination; - SAMPLE *dest = (SAMPLE*) destination; - - - - // 'needed' is size of buffer in bytes. 'size' is size in SAMPLEs, - // which is 2 bytes. If the stream is mono, we read 'size' bytes, - // so that half the buffer is full, then below we double each - // sample on the left and right channel. If the stream is stereo, - // then ov_read interleaves the samples into the full length of - // the buffer. - - // ov_read speaks bytes, we speak words. needed is the bytes to - // read, not words to read. - - // size is the maximum space in words that we have in - // destination. For stereo files, read the full buffer (size*2 - // bytes). For mono files, only read half the buffer (size bytes), - // and we will double the buffer to be in stereo later. - unsigned int needed = size * channels; - - unsigned int index=0,ret=0; +Mixxx::AudioSource::size_type SoundSourceOggVorbis::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); +} - // loop until requested number of samples has been retrieved - while (needed > 0) { - // read samples into buffer - ret = ov_read(&vf, pRead+index, needed, getByteOrder(), 2, 1, ¤t_section); +Mixxx::AudioSource::size_type SoundSourceOggVorbis::readStereoFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); +} - if (ret <= 0) { - // An error or EOF occured, break out and return what we have sofar. - break; +Mixxx::AudioSource::size_type SoundSourceOggVorbis::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer, + bool readStereoSamples) { + size_type readCount = 0; + sample_type* nextSample = sampleBuffer; + while (readCount < frameCount) { + float** pcmChannels; + int currentSection; + long readResult = ov_read_float(&m_vf, &pcmChannels, + frameCount - readCount, ¤tSection); + if (0 == readResult) { + break; // done + } + if (0 < readResult) { + if (isChannelCountMono()) { + if (readStereoSamples) { + for (long i = 0; i < readResult; ++i) { + *nextSample++ = pcmChannels[0][i]; + *nextSample++ = pcmChannels[0][i]; + } + } else { + for (long i = 0; i < readResult; ++i) { + *nextSample++ = pcmChannels[0][i]; + } + } + } else if (isChannelCountStereo() || readStereoSamples) { + for (long i = 0; i < readResult; ++i) { + *nextSample++ = pcmChannels[0][i]; + *nextSample++ = pcmChannels[1][i]; + } + } else { + for (long i = 0; i < readResult; ++i) { + for (size_type j = 0; j < getChannelCount(); ++j) { + *nextSample++ = pcmChannels[j][i]; + } + } + } + readCount += readResult; + } else { + qWarning() << "Failed to read sample data from OggVorbis file:" + << getFilename(); + break; // abort } - - index += ret; - needed -= ret; - } - - // As of here, index is the total bytes read. (index/2/channels) is the - // total frames read. - - // convert into stereo if file is mono - if (channels == 1) { - SampleUtil::doubleMonoToDualMono(dest, index / 2); - // Pretend we read twice as many bytes as we did, since we just repeated - // each pair of bytes. - index *= 2; } - - // index is the total bytes read, so the words read is index/2 - return index / 2; + return readCount; } /* - Parse the the file to get metadata + Parse the the file to get metadata */ Result SoundSourceOggVorbis::parseHeader() { QByteArray qBAFilename = getFilename().toLocal8Bit(); @@ -256,16 +179,10 @@ QImage SoundSourceOggVorbis::parseCoverArt() { } /* - Return the length of the file in samples. + Return the length of the file in samples. */ -inline long unsigned SoundSourceOggVorbis::length() -{ - return filelength; -} - -QList SoundSourceOggVorbis::supportedFileExtensions() -{ +QList SoundSourceOggVorbis::supportedFileExtensions() { QList list; list.push_back("ogg"); return list; diff --git a/src/soundsourceoggvorbis.h b/src/soundsourceoggvorbis.h index fb50643287f..868b9ca7669 100644 --- a/src/soundsourceoggvorbis.h +++ b/src/soundsourceoggvorbis.h @@ -1,8 +1,8 @@ /*************************************************************************** - soundsourceoggvorbis.h - ogg vorbis decoder - ------------------- - copyright : (C) 2003 by Svein Magne Bang - email : + soundsourceoggvorbis.h - ogg vorbis decoder + ------------------- + copyright : (C) 2003 by Svein Magne Bang + email : ***************************************************************************/ /*************************************************************************** @@ -17,28 +17,38 @@ #ifndef SOUNDSOURCEOGGVORBIS_H #define SOUNDSOURCEOGGVORBIS_H -#include +#include "soundsource.h" + #define OV_EXCLUDE_STATIC_CALLBACKS #include -#include "soundsource.h" +class SoundSourceOggVorbis: public Mixxx::SoundSource { + typedef SoundSource Super; + +public: + static QList supportedFileExtensions(); + + explicit SoundSourceOggVorbis(QString qFilename); + ~SoundSourceOggVorbis(); + + Result parseHeader() /*override*/; + QImage parseCoverArt() /*override*/; + + Result open() /*override*/; + + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; + size_type readStereoFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; + +private: + void close(); + + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer, bool readStereoSamples); -class SoundSourceOggVorbis : public Mixxx::SoundSource { - public: - explicit SoundSourceOggVorbis(QString qFilename); - ~SoundSourceOggVorbis(); - Result open(); - long seek(long); - unsigned read(unsigned long size, const SAMPLE*); - inline long unsigned length(); - Result parseHeader(); - QImage parseCoverArt(); - static QList supportedFileExtensions(); - private: - int channels; - unsigned long filelength; - OggVorbis_File vf; - int current_section; + OggVorbis_File m_vf; }; #endif diff --git a/src/soundsourceopus.cpp b/src/soundsourceopus.cpp index 36c612ab371..d05cb4fd71b 100644 --- a/src/soundsourceopus.cpp +++ b/src/soundsourceopus.cpp @@ -1,178 +1,119 @@ -// soundsourceopus.cpp - Opus decoder -// Create by 14/01/2013 Tuukka Pasanen -// Based on work 2003 by Svein Magne Bang - - #include "soundsourceopus.h" #include "soundsourcetaglib.h" -#include -#ifdef __WINDOWS__ -#include -#include -#elif __APPLE__ -#include -#elif __LINUX__ -#include -#endif - // Include this if taglib if new enough (version 1.9.1 have opusfile) #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) #include #endif -inline int getByteOrder() { -#ifdef __LINUX__ - return __BYTE_ORDER == __LITTLE_ENDIAN ? 0 : 1; -#elif __APPLE__ - return CFByteOrderGetCurrent() == CFByteOrderLittleEndian ? 0 : 1; -#else - // Assume little endian. - // TODO(XXX) BSD. - return 0; -#endif - +namespace { +// All Opus audio is encoded at 48 kHz +Mixxx::AudioSource::size_type kOpusSampleRate = 48000; } -// -// Class for reading Xiph Opus -// - SoundSourceOpus::SoundSourceOpus(QString qFilename) - : Mixxx::SoundSource(qFilename), - m_ptrOpusFile(NULL), - m_lFilelength(0) { - this->setType("opus"); + : Super(qFilename, "opus"), m_pOggOpusFile(NULL) { } SoundSourceOpus::~SoundSourceOpus() { - if (m_ptrOpusFile) { - op_free(m_ptrOpusFile); + close(); + if (m_pOggOpusFile) { + op_free(m_pOggOpusFile); } } Result SoundSourceOpus::open() { - int error = 0; const QByteArray qBAFilename(getFilename().toLocal8Bit()); - m_ptrOpusFile = op_open_file(qBAFilename.constData(), &error); - if (m_ptrOpusFile == NULL) { - qDebug() << "opus: Input does not appear to be an Opus bitstream."; - m_lFilelength = 0; + int errorCode = 0; + m_pOggOpusFile = op_open_file(qBAFilename.constData(), &errorCode); + if (!m_pOggOpusFile) { + qDebug() << "Failed to open OggOpus file:" << getFilename() + << "errorCode" << errorCode; return ERR; } - // opusfile lib all ways gives you 48000 samplerate and stereo 16 bit sample - this->setBitrate(op_bitrate_instant(m_ptrOpusFile)); - this->setSampleRate(48000); - this->setChannels(2); - - if (getChannels() > 2) { - qDebug() << "opus: No support for more than 2 channels!"; - op_free(m_ptrOpusFile); - m_lFilelength = 0; + if (!op_seekable(m_pOggOpusFile)) { + qWarning() << "OggOpus file is not seekable:" << getFilename(); + close(); return ERR; } - // op_pcm_total returns the total number of frames in the ogg file. The - // frame is the channel-independent measure of samples. The total samples in - // the file is m_iChannels * ov_pcm_total. rryan 7/2009 I verified this by - // hand. a 30 second long 48khz mono ogg and a 48khz stereo ogg both report - // 1440000 for op_pcm_total. - qint64 ret = op_pcm_total(m_ptrOpusFile, -1) * 2; - - // qDebug() << getFilename() << "chan:" << m_iChannels << "sample:" << m_iSampleRate << "LEN:" << ret; - + setChannelCount(op_channel_count(m_pOggOpusFile, -1)); + setFrameRate(kOpusSampleRate); - if (ret >= 0) { - // We pretend that the file is stereo to the rest of the world. - m_lFilelength = ret; - } else { //error - if (ret == OP_EINVAL) { - //The file is not seekable. Not sure if any action is needed. - qDebug() << "opus: file is not seekable " << getFilename(); - } + ogg_int64_t frameCount = op_pcm_total(m_pOggOpusFile, -1); + if (0 <= frameCount) { + setFrameCount(frameCount); + } else { + qWarning() << "Failed to read OggOpus file:" << getFilename(); + close(); + return ERR; } return OK; } -/* - seek to - */ - -long SoundSourceOpus::seek(long filepos) { - // In our speak, filepos is a sample in the file abstraction (i.e. it's - // stereo no matter what). filepos/2 is the frame we want to seek to. - if (filepos % 2 != 0) { - qDebug() << "SoundSourceOpus got non-even seek target."; - filepos--; +void SoundSourceOpus::close() { + if (m_pOggOpusFile) { + op_free(m_pOggOpusFile); + m_pOggOpusFile = NULL; } + Super::reset(); +} - if (op_seekable(m_ptrOpusFile)) { - // I can't say why filepos have to divide by two - // Have no idea.. probably seek point is mono.. - if (op_pcm_seek(m_ptrOpusFile, filepos / 2) != 0) { - // This is totally common (i.e. you're at EOF). Let's not leave this - // qDebug on. - - qDebug() << "opus: Seek ERR on seekable."; - } - - // qDebug() << "Wanted:" << filepos << "GOT:" << op_pcm_tell(m_ptrOpusFile); - - - //return op_pcm_tell(m_ptrOpusFile); - // We are here allways! - return filepos; - } else { - qDebug() << "opus: Seek ERR at file " << getFilename(); - return 0; +Mixxx::AudioSource::diff_type SoundSourceOpus::seekFrame(diff_type frameIndex) { + int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); + if (0 != seekResult) { + qWarning() << "Failed to seek OggVorbis file:" << getFilename(); } - return filepos; + return op_pcm_tell(m_pOggOpusFile); } - -/* - read samples into , and return the number of - samples actually read. - */ - -unsigned SoundSourceOpus::read(volatile unsigned long size, const SAMPLE * destination) { - if (size % 2 != 0) { - qDebug() << "SoundSourceOpus got non-even size in read."; - size--; +Mixxx::AudioSource::size_type SoundSourceOpus::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + size_type readCount = 0; + while (readCount < frameCount) { + int readResult = op_read_float(m_pOggOpusFile, + sampleBuffer + frames2samples(readCount), + frames2samples(frameCount - readCount), NULL); + if (0 == readResult) { + break; // done + } + if (0 < readResult) { + readCount += readResult; + } else { + qWarning() << "Failed to read sample data from OggOpus file:" + << getFilename(); + break; // abort + } } + return readCount; +} - // SAMPLE and opus_int16 are mostly same - // So just make pointer and hope for the best - opus_int16 *l_iDest = (opus_int16 *) destination; - - unsigned int l_iNeeded = size; - unsigned int l_iReaded = 0; - unsigned int l_iRet=0; - - // loop until requested number of samples has been retrieved - while (l_iNeeded > 0) { - // read samples into buffer - //ret = op_read_stereo(m_ptrOpusFile, l_iPcm, sizeof(l_iPcm)/sizeof(*l_iPcm)); - l_iRet = op_read_stereo(m_ptrOpusFile, l_iDest, l_iNeeded); - - if (l_iRet <= 0) { - // An error or EOF occured, break out and return what we have sofar. - break; +Mixxx::AudioSource::size_type SoundSourceOpus::readStereoFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + size_type readCount = 0; + while (readCount < frameCount) { + int readResult = op_read_float_stereo(m_pOggOpusFile, + sampleBuffer + (readCount * 2), + (frameCount - readCount) * 2); + if (0 == readResult) { + break; // done + } + if (0 < readResult) { + readCount += readResult; + } else { + qWarning() << "Failed to read sample data from OggOpus file:" + << getFilename(); + break; // abort } - - l_iNeeded -= l_iRet * 2; - l_iReaded += l_iRet * 2; - l_iDest += l_iRet * 2; } - - return l_iReaded; + return readCount; } /* - Parse the the file to get metadata + Parse the the file to get metadata */ Result SoundSourceOpus::parseHeader() { int error = 0; @@ -180,11 +121,11 @@ Result SoundSourceOpus::parseHeader() { QByteArray qBAFilename = getFilename().toLocal8Bit(); OggOpusFile *l_ptrOpusFile = op_open_file(qBAFilename.constData(), &error); - this->setBitrate((int)op_bitrate(l_ptrOpusFile, -1) / 1000); - this->setSampleRate(48000); - this->setChannels(2); - qint64 l_lLength = op_pcm_total(l_ptrOpusFile, -1) * getChannels(); - this->setDuration(l_lLength / (getSampleRate() * getChannels())); + + setChannels(op_channel_count(l_ptrOpusFile, -1)); + setSampleRate(kOpusSampleRate); + setBitrate(op_bitrate(l_ptrOpusFile, -1) / 1000); + setDuration(op_pcm_total(l_ptrOpusFile, -1) / getSampleRate()); // If we don't have new enough Taglib we use libopusfile parser! #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) @@ -212,42 +153,41 @@ Result SoundSourceOpus::parseHeader() { int i = 0; const OpusTags *l_ptrOpusTags = op_tags(l_ptrOpusFile, -1); - // This is left for debug reasons !! // qDebug() << "opus: We have " << l_ptrOpusTags->comments; for (i = 0; i < l_ptrOpusTags->comments; ++i) { - QString l_SWholeTag = QString(l_ptrOpusTags->user_comments[i]); - QString l_STag = l_SWholeTag.left(l_SWholeTag.indexOf("=")); - QString l_SPayload = l_SWholeTag.right((l_SWholeTag.length() - l_SWholeTag.indexOf("=")) - 1); + QString l_SWholeTag = QString(l_ptrOpusTags->user_comments[i]); + QString l_STag = l_SWholeTag.left(l_SWholeTag.indexOf("=")); + QString l_SPayload = l_SWholeTag.right((l_SWholeTag.length() - l_SWholeTag.indexOf("=")) - 1); - if (!l_STag.compare("ARTIST")) { + if (!l_STag.compare("ARTIST")) { this->setArtist(l_SPayload); - } else if (!l_STag.compare("ALBUM")) { + } else if (!l_STag.compare("ALBUM")) { this->setAlbum(l_SPayload); - } else if (!l_STag.compare("BPM")) { + } else if (!l_STag.compare("BPM")) { this->setBpm(l_SPayload.toFloat()); - } else if (!l_STag.compare("YEAR") || !l_STag.compare("DATE")) { + } else if (!l_STag.compare("YEAR") || !l_STag.compare("DATE")) { this->setYear(l_SPayload); - } else if (!l_STag.compare("GENRE")) { + } else if (!l_STag.compare("GENRE")) { this->setGenre(l_SPayload); - } else if (!l_STag.compare("TRACKNUMBER")) { + } else if (!l_STag.compare("TRACKNUMBER")) { this->setTrackNumber(l_SPayload); - } else if (!l_STag.compare("COMPOSER")) { + } else if (!l_STag.compare("COMPOSER")) { this->setComposer(l_SPayload); - } else if (!l_STag.compare("ALBUMARTIST")) { + } else if (!l_STag.compare("ALBUMARTIST")) { this->setAlbumArtist(l_SPayload); - } else if (!l_STag.compare("TITLE")) { + } else if (!l_STag.compare("TITLE")) { this->setTitle(l_SPayload); - } else if (!l_STag.compare("REPLAYGAIN_TRACK_PEAK")) { - } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) { + } else if (!l_STag.compare("REPLAYGAIN_TRACK_PEAK")) { + } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) { this->setReplayGainString (l_SPayload); - } else if (!l_STag.compare("REPLAYGAIN_ALBUM_PEAK")) { - } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) { - } + } else if (!l_STag.compare("REPLAYGAIN_ALBUM_PEAK")) { + } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) { + } - // This is left fot debug reasons!! - //qDebug() << "Comment" << i << l_ptrOpusTags->comment_lengths[i] << - //" (" << l_ptrOpusTags->user_comments[i] << ")" << l_STag << "*" << l_SPayload; + // This is left fot debug reasons!! + //qDebug() << "Comment" << i << l_ptrOpusTags->comment_lengths[i] << + //" (" << l_ptrOpusTags->user_comments[i] << ")" << l_STag << "*" << l_SPayload; } op_free(l_ptrOpusFile); @@ -257,13 +197,9 @@ Result SoundSourceOpus::parseHeader() { } /* - Return the length of the file in samples. + Return the length of the file in samples. */ -inline long unsigned SoundSourceOpus::length() { - return m_lFilelength; -} - QList SoundSourceOpus::supportedFileExtensions() { QList list; list.push_back("opus"); diff --git a/src/soundsourceopus.h b/src/soundsourceopus.h index 008fc92acd4..cab046bfc89 100644 --- a/src/soundsourceopus.h +++ b/src/soundsourceopus.h @@ -1,33 +1,35 @@ -// soundsourceopus.cpp - Opus decoder -// Create by 14/01/2013 Tuukka Pasanen -// Based on work 2003 by Svein Magne Bang - #ifndef SOUNDSOURCEOPUS_H #define SOUNDSOURCEOPUS_H -#include -#include +#include "soundsource.h" + #define OV_EXCLUDE_STATIC_CALLBACKS #include -#include "soundsource.h" +class SoundSourceOpus: public Mixxx::SoundSource { + typedef SoundSource Super; -class SoundSourceOpus : public Mixxx::SoundSource { - public: - explicit SoundSourceOpus(QString qFilename); - virtual ~SoundSourceOpus(); - - Result open(); - long seek(long); - unsigned read(unsigned long size, const SAMPLE*); - inline long unsigned length(); - Result parseHeader(); - QImage parseCoverArt(); +public: static QList supportedFileExtensions(); - - private: - OggOpusFile *m_ptrOpusFile; - quint64 m_lFilelength; + + explicit SoundSourceOpus(QString qFilename); + ~SoundSourceOpus(); + + Result parseHeader() /*override*/; + QImage parseCoverArt() /*override*/; + + Result open() /*override*/; + + diff_type seekFrame(diff_type frameIndex); + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; + size_type readStereoFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; + +private: + void close(); + + OggOpusFile *m_pOggOpusFile; }; #endif diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index d396b2e9933..a782389ad22 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -310,12 +310,12 @@ Mixxx::SoundSourcePointer SoundSourceProxy::open() const { return Mixxx::SoundSourcePointer(); } - if (0 >= m_pSoundSource->getChannels()) { - qWarning() << "Invalid number of channels:" << m_pSoundSource->getChannels(); + if (!m_pSoundSource->isChannelCountValid()) { + qWarning() << "Invalid number of channels" << m_pSoundSource->getChannelCount(); return Mixxx::SoundSourcePointer(); } - if (0 >= m_pSoundSource->getSampleRate()) { - qWarning() << "Invalid sample rate:" << m_pSoundSource->getSampleRate(); + if (!m_pSoundSource->isFrameRateValid()) { + qWarning() << "Invalid frame rate:" << m_pSoundSource->getFrameRate(); return Mixxx::SoundSourcePointer(); } diff --git a/src/soundsourcesndfile.cpp b/src/soundsourcesndfile.cpp index 8b6528db2aa..59c8a5db1a0 100644 --- a/src/soundsourcesndfile.cpp +++ b/src/soundsourcesndfile.cpp @@ -1,48 +1,21 @@ -/*************************************************************************** - soundsourcesndfile.cpp - description - ------------------- - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : -***************************************************************************/ - -/*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -***************************************************************************/ - #include "soundsourcesndfile.h" #include "soundsourcetaglib.h" -#include "sampleutil.h" -#include "util/math.h" - #include #include #include #include -#include -#include - - /* - Class for reading files using libsndfile + Class for reading files using libsndfile */ SoundSourceSndFile::SoundSourceSndFile(QString qFilename) - : Mixxx::SoundSource(qFilename), - fh(NULL), - channels(0), - filelength(0) { - // Must be set to 0 per the API for reading (non-RAW files) - memset(&info, 0, sizeof(info)); + : Super(qFilename), m_pSndFile(NULL) { + memset(&m_sfInfo, 0, sizeof(m_sfInfo)); } SoundSourceSndFile::~SoundSourceSndFile() { - sf_close(fh); + close(); } QList SoundSourceSndFile::supportedFileExtensions() { @@ -58,105 +31,71 @@ Result SoundSourceSndFile::open() { #ifdef __WINDOWS__ // Pointer valid until string changed LPCWSTR lpcwFilename = (LPCWSTR)getFilename().utf16(); - fh = sf_wchar_open(lpcwFilename, SFM_READ, &info); + m_pSndFile = sf_wchar_open(lpcwFilename, SFM_READ, &m_sfInfo); #else const QByteArray qbaFilename(getFilename().toLocal8Bit()); - fh = sf_open(qbaFilename.constData(), SFM_READ, &info); + m_pSndFile = sf_open(qbaFilename.constData(), SFM_READ, &m_sfInfo); #endif - if (fh == NULL) { // sf_format_check is only for writes - qWarning() << "libsndfile: Error opening file" << getFilename() << sf_strerror(fh); + if (m_pSndFile == NULL) { // sf_format_check is only for writes + qWarning() << "libsndfile: Error opening file" << getFilename() + << sf_strerror(m_pSndFile); return ERR; } - if (sf_error(fh)>0) { - qWarning() << "libsndfile: Error opening file" << getFilename() << sf_strerror(fh); + if (sf_error(m_pSndFile) > 0) { + qWarning() << "libsndfile: Error opening file" << getFilename() + << sf_strerror(m_pSndFile); + close(); return ERR; } - channels = info.channels; - setChannels(channels); - setSampleRate(info.samplerate); - // This is the 'virtual' filelength. No matter how many channels the file - // actually has, we pretend it has 2. - filelength = info.frames * 2; // File length with two interleaved channels + setChannelCount(m_sfInfo.channels); + setFrameRate(m_sfInfo.samplerate); + setFrameCount(m_sfInfo.frames); return OK; } -long SoundSourceSndFile::seek(long filepos) -{ - unsigned long filepos2 = (unsigned long)filepos; - if (filelength>0) - { - filepos2 = math_min(filepos2,filelength); - sf_seek(fh, (sf_count_t)filepos2/2, SEEK_SET); - //Note that we don't error check sf_seek because it reports - //benign errors under normal usage (ie. we sometimes seek past the end - //of a song, and it will stop us.) - return filepos2; +void SoundSourceSndFile::close() { + if (m_pSndFile) { + if (0 != sf_close(m_pSndFile)) { + qWarning() << "Failed to close file:" << getFilename() + << sf_strerror(m_pSndFile); + } + memset(&m_sfInfo, 0, sizeof(m_sfInfo)); } - return 0; + Super::reset(); } -/* - read samples into , and return the number of - samples actually read. A sample is a single float representing a - sample on one channel of the audio. In the case of a monaural file - then size/2 samples are read from the mono file, and they are - doubled into stereo. - */ -unsigned SoundSourceSndFile::read(unsigned long size, const SAMPLE * destination) -{ - SAMPLE * dest = (SAMPLE *)destination; - if (filelength > 0) - { - if (channels==2) - { - unsigned long no = sf_read_short(fh, dest, size); - - // rryan 2/2009 This code used to lie and say we read - // 'size' samples no matter what. I left this array - // zeroing code here in case the Reader doesn't check - // against this. - for (unsigned long i=no; i 0) { - setDuration(info.frames / info.samplerate); + if (m_sfInfo.samplerate > 0) { + setDuration(m_sfInfo.frames / m_sfInfo.samplerate); } else { qDebug() << "WARNING: WAV file with invalid samplerate." - << "Can't get duration using libsndfile."; + << "Can't get duration using libsndfile."; } } - } else { + } else if (getType().startsWith("aif")) { // Try AIFF TagLib::RIFF::AIFF::File f(qBAFilename.constData()); if (!readFileHeader(this, f)) { @@ -227,6 +165,8 @@ Result SoundSourceSndFile::parseHeader() } else { return ERR; } + } else { + return ERR; } return OK; @@ -234,8 +174,6 @@ Result SoundSourceSndFile::parseHeader() QImage SoundSourceSndFile::parseCoverArt() { QImage coverArt; - QString location = getFilename(); - setType(location.section(".",-1).toLower()); const QByteArray qBAFilename(getFilename().toLocal8Bit()); if (getType() == "flac") { @@ -256,7 +194,7 @@ QImage SoundSourceSndFile::parseCoverArt() { std::list::iterator it = covers.begin(); TagLib::FLAC::Picture* cover = *it; coverArt = QImage::fromData( - QByteArray(cover->data().data(), cover->data().size())); + QByteArray(cover->data().data(), cover->data().size())); } } } else if (getType() == "wav") { @@ -265,7 +203,7 @@ QImage SoundSourceSndFile::parseCoverArt() { if (id3v2) { coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); } - } else { + } else if (getType().startsWith("aif")) { // Try AIFF TagLib::RIFF::AIFF::File f(qBAFilename.constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); @@ -273,13 +211,6 @@ QImage SoundSourceSndFile::parseCoverArt() { coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); } } - return coverArt; -} -/* - Return the length of the file in samples. - */ -inline long unsigned SoundSourceSndFile::length() -{ - return filelength; + return coverArt; } diff --git a/src/soundsourcesndfile.h b/src/soundsourcesndfile.h index df6ea10948f..238f767fb53 100644 --- a/src/soundsourcesndfile.h +++ b/src/soundsourcesndfile.h @@ -1,24 +1,8 @@ -/*************************************************************************** - soundsourcesndfile.h - description - ------------------- - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #ifndef SOUNDSOURCESNDFILE_H #define SOUNDSOURCESNDFILE_H #include "soundsource.h" -#include + #ifdef Q_OS_WIN //Enable unicode in libsndfile on Windows //(sf_open uses UTF-8 otherwise) @@ -27,25 +11,29 @@ #endif #include +class SoundSourceSndFile: public Mixxx::SoundSource { + typedef SoundSource Super; -class SoundSourceSndFile : public Mixxx::SoundSource -{ public: + static QList supportedFileExtensions(); + explicit SoundSourceSndFile(QString qFilename); ~SoundSourceSndFile(); - Result open(); - long seek(long); - unsigned read(unsigned long size, const SAMPLE*); - inline long unsigned length(); - Result parseHeader(); - QImage parseCoverArt(); - static QList supportedFileExtensions(); + + Result parseHeader() /*override*/; + QImage parseCoverArt() /*override*/; + + Result open() /*override*/; + + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; private: - SNDFILE *fh; - SF_INFO info; - int channels; - unsigned long filelength; + void close(); + + SNDFILE* m_pSndFile; + SF_INFO m_sfInfo; }; #endif diff --git a/src/soundsourcetaglib.cpp b/src/soundsourcetaglib.cpp index 3cba92f713f..cb42025a711 100644 --- a/src/soundsourcetaglib.cpp +++ b/src/soundsourcetaglib.cpp @@ -74,17 +74,17 @@ bool readFileHeader(SoundSource* pSoundSource, const TagLib::File& f) { if (f.isValid()) { const TagLib::AudioProperties *properties = f.audioProperties(); if (properties) { + pSoundSource->setChannels(properties->channels()); + pSoundSource->setSampleRate(properties->sampleRate()); pSoundSource->setDuration(properties->length()); pSoundSource->setBitrate(properties->bitrate()); - pSoundSource->setSampleRate(properties->sampleRate()); - pSoundSource->setChannels(properties->channels()); if (s_bDebugMetadata) { qDebug() << "TagLib" - << "duration" << pSoundSource->getDuration() - << "bitrate" << pSoundSource->getBitrate() + << "channels" << pSoundSource->getChannels() << "sampleRate" << pSoundSource->getSampleRate() - << "channels" << pSoundSource->getChannels(); + << "bitrate" << pSoundSource->getBitrate() + << "duration" << pSoundSource->getDuration(); } return true; diff --git a/src/trackinfoobject.cpp b/src/trackinfoobject.cpp index b80db5bf1b2..34c020e5d71 100644 --- a/src/trackinfoobject.cpp +++ b/src/trackinfoobject.cpp @@ -205,14 +205,15 @@ void TrackInfoObject::parse(bool parseCoverArt) { setGrouping(pSoundSource->getGrouping()); setComment(pSoundSource->getComment()); setTrackNumber(pSoundSource->getTrackNumber()); + setChannels(pSoundSource->getChannels()); + setSampleRate(pSoundSource->getSampleRate()); + setDuration(pSoundSource->getDuration()); + setBitrate(pSoundSource->getBitrate()); + float replayGain = pSoundSource->getReplayGain(); - if (replayGain != 0) { + if (replayGain != 0.0f) { setReplayGain(replayGain); } - setDuration(pSoundSource->getDuration()); - setBitrate(pSoundSource->getBitrate()); - setSampleRate(pSoundSource->getSampleRate()); - setChannels(pSoundSource->getChannels()); // Need to set BPM after sample rate since beat grid creation depends on // knowing the sample rate. Bug #1020438. From 0b06194371e811d7e4f2cc351a0ef2154d65d426 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 3 Dec 2014 00:50:45 +0100 Subject: [PATCH 013/481] Rewrite of the M4A decoder without legacy code --- plugins/soundsourcem4a/m4a/comment.h | 30 -- plugins/soundsourcem4a/m4a/ip.h | 99 ----- plugins/soundsourcem4a/m4a/mp4-mixxx.cpp | 510 ---------------------- plugins/soundsourcem4a/m4a/mp4.c | 407 ----------------- plugins/soundsourcem4a/m4a/sf.h | 61 --- plugins/soundsourcem4a/soundsourcem4a.cpp | 350 +++++++++++---- plugins/soundsourcem4a/soundsourcem4a.h | 29 +- 7 files changed, 293 insertions(+), 1193 deletions(-) delete mode 100644 plugins/soundsourcem4a/m4a/comment.h delete mode 100644 plugins/soundsourcem4a/m4a/ip.h delete mode 100644 plugins/soundsourcem4a/m4a/mp4-mixxx.cpp delete mode 100644 plugins/soundsourcem4a/m4a/mp4.c delete mode 100644 plugins/soundsourcem4a/m4a/sf.h diff --git a/plugins/soundsourcem4a/m4a/comment.h b/plugins/soundsourcem4a/m4a/comment.h deleted file mode 100644 index 1c87326a31b..00000000000 --- a/plugins/soundsourcem4a/m4a/comment.h +++ /dev/null @@ -1,30 +0,0 @@ -#ifndef _COMMENT_H -#define _COMMENT_H - -struct keyval { - char *key; - char *val; -}; - -struct growing_keyvals { - struct keyval *comments; - int alloc; - int count; -}; - -#define GROWING_KEYVALS(name) struct growing_keyvals name = { NULL, 0, 0 } - -struct keyval *comments_dup(const struct keyval *comments); -void comments_free(struct keyval *comments); - -/* case insensitive key */ -const char *comments_get_val(const struct keyval *comments, const char *key); -const char *comments_get_albumartist(const struct keyval *comments); -int comments_get_int(const struct keyval *comments, const char *key); -int comments_get_date(const struct keyval *comments, const char *key); - -int comments_add(struct growing_keyvals *c, const char *key, char *val); -int comments_add_const(struct growing_keyvals *c, const char *key, const char *val); -void comments_terminate(struct growing_keyvals *c); - -#endif diff --git a/plugins/soundsourcem4a/m4a/ip.h b/plugins/soundsourcem4a/m4a/ip.h deleted file mode 100644 index c5fc97f5e6a..00000000000 --- a/plugins/soundsourcem4a/m4a/ip.h +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2004-2005 Timo Hirvonen - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of the - * License, or (at your option) any later version. - * - * 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 - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA - * 02111-1307, USA. - */ - -#ifndef _IP_H -#define _IP_H - -#include "comment.h" -#include "sf.h" - -enum { - /* no error */ - IP_ERROR_SUCCESS, - /* system error (error code in errno) */ - IP_ERROR_ERRNO, - /* file type not supported */ - IP_ERROR_UNRECOGNIZED_FILE_TYPE, - /* function not supported (usually seek) */ - IP_ERROR_FUNCTION_NOT_SUPPORTED, - /* input plugin detected corrupted file */ - IP_ERROR_FILE_FORMAT, - /* malformed uri */ - IP_ERROR_INVALID_URI, - /* sample format not supported */ - IP_ERROR_SAMPLE_FORMAT, - /* error parsing response line / headers */ - IP_ERROR_HTTP_RESPONSE, - /* usually 404 */ - IP_ERROR_HTTP_STATUS, - /* */ - IP_ERROR_INTERNAL -}; - -/* -struct input_plugin_data { - // filled by ip-layer - char *filename; - int fd; - - unsigned int remote : 1; - unsigned int metadata_changed : 1; - - // shoutcast - int counter; - int metaint; - char *metadata; - - // filled by plugin - sample_format_t sf; - void * private_ipd; -}; -*/ - -struct input_plugin_data { - // filled by ip-layer -// QString filename; - char *filename; - int fd; - - unsigned int remote : 1; - unsigned int metadata_changed : 1; - - // shoutcast - int counter; - int metaint; - char *metadata; - - // filled by plugin - sample_format_t sf; - void * private_ipd; -}; - - -struct input_plugin_ops { - int (*open)(struct input_plugin_data *ip_data); - int (*close)(struct input_plugin_data *ip_data); - int (*read)(struct input_plugin_data *ip_data, char *buffer, int count); - int (*seek)(struct input_plugin_data *ip_data, double offset); - int (*read_comments)(struct input_plugin_data *ip_data, - struct keyval **comments); - int (*duration)(struct input_plugin_data *ip_data); -}; - -#endif diff --git a/plugins/soundsourcem4a/m4a/mp4-mixxx.cpp b/plugins/soundsourcem4a/m4a/mp4-mixxx.cpp deleted file mode 100644 index 9d5882eabec..00000000000 --- a/plugins/soundsourcem4a/m4a/mp4-mixxx.cpp +++ /dev/null @@ -1,510 +0,0 @@ -// mp4-mixxx.cpp -// This file is a hopefully shortlived fork + convertsion to C++ of the M4A audio playback plugin from the C* Music Player (cmus) project. -// The original file mp4.c is also in this directory. -// -// This forked and converted by Garth and Albert in Summer 2008 to support M4A playback in Mixxx -// -// g++ $(pkg-config --cflags QtCore) $(pkg-config --libs-only-l QtCore) -lmp4v2 -lfaad -o mp4-mixxx mp4-mixxx.cpp -// -#include "util/types.h" - -#include - -namespace -{ - // AAC: "... each block decodes to 1024 time-domain samples." - std::size_t kFramesPerBlock = 1024; - - // 32-bit float - std::size_t kBytesPerSample = sizeof(CSAMPLE); - - // Only mono or stereo channels are supported - std::size_t kMaxChannels = 2; -} - -/* - * Copyright 2006 dnk - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of the - * License, or (at your option) any later version. - * - * 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 - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA - * 02111-1307, USA. - */ - -#include "ip.h" -// #include "xmalloc.h" -// #include "debug.h" -// #include "file.h" - -#ifdef __MP4V2__ - #include -#else - #include -#endif - -#include - -#include -#include -#include -#ifndef _MSC_VER - #include -#endif - -#ifdef _MSC_VER - #define S_ISDIR(mode) (mode & _S_IFDIR) - #define strcasecmp stricmp - #define strncasecmp strnicmp -#endif - -#ifdef __M4AHACK__ - typedef uint32_t SAMPLERATE_TYPE; -#else - typedef unsigned long SAMPLERATE_TYPE; -#endif - -struct mp4_private { - char *overflow_buf; - int overflow_buf_len; - - unsigned char *aac_data; - unsigned int aac_data_len; - - char *sample_buf; - unsigned int sample_buf_frame; - unsigned int sample_buf_len; - - unsigned char channels; - unsigned long sample_rate; - - faacDecHandle decoder; /* typedef void * */ - - struct { - MP4FileHandle handle; /* typedef void * */ - - MP4TrackId track; - MP4SampleId sample; - MP4SampleId num_samples; - } mp4; -}; - - -static MP4TrackId mp4_get_track(MP4FileHandle *handle) -{ - MP4TrackId num_tracks; - const char *track_type; - uint8_t obj_type; - MP4TrackId i; - - num_tracks = MP4GetNumberOfTracks(handle, NULL, 0); - - for (i = 1; i <= num_tracks; i++) { - track_type = MP4GetTrackType(handle, i); - if (!track_type) - continue; - - if (!MP4_IS_AUDIO_TRACK_TYPE(track_type)) - continue; - - /* MP4GetTrackAudioType */ - obj_type = MP4GetTrackEsdsObjectTypeId(handle, i); - if (obj_type == MP4_INVALID_AUDIO_TYPE) - continue; - - if (obj_type == MP4_MPEG4_AUDIO_TYPE) { - obj_type = MP4GetTrackAudioMpeg4Type(handle, i); - - if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(obj_type)) - return i; - } else { - if (MP4_IS_AAC_AUDIO_TYPE(obj_type)) - return i; - } - } - - return MP4_INVALID_TRACK_ID; -} - -static void mp4_init(struct input_plugin_data *ip_data) { - memset(ip_data, 0, sizeof(*ip_data)); -} - -static int mp4_open(struct input_plugin_data *ip_data) -{ - struct mp4_private *priv; - faacDecConfigurationPtr neaac_cfg; - unsigned char *buf; - unsigned int buf_size; - - /* http://sourceforge.net/forum/message.php?msg_id=3578887 */ - if (ip_data->remote) - return -IP_ERROR_FUNCTION_NOT_SUPPORTED; - - /* init private_ipd struct */ - // priv = xnew0(struct mp4_private, 1); - priv = new mp4_private(); - //priv = (mp4_private*) calloc(1, sizeof(mp4_private)); - // FIXME: there was some alloc error checking in the orgininal ver - memset(priv, 0, sizeof(*priv)); - - priv->overflow_buf_len = 0; - priv->overflow_buf = NULL; - - priv->sample_buf_len = kFramesPerBlock * kBytesPerSample * kMaxChannels; - priv->sample_buf = new char[priv->sample_buf_len]; - priv->sample_buf_frame = -1; - - ip_data->private_ipd = priv; - - priv->decoder = faacDecOpen(); - /* set decoder config */ - neaac_cfg = faacDecGetCurrentConfiguration(priv->decoder); - neaac_cfg->outputFormat = FAAD_FMT_FLOAT; /* force 32-bit float */ - neaac_cfg->downMatrix = 1; /* 5.1 -> stereo */ - neaac_cfg->defObjectType = LC; - //qDebug() << "Decoder Config" << neaac_cfg->defObjectType - // << neaac_cfg->defSampleRate - // << neaac_cfg->useOldADTSFormat - // << neaac_cfg->dontUpSampleImplicitSBR; - faacDecSetConfiguration(priv->decoder, neaac_cfg); - - /* open mpeg-4 file, check for >= ver 1.9.1 */ -#if MP4V2_PROJECT_version_hex <= 0x00010901 - priv->mp4.handle = MP4Read(ip_data->filename, 0); -#else - priv->mp4.handle = MP4Read(ip_data->filename); -#endif - if (!priv->mp4.handle) { - qDebug() << "MP4Read failed"; - goto out; - } - - /* find aac audio track */ - priv->mp4.track = mp4_get_track((MP4FileHandle*)priv->mp4.handle); - if (priv->mp4.track == MP4_INVALID_TRACK_ID) { - qDebug() << "MP4FindTrackId failed"; - goto out; - } - - // Allocate AAC read buffer - priv->aac_data_len = MP4GetTrackMaxSampleSize(priv->mp4.handle, priv->mp4.track); - priv->aac_data = new unsigned char[priv->aac_data_len]; - - priv->mp4.num_samples = MP4GetTrackNumberOfSamples(priv->mp4.handle, priv->mp4.track); - // MP4 frames are 1-indexed - priv->mp4.sample = 1; - - buf = NULL; - buf_size = 0; - if (!MP4GetTrackESConfiguration(priv->mp4.handle, priv->mp4.track, &buf, &buf_size)) { - /* failed to get mpeg-4 audio config... this is ok. - * faacDecInit2() will simply use default values instead. - */ - qDebug() << "Didn't get MP4 Audio Config (not a bad thing)"; - buf = NULL; - buf_size = 0; - } - - /* init decoder according to mpeg-4 audio config */ - if (faacDecInit2(priv->decoder, buf, buf_size, - (SAMPLERATE_TYPE*)&priv->sample_rate, &priv->channels) < 0) { - free(buf); - goto out; - } - free(buf); - - // qDebug() << "sample rate "<< priv->sample_rate <<"hz, channels" << priv->channels; - - ip_data->sf = sf_rate(priv->sample_rate) | sf_channels(priv->channels) | sf_bits(16) | sf_signed(1); -#if defined(WORDS_BIGENDIAN) - ip_data->sf |= sf_bigendian(1); -#endif - - return 0; -out: - if (priv->mp4.handle) - MP4Close(priv->mp4.handle); - if (priv->decoder) - faacDecClose(priv->decoder); - delete [] priv->sample_buf; - delete [] priv->aac_data; - delete priv; - return -IP_ERROR_FILE_FORMAT; -} - -static int mp4_close(struct input_plugin_data *ip_data) { - if ((0 == ip_data) || (0 == ip_data->private_ipd)) { - return 0; // nothing to do - } - - struct mp4_private *priv; - priv = (mp4_private*) ip_data->private_ipd; - - if (priv->mp4.handle) - MP4Close(priv->mp4.handle); - - if (priv->decoder) - faacDecClose(priv->decoder); - - if (priv->sample_buf) { - delete [] priv->sample_buf; - } - - if (priv->aac_data) { - delete [] priv->aac_data; - } - - delete priv; - ip_data->private_ipd = NULL; - - mp4_init(ip_data); - - return 0; -} - -/* returns -1 on fatal errors - * returns -2 on non-fatal errors - * 0 on eof - * number of bytes put in 'buffer' on success */ -static int decode_one_frame(struct input_plugin_data *ip_data, void *buffer, int count) -{ - struct mp4_private *priv = (mp4_private*) ip_data->private_ipd; - faacDecFrameInfo frame_info; - int bytes; - - //BUG_ON(priv->overflow_buf_len); - - if (priv->mp4.sample > priv->mp4.num_samples) - return 0; /* EOF */ - - unsigned char *aac_data = priv->aac_data; - unsigned int aac_data_len = priv->aac_data_len; - - // If you do this, then MP4ReadSample allocates the buffer for you. We don't - // want this because it's slow. - // unsigned char *aac_data = NULL; - // unsigned int aac_data_len = 0; - - int this_frame = priv->mp4.sample; - if (MP4ReadSample(priv->mp4.handle, priv->mp4.track, this_frame, - &aac_data, &aac_data_len, - NULL, NULL, NULL, NULL) == 0) { - qWarning() << "m4a: error reading mp4 sample" << priv->mp4.sample; - errno = EINVAL; - return -1; - } - - if (!aac_data) { - qWarning() << "m4a: aac_data == NULL"; - errno = EINVAL; - return -1; - } - - char* sample_buf = priv->sample_buf; - int sample_buf_len = priv->sample_buf_len; - - NeAACDecDecode2(priv->decoder, - &frame_info, - aac_data, aac_data_len, - (void**)&sample_buf, sample_buf_len); - - // qDebug() << "Sample frame" << priv->mp4.sample - // << "has" << frame_info.samples << "samples" - // << frame_info.bytesconsumed << "bytes" - // << frame_info.channels << "channels" - // << frame_info.error << "error" - // << frame_info.samplerate << "samplerate"; - - if (!sample_buf || frame_info.bytesconsumed <= 0) { - qWarning() << "m4a fatal error:" << faacDecGetErrorMessage(frame_info.error); - errno = EINVAL; - return -1; - } - - if (frame_info.error != 0) { - qDebug() << "frame error:" << faacDecGetErrorMessage(frame_info.error); - return -2; - } - - if (frame_info.samples <= 0) { - return -2; - } - - if (frame_info.channels != priv->channels || - frame_info.samplerate != priv->sample_rate) { - qDebug() << "invalid channel or sample_rate count\n"; - return -2; - } - - // The frame read was successful - priv->sample_buf_frame = this_frame; - priv->mp4.sample++; - - bytes = frame_info.samples * kBytesPerSample; - - if (bytes > count) { - /* decoded too much; keep overflow. */ - //memcpy(priv->overflow_buf_base, sample_buf + count, bytes - count); - //priv->overflow_buf = priv->overflow_buf_base; - - priv->overflow_buf = sample_buf + count; - priv->overflow_buf_len = bytes - count; - memcpy(buffer, sample_buf, count); - return count; - } - - memcpy(buffer, sample_buf, bytes); - return bytes; -} - -static int mp4_read(struct input_plugin_data *ip_data, char *buffer, int count) -{ - struct mp4_private *priv = (mp4_private*) ip_data->private_ipd; - int rc; - - /* use overflow from previous call (if any) */ - if (priv->overflow_buf_len > 0) { - int len = priv->overflow_buf_len; - - if (len > count) - len = count; - - memcpy(buffer, priv->overflow_buf, len); - priv->overflow_buf += len; - priv->overflow_buf_len -= len; - - //qDebug() << "Reading" << len << "from overflow." - // << priv->overflow_buf_len << "overflow remains"; - - return len; - } - - do { - rc = decode_one_frame(ip_data, buffer, count); - } while (rc == -2); - - return rc; -} - -static int mp4_channels(struct input_plugin_data *ip_data) { - struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; - return priv->channels; -} - -static int mp4_sample_rate(struct input_plugin_data *ip_data) { - struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; - return priv->sample_rate; -} - -static int mp4_total_frames(struct input_plugin_data *ip_data) { - struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; - return priv->mp4.num_samples * kFramesPerBlock; -} - -static int mp4_current_sample(struct input_plugin_data *ip_data) { - struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; - if (priv->overflow_buf_len == 0) { - return priv->mp4.sample; - } else { - // -1 because if overflow buf is filled then mp4.sample is incremented, and - // the samples in the overflow buf are for sample - 1 - int samples_per_block = kFramesPerBlock * priv->channels; - Q_ASSERT(0 == (priv->overflow_buf_len % kBytesPerSample)); - int overflow_samples = samples_per_block - (priv->overflow_buf_len / kBytesPerSample); - return (priv->mp4.sample - 1) + overflow_samples; - } -} - -static int mp4_current_frame(struct input_plugin_data *ip_data) { - struct mp4_private *priv = (struct mp4_private*)ip_data->private_ipd; - int current_sample = mp4_current_sample(ip_data); - return current_sample / priv->channels; -} - -static int mp4_seek_frame(struct input_plugin_data *ip_data, int frame) -{ - struct mp4_private *priv; - priv = (mp4_private*) ip_data->private_ipd; - - Q_ASSERT(frame >= 0); - unsigned int block_for_frame = 1 + (frame / kFramesPerBlock); - unsigned int block_offset_frames = frame % kFramesPerBlock; - unsigned int block_offset_samples = block_offset_frames * priv->channels; - unsigned int frame_offset_bytes = block_offset_samples * kBytesPerSample; - - //qDebug() << "Seeking to" << block_for_frame << ":" << frame_offset; - - // Invalid sample requested -- return the current position. - if (block_for_frame < 1 || block_for_frame > priv->mp4.num_samples) - return mp4_current_frame(ip_data); - - // We don't have the current frame decoded -- decode it. - if (priv->sample_buf_frame != block_for_frame) { - - // We might have to 'prime the pump' if this isn't the first frame. The - // decoder has internal state that it builds as it plays, and just seeking - // to the frame we want will result in poor audio quality (clicks and - // pops). This is akin to seeking in a video and seeing MPEG - // artifacts. Figure out how many frames we need to go backward -- 1 seems - // to work. - const unsigned int how_many_backwards = 1; - int start_frame = math_max(block_for_frame - how_many_backwards, 1U); - priv->mp4.sample = start_frame; - - // rryan 9/2009 -- the documentation is sketchy on this, but I think that - // it tells the decoder that you are seeking so it should flush its state - faacDecPostSeekReset(priv->decoder, priv->mp4.sample); - - // Loop until the current frame is past the frame we intended to read - // (i.e. we have decoded how_many_backwards + 1 frames). The desidered - // decoded frame will be stored in the overflow buffer, since we're asking - // to read 0 bytes. - int result; - do { - result = decode_one_frame(ip_data, 0, 0); - if (result < 0) qDebug() << "SEEK_ERROR"; - } while (result == -2 || priv->mp4.sample <= block_for_frame); - - if (result == -1 || result == 0) { - return mp4_current_frame(ip_data); - } - } else { - qDebug() << "Seek within frame"; - } - - // Now the overflow buffer contains the sample we want to seek to. Fix the - // overflow buffer so that the next call to read() will read starting with the - // requested sample. - priv->overflow_buf = priv->sample_buf; - priv->overflow_buf += frame_offset_bytes; - priv->overflow_buf_len -= frame_offset_bytes; - - return mp4_current_frame(ip_data); -} - -static int mp4_duration(struct input_plugin_data *ip_data) -{ - struct mp4_private *priv; - uint32_t scale; - uint64_t duration; - - priv = (mp4_private*) ip_data->private_ipd; - - scale = MP4GetTrackTimeScale(priv->mp4.handle, priv->mp4.track); - if (scale == 0) - return 0; - - duration = MP4GetTrackDuration(priv->mp4.handle, priv->mp4.track); - - return duration / scale; -} diff --git a/plugins/soundsourcem4a/m4a/mp4.c b/plugins/soundsourcem4a/m4a/mp4.c deleted file mode 100644 index b4a5a531794..00000000000 --- a/plugins/soundsourcem4a/m4a/mp4.c +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright 2006 dnk - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of the - * License, or (at your option) any later version. - * - * 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 - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA - * 02111-1307, USA. - */ - -#include "ip.h" -#include "xmalloc.h" -#include "debug.h" -#include "file.h" - -#ifdef __MP4V2__ - #include -#else - #include -#endif -#include - -#include -#include -#include -#include - -struct mp4_private { - char *overflow_buf; - int overflow_buf_len; - - unsigned char channels; - unsigned long sample_rate; - - faacDecHandle decoder; /* typedef void * */ - - struct { - MP4FileHandle handle; /* typedef void * */ - - MP4TrackId track; - MP4SampleId sample; - MP4SampleId num_samples; - } mp4; -}; - - -static MP4TrackId mp4_get_track(MP4FileHandle *handle) -{ - MP4TrackId num_tracks; - const char *track_type; - uint8_t obj_type; - MP4TrackId i; - - num_tracks = MP4GetNumberOfTracks(handle, NULL, 0); - - for (i = 1; i <= num_tracks; i++) { - track_type = MP4GetTrackType(handle, i); - if (!track_type) - continue; - - if (!MP4_IS_AUDIO_TRACK_TYPE(track_type)) - continue; - - /* MP4GetTrackAudioType */ - obj_type = MP4GetTrackEsdsObjectTypeId(handle, i); - if (obj_type == MP4_INVALID_AUDIO_TYPE) - continue; - - if (obj_type == MP4_MPEG4_AUDIO_TYPE) { - obj_type = MP4GetTrackAudioMpeg4Type(handle, i); - - if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(obj_type)) - return i; - } else { - if (MP4_IS_AAC_AUDIO_TYPE(obj_type)) - return i; - } - } - - return MP4_INVALID_TRACK_ID; -} - -static int mp4_open(struct input_plugin_data *ip_data) -{ - struct mp4_private *priv; - faacDecConfigurationPtr neaac_cfg; - unsigned char *buf; - unsigned int buf_size; - - - /* http://sourceforge.net/forum/message.php?msg_id=3578887 */ - if (ip_data->remote) - return -IP_ERROR_FUNCTION_NOT_SUPPORTED; - - /* init private struct */ - priv = xnew0(struct mp4_private, 1); - ip_data->private = priv; - - priv->decoder = faacDecOpen(); - - /* set decoder config */ - neaac_cfg = faacDecGetCurrentConfiguration(priv->decoder); - neaac_cfg->outputFormat = FAAD_FMT_16BIT; /* force 16 bit audio */ - neaac_cfg->downMatrix = 1; /* 5.1 -> stereo */ - faacDecSetConfiguration(priv->decoder, neaac_cfg); - - /* open mpeg-4 file */ - priv->mp4.handle = MP4Read(ip_data->filename, 0); - if (!priv->mp4.handle) { - d_print("MP4Read failed\n"); - goto out; - } - - /* find aac audio track */ - priv->mp4.track = mp4_get_track(priv->mp4.handle); - if (priv->mp4.track == MP4_INVALID_TRACK_ID) { - d_print("MP4FindTrackId failed\n"); - goto out; - } - - priv->mp4.num_samples = MP4GetTrackNumberOfSamples(priv->mp4.handle, priv->mp4.track); - - priv->mp4.sample = 1; - - buf = NULL; - buf_size = 0; - if (!MP4GetTrackESConfiguration(priv->mp4.handle, priv->mp4.track, &buf, &buf_size)) { - /* failed to get mpeg-4 audio config... this is ok. - * faacDecInit2() will simply use default values instead. - */ - buf = NULL; - buf_size = 0; - } - - /* init decoder according to mpeg-4 audio config */ - if (faacDecInit2(priv->decoder, buf, buf_size, &priv->sample_rate, &priv->channels) < 0) { - free(buf); - goto out; - } - - free(buf); - - d_print("sample rate %luhz, channels %u\n", priv->sample_rate, priv->channels); - - ip_data->sf = sf_rate(priv->sample_rate) | sf_channels(priv->channels) | sf_bits(16) | sf_signed(1); -#if defined(WORDS_BIGENDIAN) - ip_data->sf |= sf_bigendian(1); -#endif - - return 0; - -out: - if (priv->mp4.handle) - MP4Close(priv->mp4.handle); - if (priv->decoder) - faacDecClose(priv->decoder); - free(priv); - return -IP_ERROR_FILE_FORMAT; -} - -static int mp4_close(struct input_plugin_data *ip_data) -{ - struct mp4_private *priv; - - priv = ip_data->private; - - if (priv->mp4.handle) - MP4Close(priv->mp4.handle); - - if (priv->decoder) - faacDecClose(priv->decoder); - - free(priv); - ip_data->private = NULL; - - return 0; -} - -/* returns -1 on fatal errors - * returns -2 on non-fatal errors - * 0 on eof - * number of bytes put in 'buffer' on success */ -static int decode_one_frame(struct input_plugin_data *ip_data, void *buffer, int count) -{ - struct mp4_private *priv; - unsigned char *aac_data = NULL; - unsigned int aac_data_len = 0; - faacDecFrameInfo frame_info; - char *sample_buf; - int bytes; - - priv = ip_data->private; - - BUG_ON(priv->overflow_buf_len); - - if (priv->mp4.sample > priv->mp4.num_samples) - return 0; /* EOF */ - - if (MP4ReadSample(priv->mp4.handle, priv->mp4.track, priv->mp4.sample, - &aac_data, &aac_data_len, NULL, NULL, NULL, NULL) == 0) { - d_print("error reading mp4 sample %d\n", priv->mp4.sample); - errno = EINVAL; - return -1; - } - - priv->mp4.sample++; - - if (!aac_data) { - d_print("aac_data == NULL\n"); - errno = EINVAL; - return -1; - } - - sample_buf = faacDecDecode(priv->decoder, &frame_info, aac_data, aac_data_len); - - free(aac_data); - - if (!sample_buf || frame_info.bytesconsumed <= 0) { - d_print("fatal error: %s\n", faacDecGetErrorMessage(frame_info.error)); - errno = EINVAL; - return -1; - } - - if (frame_info.error != 0) { - d_print("frame error: %s\n", faacDecGetErrorMessage(frame_info.error)); - return -2; - } - - if (frame_info.samples <= 0) - return -2; - - if (frame_info.channels != priv->channels || frame_info.samplerate != priv->sample_rate) { - d_print("invalid channel or sample_rate count\n"); - return -2; - } - - /* 16-bit samples */ - bytes = frame_info.samples * 2; - - if (bytes > count) { - /* decoded too much; keep overflow. */ - priv->overflow_buf = sample_buf + count; - priv->overflow_buf_len = bytes - count; - memcpy(buffer, sample_buf, count); - return count; - } else { - memcpy(buffer, sample_buf, bytes); - } - - return bytes; -} - -static int mp4_read(struct input_plugin_data *ip_data, char *buffer, int count) -{ - struct mp4_private *priv; - int rc; - - priv = ip_data->private; - - /* use overflow from previous call (if any) */ - if (priv->overflow_buf_len > 0) { - int len = priv->overflow_buf_len; - - if (len > count) - len = count; - - memcpy(buffer, priv->overflow_buf, len); - priv->overflow_buf += len; - priv->overflow_buf_len -= len; - - return len; - } - - do { - rc = decode_one_frame(ip_data, buffer, count); - } while (rc == -2); - - return rc; -} - -static int mp4_seek(struct input_plugin_data *ip_data, double offset) -{ - struct mp4_private *priv; - MP4SampleId sample; - uint32_t scale; - - priv = ip_data->private; - - scale = MP4GetTrackTimeScale(priv->mp4.handle, priv->mp4.track); - if (scale == 0) - return -IP_ERROR_INTERNAL; - - sample = MP4GetSampleIdFromTime(priv->mp4.handle, priv->mp4.track, - (MP4Timestamp)(offset * (double)scale), 0); - if (sample == MP4_INVALID_SAMPLE_ID) - return -IP_ERROR_INTERNAL; - - priv->mp4.sample = sample; - - d_print("seeking to sample %d\n", sample); - - return 0; -} - -static int mp4_read_comments(struct input_plugin_data *ip_data, - struct keyval **comments) -{ - struct mp4_private *priv; - uint16_t meta_num, meta_total; - uint8_t val; - /*uint8_t *ustr; - uint32_t size;*/ - char *str; - GROWING_KEYVALS(c); - - priv = ip_data->private; - - /* MP4GetMetadata* provides malloced pointers, and the data - * is in UTF-8 (or at least it should be). */ - if (MP4GetMetadataArtist(priv->mp4.handle, &str)) - comments_add(&c, "artist", str); - if (MP4GetMetadataAlbum(priv->mp4.handle, &str)) - comments_add(&c, "album", str); - if (MP4GetMetadataName(priv->mp4.handle, &str)) - comments_add(&c, "title", str); - if (MP4GetMetadataGenre(priv->mp4.handle, &str)) - comments_add(&c, "genre", str); - if (MP4GetMetadataYear(priv->mp4.handle, &str)) - comments_add(&c, "date", str); - - if (MP4GetMetadataCompilation(priv->mp4.handle, &val)) - comments_add_const(&c, "compilation", val ? "yes" : "no"); -#if 0 - if (MP4GetBytesProperty(priv->mp4.handle, "moov.udta.meta.ilst.aART.data", &ustr, &size)) { - char *xstr; - - /* What's this? - * This is the result from lack of documentation. - * It's supposed to return just a string, but it - * returns an additional 16 bytes of junk at the - * beginning. Could be a bug. Could be intentional. - * Hopefully this works around it: - */ - if (ustr[0] == 0 && size > 16) { - ustr += 16; - size -= 16; - } - xstr = xmalloc(size + 1); - memcpy(xstr, ustr, size); - xstr[size] = 0; - comments_add(&c, "albumartist", xstr); - free(xstr); - } -#endif - if (MP4GetMetadataTrack(priv->mp4.handle, &meta_num, &meta_total)) { - char buf[6]; - snprintf(buf, 6, "%u", meta_num); - comments_add_const(&c, "tracknumber", buf); - } - if (MP4GetMetadataDisk(priv->mp4.handle, &meta_num, &meta_total)) { - char buf[6]; - snprintf(buf, 6, "%u", meta_num); - comments_add_const(&c, "discnumber", buf); - } - - comments_terminate(&c); - *comments = c.comments; - return 0; -} - -static int mp4_duration(struct input_plugin_data *ip_data) -{ - struct mp4_private *priv; - uint32_t scale; - uint64_t duration; - - priv = ip_data->private; - - scale = MP4GetTrackTimeScale(priv->mp4.handle, priv->mp4.track); - if (scale == 0) - return 0; - - duration = MP4GetTrackDuration(priv->mp4.handle, priv->mp4.track); - - return duration / scale; -} - -const struct input_plugin_ops ip_ops = { - .open = mp4_open, - .close = mp4_close, - .read = mp4_read, - .seek = mp4_seek, - .read_comments = mp4_read_comments, - .duration = mp4_duration -}; - -const char * const ip_extensions[] = { "mp4", "m4a", "m4b", NULL }; -const char * const ip_mime_types[] = { /*"audio/mp4", "audio/mp4a-latm",*/ NULL }; diff --git a/plugins/soundsourcem4a/m4a/sf.h b/plugins/soundsourcem4a/m4a/sf.h deleted file mode 100644 index 001409b7c69..00000000000 --- a/plugins/soundsourcem4a/m4a/sf.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2004 Timo Hirvonen - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation; either version 2 of the - * License, or (at your option) any later version. - * - * 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 - * General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA - * 02111-1307, USA. - */ - -#ifndef _SF_H -#define _SF_H - -/* - * 0 1 big_endian 0-1 - * 1 1 is_signed 0-1 - * 2 1 unused 0 - * 3-5 3 bits >> 3 0-7 (* 8 = 0-56) - * 6-23 18 rate 0-262143 - * 24-31 8 channels 0-255 - */ -typedef unsigned int sample_format_t; - -#define SF_BIGENDIAN_MASK 0x00000001 -#define SF_SIGNED_MASK 0x00000002 -#define SF_BITS_MASK 0x00000038 -#define SF_RATE_MASK 0x00ffffc0 -#define SF_CHANNELS_MASK 0xff000000 - -#define SF_BIGENDIAN_SHIFT 0 -#define SF_SIGNED_SHIFT 1 -#define SF_BITS_SHIFT 0 -#define SF_RATE_SHIFT 6 -#define SF_CHANNELS_SHIFT 24 - -#define sf_get_bigendian(sf) (((sf) & SF_BIGENDIAN_MASK) >> SF_BIGENDIAN_SHIFT) -#define sf_get_signed(sf) (((sf) & SF_SIGNED_MASK ) >> SF_SIGNED_SHIFT) -#define sf_get_bits(sf) (((sf) & SF_BITS_MASK ) >> SF_BITS_SHIFT) -#define sf_get_rate(sf) (((sf) & SF_RATE_MASK ) >> SF_RATE_SHIFT) -#define sf_get_channels(sf) (((sf) & SF_CHANNELS_MASK ) >> SF_CHANNELS_SHIFT) - -#define sf_bigendian(val) (((val) << SF_BIGENDIAN_SHIFT) & SF_BIGENDIAN_MASK) -#define sf_signed(val) (((val) << SF_SIGNED_SHIFT ) & SF_SIGNED_MASK) -#define sf_bits(val) (((val) << SF_BITS_SHIFT ) & SF_BITS_MASK) -#define sf_rate(val) (((val) << SF_RATE_SHIFT ) & SF_RATE_MASK) -#define sf_channels(val) (((val) << SF_CHANNELS_SHIFT ) & SF_CHANNELS_MASK) - -#define sf_get_sample_size(sf) (sf_get_bits((sf)) >> 3) -#define sf_get_frame_size(sf) (sf_get_sample_size((sf)) * sf_get_channels((sf))) -#define sf_get_second_size(sf) (sf_get_rate((sf)) * sf_get_frame_size((sf))) - -#endif diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index e6da7f104f5..105f59dd553 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -1,8 +1,8 @@ /*************************************************************************** - soundsourcem4a.cpp - mp4/m4a decoder - ------------------- - copyright : (C) 2008 by Garth Dahlstrom - email : ironstorm@users.sf.net + soundsourcem4a.cpp - mp4/m4a decoder + ------------------- + copyright : (C) 2008 by Garth Dahlstrom + email : ironstorm@users.sf.net ***************************************************************************/ /*************************************************************************** @@ -16,114 +16,311 @@ #include "soundsourcem4a.h" #include "soundsourcetaglib.h" +#include "sampleutil.h" #include -#include - -#ifdef __MP4V2__ - #include -#else - #include -#endif - #ifdef __WINDOWS__ #include #include #endif -#include "m4a/mp4-mixxx.cpp" +#ifdef _MSC_VER +#define S_ISDIR(mode) (mode & _S_IFDIR) +#define strcasecmp stricmp +#define strncasecmp strnicmp +#endif + +#ifdef __M4AHACK__ +typedef uint32_t SAMPLERATE_TYPE; +#else +typedef unsigned long SAMPLERATE_TYPE; +#endif namespace Mixxx { +namespace { +// AAC: "... each block decodes to 1024 time-domain samples." +const AudioSource::size_type kFramesPerSampleBlock = 1024; + +// MP4SampleId is 1-based +const MP4SampleId kMinSampleId = 1; + +// Decoding will be restarted one or more blocks of samples +// before the actual position to avoid audible glitches. +// One block of samples seems to be enough here. +const MP4SampleId kSampleIdPrefetchCount = 1; +} + SoundSourceM4A::SoundSourceM4A(QString qFileName) - : Super(qFileName, "m4a") - , trackId(0) { - mp4_init(&ipd); + : Super(qFileName, "m4a"), m_hFile(MP4_INVALID_FILE_HANDLE), m_trackId( + MP4_INVALID_TRACK_ID), m_maxSampleId(MP4_INVALID_SAMPLE_ID), m_curSampleId( + MP4_INVALID_SAMPLE_ID), m_inputBufferOffset(0), m_inputBufferLength( + 0), m_hDecoder(NULL), m_curFrameIndex(0) { } SoundSourceM4A::~SoundSourceM4A() { - delete[] ipd.filename; - mp4_close(&ipd); + close(); } -Result SoundSourceM4A::open() -{ - //Initialize the FAAD2 decoder... - return initializeDecoder(); +namespace { +MP4TrackId findAacTrackId(MP4FileHandle hFile) { + const MP4TrackId maxTrackId = MP4GetNumberOfTracks(hFile, NULL, 0); + for (MP4TrackId trackId = 1; trackId <= maxTrackId; ++trackId) { + const char* trackType = MP4GetTrackType(hFile, trackId); + if ((NULL == trackType) || !MP4_IS_AUDIO_TRACK_TYPE(trackType)) { + continue; + } + const char* mediaDataName = MP4GetTrackMediaDataName(hFile, trackId); + if (0 != strcasecmp(mediaDataName, "mp4a")) { + continue; + } + const u_int8_t audioType = MP4GetTrackEsdsObjectTypeId(hFile, trackId); + if (MP4_INVALID_AUDIO_TYPE == audioType) { + continue; + } + if (MP4_MPEG4_AUDIO_TYPE == audioType) { + const u_int8_t audioMpeg4Type = MP4GetTrackAudioMpeg4Type(hFile, + trackId); + if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(audioMpeg4Type)) { + return trackId; + } + } else if (MP4_IS_AAC_AUDIO_TYPE(audioType)) { + return trackId; + } + } + return MP4_INVALID_TRACK_ID; +} } -Result SoundSourceM4A::initializeDecoder() -{ - // Copy QString to char[] buffer for mp4_open to read from later - const QByteArray qbaFileName(getFilename().toLocal8Bit()); - int bytes = qbaFileName.length() + 1; - ipd.filename = new char[bytes]; - strncpy(ipd.filename, qbaFileName.constData(), bytes); - ipd.filename[bytes-1] = '\0'; - ipd.remote = false; // File is not an stream - // The file was loading and failing erratically because - // ipd.remote was an in an uninitialized state, it needed to be - // set to false. - - int mp4_open_status = mp4_open(&ipd); - if (mp4_open_status != 0) { - qWarning() << "SSM4A::initializeDecoder failed" - << getFilename() << " with status:" << mp4_open_status; +Result SoundSourceM4A::open() { + if (MP4_INVALID_FILE_HANDLE != m_hFile) { + qWarning() << "File has already been opened!"; return ERR; } - // mp4_open succeeded -> populate variables - int channels = mp4_channels(&ipd); - if (2 < channels) { - qWarning() << "SSM4A::initializeDecoder failed" - << getFilename() << "unsupported number of channels" << channels; + /* open MP4 file, check for >= ver 1.9.1 */ + const QByteArray qbaFilename(getFilename().toLocal8Bit()); +#if MP4V2_PROJECT_version_hex <= 0x00010901 + m_hFile = MP4Read(qbaFilename.constData(), 0); +#else + m_hFile = MP4Read(qbaFilename.constData()); +#endif + if (MP4_INVALID_FILE_HANDLE == m_hFile) { + close(); + qWarning() << "Failed to open file for reading:" << getFilename(); return ERR; } - setChannelCount(channels); - setFrameRate(mp4_sample_rate(&ipd)); - setFrameCount(mp4_total_frames(&ipd)); + m_trackId = findAacTrackId(m_hFile); + if (MP4_INVALID_TRACK_ID == m_trackId) { + close(); + qWarning() << "No AAC track found in file:" << getFilename(); + return ERR; + } - setDuration(mp4_duration(&ipd)); + m_maxSampleId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); + if (MP4_INVALID_SAMPLE_ID == m_maxSampleId) { + close(); + qWarning() << "Failed to read file structure:" << getFilename(); + return ERR; + } + m_curSampleId = MP4_INVALID_SAMPLE_ID; - qDebug() << "#channels" << getChannelCount() - << "frame rate" << getFrameRate() - << "#frames" << getFrameCount() - << "duration" << getDuration(); + m_inputBuffer.resize(MP4GetTrackMaxSampleSize(m_hFile, m_trackId), 0); + m_inputBufferOffset = 0; + m_inputBufferLength = 0; + + if (!m_hDecoder) { + // lazy initialization of the AAC decoder + m_hDecoder = faacDecOpen(); + if (!m_hDecoder) { + qWarning() << "Failed to open the AAC decoder!"; + close(); + return ERR; + } + faacDecConfigurationPtr pDecoderConfig = faacDecGetCurrentConfiguration( + m_hDecoder); + pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ + pDecoderConfig->downMatrix = 1; /* 5.1 -> stereo */ + pDecoderConfig->defObjectType = LC; + if (!faacDecSetConfiguration(m_hDecoder, pDecoderConfig)) { + close(); + qWarning() << "Failed to configure AAC decoder!"; + return ERR; + } + } + + u_int8_t* configBuffer = NULL; + u_int32_t configBufferSize = 0; + if (!MP4GetTrackESConfiguration(m_hFile, m_trackId, &configBuffer, + &configBufferSize)) { + /* failed to get mpeg-4 audio config... this is ok. + * faacDecInit2() will simply use default values instead. + */ + qWarning() + << "Failed to read the MP4 audio configuration. Continuing with default values."; + } + + SAMPLERATE_TYPE sampleRate; + unsigned char channelCount; + if (0 > faacDecInit2(m_hDecoder, configBuffer, configBufferSize, + &sampleRate, &channelCount)) { + free(configBuffer); + close(); + qWarning() << "Failed to initialize the AAC decoder!"; + return ERR; + } else { + free(configBuffer); + } + + setChannelCount(channelCount); + setFrameRate(sampleRate); + setFrameCount(m_maxSampleId * kFramesPerSampleBlock); + + const u_int32_t timeScale = MP4GetTrackTimeScale(m_hFile, m_trackId); + const MP4Duration duration = MP4GetTrackDuration(m_hFile, m_trackId); + setDuration(duration / timeScale); + + qDebug() << "#channels" << getChannelCount() << "frame rate" + << getFrameRate() << "#frames" << getFrameCount() << "duration" + << getDuration(); + + const SampleBuffer::size_type prefetchSampleBufferSize = (kSampleIdPrefetchCount + 1) * frames2samples(kFramesPerSampleBlock); + SampleBuffer(prefetchSampleBufferSize).swap(m_prefetchSampleBuffer); + + // invalidate current frame index + m_curFrameIndex = getFrameCount(); + // seek to beginning of file + if (0 != seekFrame(0)) { + close(); + qWarning() << "Failed to seek to the beginning of the file!"; + return ERR; + } return OK; } +void SoundSourceM4A::close() { + if (m_hDecoder) { + NeAACDecClose(m_hDecoder); + m_hDecoder = NULL; + } + if (MP4_INVALID_FILE_HANDLE != m_hFile) { + MP4Close(m_hFile); + m_hFile = MP4_INVALID_FILE_HANDLE; + } + m_trackId = MP4_INVALID_TRACK_ID; + m_maxSampleId = MP4_INVALID_SAMPLE_ID; + m_curSampleId = MP4_INVALID_SAMPLE_ID; + m_inputBuffer.clear(); + m_inputBufferOffset = 0; + m_inputBufferLength = 0; + m_curFrameIndex = 0; + Super::reset(); +} + +bool SoundSourceM4A::isValidSampleId(MP4SampleId sampleId) const { + return (sampleId >= kMinSampleId) && (sampleId <= m_maxSampleId); +} + AudioSource::diff_type SoundSourceM4A::seekFrame(diff_type frameIndex) { - return mp4_seek_frame(&ipd, frameIndex); + if (m_curFrameIndex != frameIndex) { + const MP4SampleId sampleId = kMinSampleId + + (frameIndex / kFramesPerSampleBlock); + if (!isValidSampleId(sampleId)) { + return m_curFrameIndex; + } + if ((m_curSampleId != sampleId) || (frameIndex < m_curFrameIndex)) { + // restart decoding one or more blocks of samples + // backwards to avoid audible glitches + m_curSampleId = math_max(sampleId - kSampleIdPrefetchCount, kMinSampleId); + m_curFrameIndex = (m_curSampleId - kMinSampleId) * kFramesPerSampleBlock; + // rryan 9/2009 -- the documentation is sketchy on this, but I think that + // it tells the decoder that you are seeking so it should flush its state + faacDecPostSeekReset(m_hDecoder, m_curSampleId); + // discard input buffer + m_inputBufferOffset = 0; + m_inputBufferLength = 0; + } + // decoding starts before the actual target position + Q_ASSERT(m_curFrameIndex <= frameIndex); + const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; + // prefetch (decode and discard) all samples up to the target position + Q_ASSERT(frames2samples(prefetchFrameCount) <= m_prefetchSampleBuffer.size()); + readFrameSamplesInterleaved(prefetchFrameCount, &m_prefetchSampleBuffer[0]); + } + Q_ASSERT(m_curFrameIndex == frameIndex); + return frameIndex; } -AudioSource::size_type SoundSourceM4A::readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) { - // We want to read a total of "frameCount" frames, and the mp4_read() - // function wants to know how many bytes we want to decode. One - // sample is 32-bits = 4 bytes here. One block contains 1024 - // frames with samples for each channel. - const size_type bytes_per_block = kFramesPerBlock * getChannelCount() * sizeof(sample_type); - const size_type total_samples_to_decode = frames2samples(frameCount); - const size_type total_bytes_to_decode = total_samples_to_decode * sizeof(sample_type); - size_type total_bytes_decoded = 0; - char* byteBuffer = reinterpret_cast(sampleBuffer); - do { - const size_type num_bytes_req = math_min(bytes_per_block, total_bytes_to_decode - total_bytes_decoded); - const int numRead = mp4_read(&ipd, byteBuffer, num_bytes_req); - if (numRead <= 0) { - //qDebug() << "SSM4A::read: EOF"; - break; +AudioSource::size_type SoundSourceM4A::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + sample_type* pSampleBuffer = sampleBuffer; + const diff_type readFrameIndex = m_curFrameIndex; + size_type readFrameCount = m_curFrameIndex - readFrameIndex; + while ((readFrameCount = m_curFrameIndex - readFrameIndex) < frameCount) { + if (isValidSampleId(m_curSampleId) && (0 == m_inputBufferLength)) { + // fill input buffer with block of samples + InputBuffer::value_type* pInputBuffer = &m_inputBuffer[0]; + m_inputBufferOffset = 0; + m_inputBufferLength = m_inputBuffer.size(); // in/out parameter + if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleId++, + &pInputBuffer, &m_inputBufferLength, + NULL, NULL, NULL, NULL)) { + qWarning() << "Failed to read MP4 sample data" << m_curSampleId; + break; // abort + } + } + if (0 == m_inputBufferLength) { + // no more input data available (EOF) + break;// done + } + faacDecFrameInfo decFrameInfo; + decFrameInfo.bytesconsumed = 0; + decFrameInfo.samples = 0; + // decode samples into sampleBuffer + const size_type readFrameCount = m_curFrameIndex - readFrameIndex; + const size_type decodeBufferCapacityInBytes = frames2samples( + frameCount - readFrameCount) * sizeof(*sampleBuffer); + Q_ASSERT(0 < decodeBufferCapacityInBytes); + void* pDecodeBuffer = pSampleBuffer; + NeAACDecDecode2(m_hDecoder, &decFrameInfo, + &m_inputBuffer[m_inputBufferOffset], + m_inputBufferLength / sizeof(m_inputBuffer[0]), + &pDecodeBuffer, decodeBufferCapacityInBytes); + // samples should have been decoded into our own buffer + Q_ASSERT(pSampleBuffer == pDecodeBuffer); + pSampleBuffer += decFrameInfo.samples; + // only the input data that is available should have been read + Q_ASSERT( + decFrameInfo.bytesconsumed + <= (m_inputBufferLength / sizeof(m_inputBuffer[0]))); + // consume input data + m_inputBufferOffset += decFrameInfo.bytesconsumed + * sizeof(m_inputBuffer[0]); + m_inputBufferLength -= decFrameInfo.bytesconsumed + * sizeof(m_inputBuffer[0]); + m_curFrameIndex += samples2frames(decFrameInfo.samples); + if (0 != decFrameInfo.error) { + qWarning() << "AAC decoding error:" + << faacDecGetErrorMessage(decFrameInfo.error); + break; // abort } - byteBuffer += numRead; - total_bytes_decoded += numRead; - } while (total_bytes_decoded < total_bytes_to_decode); - const size_type total_samples_decoded = total_bytes_decoded / sizeof(sample_type); - return samples2frames(total_samples_decoded); + if (getChannelCount() != decFrameInfo.channels) { + qWarning() << "Unexpected number of channels:" + << decFrameInfo.channels; + break; // abort + } + if (getFrameRate() != decFrameInfo.samplerate) { + qWarning() << "Unexpected sample rate:" << decFrameInfo.samplerate; + break; // abort + } + } + return readFrameCount; } -Result SoundSourceM4A::parseHeader(){ +Result SoundSourceM4A::parseHeader() { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); if (!readFileHeader(this, f)) { @@ -156,8 +353,7 @@ QImage SoundSourceM4A::parseCoverArt() { } } -QList SoundSourceM4A::supportedFileExtensions() -{ +QList SoundSourceM4A::supportedFileExtensions() { QList list; list.push_back("m4a"); list.push_back("mp4"); diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index de02d86add3..95039c35e4e 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -19,8 +19,6 @@ #include "soundsource.h" -#include "m4a/ip.h" - #include "defs_version.h" #ifdef __MP4V2__ @@ -31,10 +29,7 @@ #include -#include -#include - -#include +#include //As per QLibrary docs: http://doc.trolltech.com/4.6/qlibrary.html#resolve #ifdef Q_OS_WIN @@ -65,10 +60,26 @@ class SoundSourceM4A : public SoundSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; private: - Result initializeDecoder(); + void close(); + + bool isValidSampleId(MP4SampleId sampleId) const; + + MP4FileHandle m_hFile; + MP4TrackId m_trackId; + MP4SampleId m_maxSampleId; + MP4SampleId m_curSampleId; + + typedef std::vector InputBuffer; + InputBuffer m_inputBuffer; + InputBuffer::size_type m_inputBufferOffset; + u_int32_t m_inputBufferLength; + + faacDecHandle m_hDecoder; + + typedef std::vector SampleBuffer; + SampleBuffer m_prefetchSampleBuffer; - input_plugin_data ipd; - int trackId; + diff_type m_curFrameIndex; }; extern "C" MY_EXPORT const char* getMixxxVersion() From 5431f737603ad367078febc6c34f0193d149734e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 3 Dec 2014 12:40:20 +0100 Subject: [PATCH 014/481] Slightly simplify the analysis of tracks --- src/analyserqueue.cpp | 55 ++++++++++++++----------------------------- src/analyserqueue.h | 14 +++++++---- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 8accc6cbc32..4c1a4d9dc19 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -29,17 +29,19 @@ namespace { const Mixxx::AudioSource::size_type kAnalysisChannels = 2; // stereo - // We need to use a smaller block size becuase on Linux, the AnalyserQueue - // can starve the CPU of its resources, resulting in xruns.. A block size of - // 8192 seems to do fine. - const int kAnalysisBlockSize = 8192; + // We need to use a smaller block size because on Linux, the AnalyserQueue + // can starve the CPU of its resources, resulting in xruns. A block size of + // 4096 samples per channel seems to do fine. + const Mixxx::AudioSource::size_type kAnalysisFrameCount = 4096; + + const Mixxx::AudioSource::size_type kAnalysisSampleCount = kAnalysisFrameCount * kAnalysisChannels; } AnalyserQueue::AnalyserQueue(TrackCollection* pTrackCollection) : m_aq(), m_exit(false), m_aiCheckPriorities(false), - m_pStereoSamples(new Mixxx::AudioSource::sample_type[kAnalysisBlockSize]), + m_sampleBuffer(kAnalysisSampleCount), m_tioq(), m_qm(), m_qwait(), @@ -61,8 +63,6 @@ AnalyserQueue::~AnalyserQueue() { delete an; } //qDebug() << "AnalyserQueue::~AnalyserQueue()"; - - delete [] m_pStereoSamples; } void AnalyserQueue::addAnalyser(Analyser* an) { @@ -162,60 +162,41 @@ TrackPointer AnalyserQueue::dequeueNextBlocking() { // This is called from the AnalyserQueue thread bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudioSource) { - const Mixxx::AudioSource::size_type totalFrames = pAudioSource->getFrameCount(); - const Mixxx::AudioSource::size_type frameCount = kAnalysisBlockSize / kAnalysisChannels; - Mixxx::AudioSource::size_type processedFrames = 0; - Mixxx::AudioSource::size_type readFrames = 0; QTime progressUpdateInhibitTimer; progressUpdateInhibitTimer.start(); // Inhibit Updates for 60 milliseconds + Mixxx::AudioSource::size_type progressFrameCount = 0; bool dieflag = false; bool cancelled = false; - int progress; // progress in 0 ... 100 - do { ScopedTimer t("AnalyserQueue::doAnalysis block"); - readFrames = pAudioSource->readStereoFrameSamplesInterleaved(frameCount, m_pStereoSamples); - const Mixxx::AudioSource::size_type readStereoFrameSamplesInterleaved = readFrames * kAnalysisChannels; + const Mixxx::AudioSource::size_type readFrameCount = pAudioSource->readStereoFrameSamplesInterleaved(kAnalysisFrameCount, &m_sampleBuffer[0]); // To compare apples to apples, let's only look at blocks that are the // full block size. - if (readFrames != frameCount) { - t.cancel(); - } - - // Safety net in case something later barfs on 0 sample input - if (frameCount == 0) { - t.cancel(); - break; - } - - // If we get more samples than length, ask the analysers to process - // up to the number we promised, then stop reading - AD - if (readFrames + processedFrames > totalFrames) { - qDebug() << "While processing track of length " << totalFrames << " actually got " - << (readFrames + processedFrames) << " frames, truncating analysis at expected length"; - readFrames = totalFrames - processedFrames; - dieflag = true; + if (readFrameCount < kAnalysisFrameCount) { + // The whole file should have been read now! + Q_ASSERT(pAudioSource->getFrameCount() == (progressFrameCount + readFrameCount)); + break; // done } QListIterator it(m_aq); - while (it.hasNext()) { Analyser* an = it.next(); //qDebug() << typeid(*an).name() << ".process()"; - an->process(m_pStereoSamples, readStereoFrameSamplesInterleaved); + an->process(&m_sampleBuffer[0], readFrameCount * kAnalysisChannels); //qDebug() << "Done " << typeid(*an).name() << ".process()"; } // emit progress updates // During the doAnalysis function it goes only to 100% - FINALIZE_PERCENT // because the finalise functions will take also some time - processedFrames += readFrames; //fp div here prevents insane signed overflow - progress = (int)(((float)processedFrames)/totalFrames * + progressFrameCount += readFrameCount; + Q_ASSERT(progressFrameCount <= pAudioSource->getFrameCount()); + int progress = (int)(((float)progressFrameCount) / pAudioSource->getFrameCount() * (1000 - FINALIZE_PERCENT)); if (m_progressInfo.track_progress != progress) { @@ -252,7 +233,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi if (dieflag || cancelled) { t.cancel(); } - } while(!dieflag && (frameCount == readFrames)); + } while (!dieflag && (progressFrameCount < pAudioSource->getFrameCount())); return !cancelled; //don't return !dieflag or we might reanalyze over and over } diff --git a/src/analyserqueue.h b/src/analyserqueue.h index cb36c5a9508..1b6ae6fea8b 100644 --- a/src/analyserqueue.h +++ b/src/analyserqueue.h @@ -1,16 +1,18 @@ #ifndef ANALYSERQUEUE_H #define ANALYSERQUEUE_H +#include "configobject.h" +#include "analyser.h" +#include "trackinfoobject.h" +#include "audiosource.h" + #include #include #include #include #include -#include "configobject.h" -#include "analyser.h" -#include "trackinfoobject.h" -#include "audiosource.h" +#include class TrackCollection; @@ -63,7 +65,9 @@ class AnalyserQueue : public QThread { bool m_exit; QAtomicInt m_aiCheckPriorities; - Mixxx::AudioSource::sample_type* m_pStereoSamples; + + typedef std::vector SampleBuffer; + SampleBuffer m_sampleBuffer; // The processing queue and associated mutex QQueue m_tioq; From e3678e9506baba274223eff3eac49d2d7807296b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 8 Dec 2014 23:36:06 +0100 Subject: [PATCH 015/481] Initial cleanup of SoundSourceMp3 code --- src/soundsourcemp3.cpp | 510 ++++++++++++++++++++--------------------- src/soundsourcemp3.h | 63 ++--- 2 files changed, 280 insertions(+), 293 deletions(-) diff --git a/src/soundsourcemp3.cpp b/src/soundsourcemp3.cpp index b0d8803e696..cfafccd63c4 100644 --- a/src/soundsourcemp3.cpp +++ b/src/soundsourcemp3.cpp @@ -20,25 +20,37 @@ #include -#include +namespace +{ + const int kSeekSyncFrameCount = 4; // required for synchronization + + const Mixxx::AudioSource::sample_type kMadScale = + Mixxx::AudioSource::kSampleValuePeak + / Mixxx::AudioSource::sample_type( + mad_fixed_t(1) << MAD_F_FRACBITS); + + inline Mixxx::AudioSource::sample_type madScale(mad_fixed_t sample) { + return sample * kMadScale; + } + // Optimization: Reserve initial capacity for seek frame list + const size_t kMinutesPerFile = 10; // enough for the majority of files (tunable) + const size_t kSecondsPerMinute = 60; // fixed + const size_t kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> 1000 / 26 + const size_t kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; +} SoundSourceMp3::SoundSourceMp3(QString qFilename) - : Super(qFilename, "mp3") - , m_file(qFilename) - , inputbuf(NULL) - , inputbuf_len(0) - , framecount(0) - , currentframe(0) - , pos(mad_timer_zero) - , filelength(mad_timer_zero) - , units(MAD_UNITS_44100_HZ) - , m_madSynthOffset(0) - , m_currentSeekFrameIndex(0) - , m_iAvgFrameSize(0) { - mad_stream_init(&madStream); + : Super(qFilename, "mp3"), + m_file(qFilename), + m_fileSize(0), + m_pFileData(NULL), + m_iAvgFrameSize(0), + m_currentSeekFrameIndex(0), + m_madSynthOffset(0) { + mad_stream_init(&m_madStream); mad_synth_init(&m_madSynth); - mad_frame_init(&madFrame); + mad_frame_init(&m_madFrame); } SoundSourceMp3::~SoundSourceMp3() @@ -54,240 +66,237 @@ QList SoundSourceMp3::supportedFileExtensions() } Result SoundSourceMp3::open() { - m_file.setFileName(getFilename()); if (!m_file.open(QIODevice::ReadOnly)) { - //qDebug() << "MAD: Open failed:" << getFilename(); + qWarning() << "Failed to open file:" << getFilename(); return ERR; } // Get a pointer to the file using memory mapped IO - inputbuf_len = m_file.size(); - inputbuf = m_file.map(0, inputbuf_len); + m_fileSize = m_file.size(); + m_pFileData = m_file.map(0, m_fileSize); // Transfer it to the mad stream-buffer: - mad_stream_init(&madStream); - mad_stream_options(&madStream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(&madStream, inputbuf, inputbuf_len); + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); + + m_seekFrameList.reserve(kSeekFrameListCapacity); /* - Decode all the headers, and fill in stats: + Decode all the headers, and fill in stats: */ - mad_header madHeader; - mad_header_init(&madHeader); - mad_timer_t filelength = mad_timer_zero; - int sumBitrate = 0; - currentframe = 0; - pos = mad_timer_zero; - - while ((madStream.bufend - madStream.this_frame) > 0) - { - if (mad_header_decode (&madHeader, &madStream) == -1) { - if (!MAD_RECOVERABLE (madStream.error)) { + unsigned long sumBitrate = 0; + unsigned int madFrameCount = 0; + setChannelCount(kChannelCountDefault); + setFrameRate(kFrameRateDefault); + mad_timer_t madFileDuration = mad_timer_zero; + mad_units madUnits; + while ((m_madStream.bufend - m_madStream.this_frame) > 0) { + mad_header madHeader; + mad_header_init(&madHeader); + if (mad_header_decode(&madHeader, &m_madStream) == -1) { + if (MAD_ERROR_BUFLEN == m_madStream.error) { + // EOF -> done break; } - if (madStream.error == MAD_ERROR_LOSTSYNC) { + if (m_madStream.error == MAD_ERROR_LOSTSYNC) { // ignore LOSTSYNC due to ID3 tags - int tagsize = id3_tag_query (madStream.this_frame, madStream.bufend - madStream.this_frame); + int tagsize = id3_tag_query(m_madStream.this_frame, + m_madStream.bufend - m_madStream.this_frame); if (tagsize > 0) { //qDebug() << "SSMP3::SSMP3() : skipping ID3 tag size " << tagsize; - mad_stream_skip (&madStream, tagsize); + mad_stream_skip(&m_madStream, tagsize); + mad_header_finish(&madHeader); continue; } } - - // qDebug() << "MAD: ERR decoding header " - // << currentframe << ": " - // << mad_stream_errorstr(Stream) - // << " (len=" << mad_timer_count(filelength,MAD_UNITS_MILLISECONDS) - // << ")"; - continue; + qWarning() << "Unexpected error in MP3 frame header of file:" << getFilename() << mad_stream_errorstr(&m_madStream); + // abort + mad_header_finish(&madHeader); + close(); + return ERR; } // Grab data from madHeader - - setChannelCount(MAD_NCHANNELS(&madHeader)); - - // This warns us only when the reported sample rate changes. (and when - // it is first set) - if (getFrameRate() == 0 && madHeader.samplerate > 0) { - setFrameRate(madHeader.samplerate); - } else if (getFrameRate() != madHeader.samplerate) { - qDebug() << "SSMP3: file has differing samplerate in some headers:" - << getFilename() - << getFrameRate() << "vs" << madHeader.samplerate; + const size_type madChannelCount = MAD_NCHANNELS(&madHeader); + if (kChannelCountDefault == getChannelCount()) { + // initially set the number of channels + setChannelCount(madChannelCount); + } else { + // check for consistent number of channels + if ((0 < madChannelCount) && getChannelCount() != madChannelCount) { + qWarning() << "Differing number of channels in some headers:" + << getFilename() << getChannelCount() << "<>" + << madChannelCount; + } } - - switch (getFrameRate()) - { - case 8000: - units = MAD_UNITS_8000_HZ; - break; - case 11025: - units = MAD_UNITS_11025_HZ; - break; - case 12000: - units = MAD_UNITS_12000_HZ; - break; - case 16000: - units = MAD_UNITS_16000_HZ; - break; - case 22050: - units = MAD_UNITS_22050_HZ; - break; - case 24000: - units = MAD_UNITS_24000_HZ; - break; - case 32000: - units = MAD_UNITS_32000_HZ; - break; - case 44100: - units = MAD_UNITS_44100_HZ; - break; - case 48000: - units = MAD_UNITS_48000_HZ; - break; - default: //By the MP3 specs, an MP3 _has_ to have one of the above samplerates... - qWarning() << "MP3 with invalid sample rate (" << getFrameRate() << "), defaulting to 44100"; - setFrameRate(44100); //Prevents division by zero errors. - units = MAD_UNITS_44100_HZ; + const size_type madSampleRate = madHeader.samplerate; + if (kFrameRateDefault == getFrameRate()) { + // initially set the frame/sample rate + switch (madSampleRate) { + case 8000: + madUnits = MAD_UNITS_8000_HZ; + break; + case 11025: + madUnits = MAD_UNITS_11025_HZ; + break; + case 12000: + madUnits = MAD_UNITS_12000_HZ; + break; + case 16000: + madUnits = MAD_UNITS_16000_HZ; + break; + case 22050: + madUnits = MAD_UNITS_22050_HZ; + break; + case 24000: + madUnits = MAD_UNITS_24000_HZ; + break; + case 32000: + madUnits = MAD_UNITS_32000_HZ; + break; + case 44100: + madUnits = MAD_UNITS_44100_HZ; + break; + case 48000: + madUnits = MAD_UNITS_48000_HZ; + break; + default: + qWarning() << "Invalid sample rate:" + << getFilename() << madSampleRate; + // abort + close(); + return ERR; + } + setFrameRate(madSampleRate); + } else { + // check for consistent frame/sample rate + if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { + qWarning() << "Differing sample rate in some headers:" + << getFilename() << getFrameRate() << "<>" + << madSampleRate; + // abort + close(); + return ERR; + } } - mad_timer_add (&filelength, madHeader.duration); + mad_timer_add(&madFileDuration, madHeader.duration); sumBitrate += madHeader.bitrate; + mad_header_finish(&madHeader); + // Add frame to list of frames - MadSeekFrameType * p = new MadSeekFrameType; - p->m_pStreamPos = (unsigned char *)madStream.this_frame; - p->pos = mad_timer_count(filelength, units); - m_qSeekList.append(p); - currentframe++; + MadSeekFrameType seekFramePos; + seekFramePos.pFrameData = m_madStream.this_frame; + seekFramePos.pos = mad_timer_count(madFileDuration, madUnits); + m_seekFrameList.push_back(seekFramePos); + ++madFrameCount; } - //qDebug() << "channels " << m_iChannels; - mad_header_finish (&Header); // This is a macro for nothing. - - // This is not a working MP3 file. - if (currentframe == 0) { - qDebug() << "SSMP3: This is not a working MP3 file:" << getFilename(); + if (0 < madFrameCount) { + m_iAvgFrameSize = getFrameCount() / madFrameCount; + int avgBitrate = sumBitrate / madFrameCount; + setBitrate(avgBitrate); + } else { + // This is not a working MP3 file. + qWarning() << "SSMP3: This is not a working MP3 file:" << getFilename(); + // abort + close(); return ERR; } + setFrameCount(mad_timer_count(madFileDuration, madUnits)); - framecount = currentframe; - setFrameCount(mad_timer_count(filelength, units)); + // set the actual duration + setDuration(getFrameCount() / getFrameRate()); - if (0 < framecount) { - m_iAvgFrameSize = getFrameCount() / framecount; - int avgBitrate = sumBitrate / framecount; - setBitrate(avgBitrate); - } else { - m_iAvgFrameSize = 0; - } - //TODO: Emit metadata updated signal? + qDebug() << getChannelCount() << getFrameRate() << getFrameCount(); - // Re-init buffer: + // restart decoding seekFrame(0); - currentframe = 0; return OK; } -void SoundSourceMp3::close() -{ - mad_stream_finish(&madStream); - mad_frame_finish(&madFrame); +void SoundSourceMp3::close() { + m_seekFrameList.clear(); + m_iAvgFrameSize = 0; + m_currentSeekFrameIndex = 0; + mad_synth_finish(&m_madSynth); + m_madSynthOffset = 0; - m_file.unmap(inputbuf); - inputbuf = 0; - inputbuf_len = 0; + mad_frame_finish(&m_madFrame); - m_file.close(); + mad_stream_finish(&m_madStream); - //Free the pointers in our seek list, LIBERATE THEM!!! - for (int i = 0; i < m_qSeekList.count(); i++) - { - delete m_qSeekList[i]; - } - m_qSeekList.clear(); -} - -MadSeekFrameType* SoundSourceMp3::getSeekFrame(long frameIndex) const { - if (frameIndex < 0 || frameIndex >= m_qSeekList.size()) { - return NULL; - } - return m_qSeekList.at(frameIndex); -} + m_file.unmap(m_pFileData); + m_fileSize = 0; + m_pFileData = NULL; -namespace -{ - const int kSeekSyncFrameCount = 4; // required for synchronization + m_file.close(); } Mixxx::AudioSource::diff_type SoundSourceMp3::seekFrame(diff_type frameIndex) { - MadSeekFrameType* cur = NULL; + const MadSeekFrameType* cur = NULL; + int framePos; if (frameIndex == 0) { // Seek to beginning of file - + framePos = 0; + } else { + framePos = findFrame(frameIndex); + if (framePos == 0 || framePos > frameIndex + || m_currentSeekFrameIndex <= kSeekSyncFrameCount) { + qDebug() << "Problem finding good seek frame (wanted " << frameIndex + << ", got " << framePos << "), starting from 0"; + framePos = 0; + } + } + if (framePos == 0) { // Re-init buffer: - mad_stream_finish(&madStream); - mad_stream_init(&madStream); - mad_stream_options(&madStream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(&madStream, (unsigned char *) inputbuf, inputbuf_len); - mad_frame_init(&madFrame); + mad_stream_finish(&m_madStream); + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); + mad_frame_init(&m_madFrame); mad_synth_init(&m_madSynth); m_madSynthOffset = 0; m_currentSeekFrameIndex = 0; cur = getSeekFrame(m_currentSeekFrameIndex); } else { - int framePos = findFrame(frameIndex); - if (framePos == 0 || framePos > frameIndex || m_currentSeekFrameIndex <= kSeekSyncFrameCount) { - qDebug() << "Problem finding good seek frame (wanted " << frameIndex << ", got " << framePos << "), starting from 0"; - - // Re-init buffer: - mad_stream_finish(&madStream); - mad_stream_init(&madStream); - mad_stream_options(&madStream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(&madStream, (unsigned char *) inputbuf, inputbuf_len); - mad_frame_init(&madFrame); - mad_synth_init(&m_madSynth); - m_madSynthOffset = 0; - m_currentSeekFrameIndex = 0; - cur = getSeekFrame(m_currentSeekFrameIndex); - } else { - //qDebug() << "frame pos " << cur->pos; + //qDebug() << "frame pos " << cur->pos; - // Start four frames before wanted frame to get in sync... - m_currentSeekFrameIndex -= kSeekSyncFrameCount; + // Start four frames before wanted frame to get in sync... + m_currentSeekFrameIndex -= kSeekSyncFrameCount; + cur = getSeekFrame(m_currentSeekFrameIndex); + if (cur != NULL) { + // Start from the new frame + mad_stream_finish(&m_madStream); + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + // qDebug() << "mp3 restore " << cur->m_pStreamPos; + mad_stream_buffer(&m_madStream, cur->pFrameData, + m_fileSize - (cur->pFrameData - m_pFileData)); + + // Mute'ing is done here to eliminate potential pops/clicks from skipping + // Rob Leslie explains why here: + // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html + mad_synth_mute(&m_madSynth); + mad_frame_mute(&m_madFrame); + + // Decode the three frames before + mad_frame_decode(&m_madFrame, &m_madStream); + mad_frame_decode(&m_madFrame, &m_madStream); + mad_frame_decode(&m_madFrame, &m_madStream); + mad_frame_decode(&m_madFrame, &m_madStream); + + // this is also explained in the above mad-dev post + mad_synth_frame(&m_madSynth, &m_madFrame); + + // Set current position + m_madSynthOffset = 0; + m_currentSeekFrameIndex += 4; cur = getSeekFrame(m_currentSeekFrameIndex); - if (cur != NULL) { - // Start from the new frame - mad_stream_finish(&madStream); - mad_stream_init(&madStream); - mad_stream_options(&madStream, MAD_OPTION_IGNORECRC); - // qDebug() << "mp3 restore " << cur->m_pStreamPos; - mad_stream_buffer(&madStream, (const unsigned char *)cur->m_pStreamPos, - inputbuf_len-(long int)(cur->m_pStreamPos-(unsigned char *)inputbuf)); - - // Mute'ing is done here to eliminate potential pops/clicks from skipping - // Rob Leslie explains why here: - // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html - mad_synth_mute(&m_madSynth); - mad_frame_mute(&madFrame); - - // Decode the three frames before - mad_frame_decode(&madFrame, &madStream); - mad_frame_decode(&madFrame, &madStream); - mad_frame_decode(&madFrame, &madStream); - mad_frame_decode(&madFrame, &madStream); - - // this is also explained in the above mad-dev post - mad_synth_frame(&m_madSynth, &madFrame); - - // Set current position - m_madSynthOffset = 0; - m_currentSeekFrameIndex += 4; - cur = getSeekFrame(m_currentSeekFrameIndex); - } } // Synthesize the samples from the frame which should be discard to reach the requested position if (cur != NULL) { //the "if" prevents crashes on bad files. @@ -301,23 +310,24 @@ Mixxx::AudioSource::diff_type SoundSourceMp3::seekFrame(diff_type frameIndex) { } /* - decode the chosen number of samples and discard -*/ -Mixxx::AudioSource::size_type SoundSourceMp3::discardFrames(size_type frameCount) { + decode the chosen number of samples and discard + */ +Mixxx::AudioSource::size_type SoundSourceMp3::discardFrames( + size_type frameCount) { size_type framesDiscarded = 0; while (framesDiscarded < frameCount) { if (0 == m_madSynthOffset) { - if (mad_frame_decode(&madFrame, &madStream)) { - if (MAD_RECOVERABLE(madStream.error)) { + if (mad_frame_decode(&m_madFrame, &m_madStream)) { + if (MAD_RECOVERABLE(m_madStream.error)) { continue; - } else if(madStream.error == MAD_ERROR_BUFLEN) { + } else if (m_madStream.error == MAD_ERROR_BUFLEN) { break; } else { break; } } - mad_synth_frame(&m_madSynth, &madFrame); + mad_synth_frame(&m_madSynth, &m_madFrame); } size_type no = math_min(size_type(m_madSynth.pcm.length - m_madSynthOffset), frameCount - framesDiscarded); m_madSynthOffset += no; @@ -330,25 +340,19 @@ Mixxx::AudioSource::size_type SoundSourceMp3::discardFrames(size_type frameCount return framesDiscarded; } -Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) { +Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); } -Mixxx::AudioSource::size_type SoundSourceMp3::readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) { +Mixxx::AudioSource::size_type SoundSourceMp3::readStereoFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); } -namespace -{ - const Mixxx::AudioSource::sample_type kMadScale = Mixxx::AudioSource::kSampleValuePeak / Mixxx::AudioSource::sample_type(mad_fixed_t(1) << MAD_F_FRACBITS); - - inline - Mixxx::AudioSource::sample_type madScale(mad_fixed_t sample) { - return sample * kMadScale; - } -} - -Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples) { +Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer, + bool readStereoSamples) { size_type framesRead = 0; sample_type* sampleBufferPtr = sampleBuffer; @@ -356,20 +360,21 @@ Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved(size_t while (framesRead < frameCount) { // qDebug() << "no " << framesRead; if (0 == m_madSynthOffset) { - if (mad_frame_decode(&madFrame, &madStream)) { - if (MAD_RECOVERABLE(madStream.error)) { - if (madStream.error == MAD_ERROR_LOSTSYNC) { + if (mad_frame_decode(&m_madFrame, &m_madStream)) { + if (MAD_RECOVERABLE(m_madStream.error)) { + if (m_madStream.error == MAD_ERROR_LOSTSYNC) { // Ignore LOSTSYNC due to ID3 tags - int tagsize = id3_tag_query(madStream.this_frame, madStream.bufend - madStream.this_frame); + int tagsize = id3_tag_query(m_madStream.this_frame, + m_madStream.bufend - m_madStream.this_frame); if (tagsize > 0) { //qDebug() << "SSMP3::Read Skipping ID3 tag size: " << tagsize; - mad_stream_skip(&madStream, tagsize); + mad_stream_skip(&m_madStream, tagsize); } continue; } //qDebug() << "MAD: Recoverable frame level ERR (" << mad_stream_errorstr(Stream) << ")"; continue; - } else if (madStream.error == MAD_ERROR_BUFLEN) { + } else if (m_madStream.error == MAD_ERROR_BUFLEN) { // qDebug() << "MAD: buflen ERR"; break; } else { @@ -380,14 +385,15 @@ Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved(size_t /* Once decoded the frame is synthesized to PCM samples. No ERRs * are reported by mad_synth_frame(); */ - mad_synth_frame(&m_madSynth, &madFrame); + mad_synth_frame(&m_madSynth, &m_madFrame); //qDebug() << "synthlen " << m_madSynth.pcm.length << ", remain " << (frameCount - framesRead); } size_type no = math_min(size_type(m_madSynth.pcm.length - m_madSynthOffset), frameCount - framesRead); if (isChannelCountMono()) { for (size_type i = 0; i < no; ++i) { - const sample_type sampleValue = madScale(m_madSynth.pcm.samples[0][m_madSynthOffset + i]); + const sample_type sampleValue = madScale( + m_madSynth.pcm.samples[0][m_madSynthOffset + i]); *(sampleBufferPtr++) = sampleValue; if (readStereoSamples) { *(sampleBufferPtr++) = sampleValue; @@ -395,13 +401,16 @@ Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved(size_t } } else if (isChannelCountStereo() || readStereoSamples) { for (size_type i = 0; i < no; ++i) { - *(sampleBufferPtr++) = madScale(m_madSynth.pcm.samples[0][m_madSynthOffset + i]); - *(sampleBufferPtr++) = madScale(m_madSynth.pcm.samples[1][m_madSynthOffset + i]); + *(sampleBufferPtr++) = madScale( + m_madSynth.pcm.samples[0][m_madSynthOffset + i]); + *(sampleBufferPtr++) = madScale( + m_madSynth.pcm.samples[1][m_madSynthOffset + i]); } } else { for (size_type i = 0; i < no; ++i) { for (size_type j = 0; j < getChannelCount(); ++j) { - *(sampleBufferPtr++) = madScale(m_madSynth.pcm.samples[j][m_madSynthOffset + i]); + *(sampleBufferPtr++) = madScale( + m_madSynth.pcm.samples[j][m_madSynthOffset + i]); } } } @@ -461,55 +470,32 @@ QImage SoundSourceMp3::parseCoverArt() { return coverArt; } -int SoundSourceMp3::findFrame(int pos) -{ - // Guess position of frame in m_qSeekList based on average frame size - m_currentSeekFrameIndex = math_min((unsigned int) m_qSeekList.count()-1, - m_iAvgFrameSize ? (unsigned int)(pos/m_iAvgFrameSize) : 0); - MadSeekFrameType* temp = getSeekFrame(m_currentSeekFrameIndex); - -/* - if (temp!=0) - qDebug() << "find " << pos << ", got " << temp->pos; - else - qDebug() << "find " << pos << ", tried idx " << math_min(m_qSeekList.count()-1 << ", total " << pos/m_iAvgFrameSize); - */ +int SoundSourceMp3::findFrame(int pos) { + // Guess position of frame in m_seekFrameList based on average frame size + m_currentSeekFrameIndex = math_min((unsigned int) m_seekFrameList.size() - 1, + m_iAvgFrameSize ? (unsigned int)(pos/m_iAvgFrameSize) : 0); // Ensure that the list element is not at a greater position than pos - while (temp != NULL && temp->pos > pos) - { - m_currentSeekFrameIndex--; - temp = getSeekFrame(m_currentSeekFrameIndex); -// if (temp!=0) qDebug() << "backing " << pos << ", got " << temp->pos; + const MadSeekFrameType* temp = getSeekFrame(m_currentSeekFrameIndex); + while (temp != NULL && temp->pos > pos) { + temp = getSeekFrame(--m_currentSeekFrameIndex); } // Ensure that the following position is also not smaller than pos - if (temp != NULL) - { + if (temp != NULL) { temp = getSeekFrame(m_currentSeekFrameIndex); - while (temp != NULL && temp->pos < pos) - { - m_currentSeekFrameIndex++; - temp = getSeekFrame(m_currentSeekFrameIndex); -// if (temp!=0) qDebug() << "fwd'ing " << pos << ", got " << temp->pos; + while (temp != NULL && temp->pos < pos) { + temp = getSeekFrame(++m_currentSeekFrameIndex); } - - if (temp == NULL) - m_currentSeekFrameIndex = m_qSeekList.count()-1; - else - m_currentSeekFrameIndex--; - - temp = getSeekFrame(m_currentSeekFrameIndex); + if (temp == NULL) { + m_currentSeekFrameIndex = m_seekFrameList.size(); + } + temp = getSeekFrame(--m_currentSeekFrameIndex); } - if (temp != NULL) - { -// qDebug() << "ended at " << pos << ", got " << temp->pos; + if (temp != NULL) { return temp->pos; - } - else - { -// qDebug() << "ended at 0"; + } else { return 0; } } diff --git a/src/soundsourcemp3.h b/src/soundsourcemp3.h index e7bd566be3b..87768284abd 100644 --- a/src/soundsourcemp3.h +++ b/src/soundsourcemp3.h @@ -33,15 +33,9 @@ #include #include -#include #include -/** Struct used to store mad frames for seeking */ -typedef struct MadSeekFrameType { - unsigned char *m_pStreamPos; - long int pos; -} MadSeekFrameType; - +#include /** *@author Tue and Ken Haste Andersen @@ -71,39 +65,46 @@ class SoundSourceMp3 : public Mixxx::SoundSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); /** Returns the position of the frame which was found. The found frame is set to - * the current element in m_qSeekList */ + * the current element in m_qSeekList */ int findFrame(int pos); size_type discardFrames(size_type frameCount); - MadSeekFrameType* getSeekFrame(long frameIndex) const; QFile m_file; + quint64 m_fileSize; + unsigned char* m_pFileData; - unsigned char *inputbuf; - unsigned inputbuf_len; + mad_stream m_madStream; - int framecount; - int currentframe; - /** current play position. */ - mad_timer_t pos; - mad_timer_t filelength; - enum mad_units units; - mad_stream madStream; - mad_frame madFrame; - - mad_synth m_madSynth; - unsigned short m_madSynthOffset; // left overs from the previous read + /** Struct used to store mad frames for seeking */ + struct MadSeekFrameType { + const unsigned char *pFrameData; + long int pos; + }; /** It is not possible to make a precise seek in an mp3 file without decoding the whole stream. - * To have precise seek within a limited range from the current decode position, we keep track - * of past decodeded frame, and their exact position. If a seek occours and it is within the - * range of frames we keep track of a precise seek occours, otherwise an unprecise seek is performed - */ - QList m_qSeekList; - /** Index iterator for m_qSeekList. Helps us keep track of where we are in the file. */ - long m_currentSeekFrameIndex; - /** Average frame size used when searching for a frame*/ + * To have precise seek within a limited range from the current decode position, we keep track + * of past decodeded frame, and their exact position. If a seek occours and it is within the + * range of frames we keep track of a precise seek occours, otherwise an unprecise seek is performed + */ + typedef std::vector MadSeekFrameList; + MadSeekFrameList m_seekFrameList; int m_iAvgFrameSize; + /** Index iterator for m_qSeekList. Helps us keep track of where we are in the file. */ + MadSeekFrameList::size_type m_currentSeekFrameIndex; + + inline + const MadSeekFrameType* getSeekFrame(MadSeekFrameList::size_type frameIndex) const { + if (m_seekFrameList.size() > frameIndex) { + return &m_seekFrameList[frameIndex]; + } else { + return NULL; + } + } + + // current play position + mad_frame m_madFrame; + mad_synth m_madSynth; + unsigned short m_madSynthOffset; // left overs from the previous read }; - #endif From 8a7ba3a2efb268f91eb67551d4c4da804ad2cc1d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 10 Dec 2014 02:07:06 +0100 Subject: [PATCH 016/481] More SoundSourceMp3 optimizations and improvements --- src/soundsourcemp3.cpp | 371 +++++++++++++++++------------------------ src/soundsourcemp3.h | 35 ++-- 2 files changed, 169 insertions(+), 237 deletions(-) diff --git a/src/soundsourcemp3.cpp b/src/soundsourcemp3.cpp index cfafccd63c4..d171b1affde 100644 --- a/src/soundsourcemp3.cpp +++ b/src/soundsourcemp3.cpp @@ -22,7 +22,11 @@ namespace { - const int kSeekSyncFrameCount = 4; // required for synchronization + const Mixxx::AudioSource::size_type kSeekFramePrefetchCount = 2; // required for synchronization + + const Mixxx::AudioSource::size_type kMaxSamplesPerMp3Frame = 1152; + + const Mixxx::AudioSource::diff_type kMaxSkipFrameSamplesWhenSeeking = 2 * kSeekFramePrefetchCount * kMaxSamplesPerMp3Frame; const Mixxx::AudioSource::sample_type kMadScale = Mixxx::AudioSource::kSampleValuePeak @@ -34,10 +38,21 @@ namespace } // Optimization: Reserve initial capacity for seek frame list - const size_t kMinutesPerFile = 10; // enough for the majority of files (tunable) - const size_t kSecondsPerMinute = 60; // fixed - const size_t kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> 1000 / 26 - const size_t kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; + const Mixxx::AudioSource::size_type kMinutesPerFile = 10; // enough for the majority of files (tunable) + const Mixxx::AudioSource::size_type kSecondsPerMinute = 60; // fixed + const Mixxx::AudioSource::size_type kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 + const Mixxx::AudioSource::size_type kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; + + bool mad_skip_id3_tag(mad_stream* pStream) { + long tagsize = id3_tag_query(pStream->this_frame, + pStream->bufend - pStream->this_frame); + if (0 < tagsize) { + mad_stream_skip(pStream, tagsize); + return true; + } else { + return false; + } + } } SoundSourceMp3::SoundSourceMp3(QString qFilename) @@ -45,12 +60,12 @@ SoundSourceMp3::SoundSourceMp3(QString qFilename) m_file(qFilename), m_fileSize(0), m_pFileData(NULL), - m_iAvgFrameSize(0), - m_currentSeekFrameIndex(0), - m_madSynthOffset(0) { + m_avgSeekFrameCount(0), + m_curFrameIndex(0), + m_madSynthCount(0) { mad_stream_init(&m_madStream); - mad_synth_init(&m_madSynth); mad_frame_init(&m_madFrame); + mad_synth_init(&m_madSynth); } SoundSourceMp3::~SoundSourceMp3() @@ -94,27 +109,28 @@ Result SoundSourceMp3::open() { while ((m_madStream.bufend - m_madStream.this_frame) > 0) { mad_header madHeader; mad_header_init(&madHeader); - if (mad_header_decode(&madHeader, &m_madStream) == -1) { - if (MAD_ERROR_BUFLEN == m_madStream.error) { - // EOF -> done - break; - } - if (m_madStream.error == MAD_ERROR_LOSTSYNC) { - // ignore LOSTSYNC due to ID3 tags - int tagsize = id3_tag_query(m_madStream.this_frame, - m_madStream.bufend - m_madStream.this_frame); - if (tagsize > 0) { - //qDebug() << "SSMP3::SSMP3() : skipping ID3 tag size " << tagsize; - mad_stream_skip(&m_madStream, tagsize); + if (0 != mad_header_decode(&madHeader, &m_madStream)) { + if (MAD_RECOVERABLE(m_madStream.error)) { + if (MAD_ERROR_LOSTSYNC == m_madStream.error) { + // Ignore LOSTSYNC due to ID3 tags + mad_skip_id3_tag(&m_madStream); + } else { + qDebug() << "Recoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); + } + mad_header_finish(&madHeader); + continue; + } else { + if (MAD_ERROR_BUFLEN == m_madStream.error) { + // EOF -> done + break; + } else { + qWarning() << "Unrecoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); + // abort mad_header_finish(&madHeader); - continue; + close(); + return ERR; } } - qWarning() << "Unexpected error in MP3 frame header of file:" << getFilename() << mad_stream_errorstr(&m_madStream); - // abort - mad_header_finish(&madHeader); - close(); - return ERR; } // Grab data from madHeader @@ -181,21 +197,23 @@ Result SoundSourceMp3::open() { } } + // Add frame to list of frames + MadSeekFrameType seekFrame; + seekFrame.pFileData = m_madStream.this_frame; + seekFrame.frameIndex = mad_timer_count(madFileDuration, madUnits); + m_seekFrameList.push_back(seekFrame); + mad_timer_add(&madFileDuration, madHeader.duration); sumBitrate += madHeader.bitrate; mad_header_finish(&madHeader); - // Add frame to list of frames - MadSeekFrameType seekFramePos; - seekFramePos.pFrameData = m_madStream.this_frame; - seekFramePos.pos = mad_timer_count(madFileDuration, madUnits); - m_seekFrameList.push_back(seekFramePos); ++madFrameCount; } if (0 < madFrameCount) { - m_iAvgFrameSize = getFrameCount() / madFrameCount; + setFrameCount(mad_timer_count(madFileDuration, madUnits)); + m_avgSeekFrameCount = getFrameCount() / madFrameCount; int avgBitrate = sumBitrate / madFrameCount; setBitrate(avgBitrate); } else { @@ -205,14 +223,12 @@ Result SoundSourceMp3::open() { close(); return ERR; } - setFrameCount(mad_timer_count(madFileDuration, madUnits)); // set the actual duration setDuration(getFrameCount() / getFrameRate()); - qDebug() << getChannelCount() << getFrameRate() << getFrameCount(); - // restart decoding + m_curFrameIndex = getFrameCount(); seekFrame(0); return OK; @@ -220,124 +236,87 @@ Result SoundSourceMp3::open() { void SoundSourceMp3::close() { m_seekFrameList.clear(); - m_iAvgFrameSize = 0; - m_currentSeekFrameIndex = 0; + m_avgSeekFrameCount = 0; + m_curFrameIndex = 0; mad_synth_finish(&m_madSynth); - m_madSynthOffset = 0; - mad_frame_finish(&m_madFrame); - mad_stream_finish(&m_madStream); + m_madSynthCount = 0; m_file.unmap(m_pFileData); m_fileSize = 0; m_pFileData = NULL; - m_file.close(); } -Mixxx::AudioSource::diff_type SoundSourceMp3::seekFrame(diff_type frameIndex) { - const MadSeekFrameType* cur = NULL; - int framePos; - if (frameIndex == 0) { - // Seek to beginning of file - framePos = 0; - } else { - framePos = findFrame(frameIndex); - if (framePos == 0 || framePos > frameIndex - || m_currentSeekFrameIndex <= kSeekSyncFrameCount) { - qDebug() << "Problem finding good seek frame (wanted " << frameIndex - << ", got " << framePos << "), starting from 0"; - framePos = 0; - } +SoundSourceMp3::MadSeekFrameList::size_type SoundSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { + qDebug() << "findSeekFrameIndex()" << frameIndex; + + if ((0 >= frameIndex) || m_seekFrameList.empty()) { + return 0; } - if (framePos == 0) { - // Re-init buffer: - mad_stream_finish(&m_madStream); - mad_stream_init(&m_madStream); - mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); - mad_frame_init(&m_madFrame); - mad_synth_init(&m_madSynth); - m_madSynthOffset = 0; - m_currentSeekFrameIndex = 0; - cur = getSeekFrame(m_currentSeekFrameIndex); - } else { - //qDebug() << "frame pos " << cur->pos; - - // Start four frames before wanted frame to get in sync... - m_currentSeekFrameIndex -= kSeekSyncFrameCount; - cur = getSeekFrame(m_currentSeekFrameIndex); - if (cur != NULL) { - // Start from the new frame - mad_stream_finish(&m_madStream); - mad_stream_init(&m_madStream); - mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - // qDebug() << "mp3 restore " << cur->m_pStreamPos; - mad_stream_buffer(&m_madStream, cur->pFrameData, - m_fileSize - (cur->pFrameData - m_pFileData)); - - // Mute'ing is done here to eliminate potential pops/clicks from skipping - // Rob Leslie explains why here: - // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html - mad_synth_mute(&m_madSynth); - mad_frame_mute(&m_madFrame); - - // Decode the three frames before - mad_frame_decode(&m_madFrame, &m_madStream); - mad_frame_decode(&m_madFrame, &m_madStream); - mad_frame_decode(&m_madFrame, &m_madStream); - mad_frame_decode(&m_madFrame, &m_madStream); - - // this is also explained in the above mad-dev post - mad_synth_frame(&m_madSynth, &m_madFrame); - // Set current position - m_madSynthOffset = 0; - m_currentSeekFrameIndex += 4; - cur = getSeekFrame(m_currentSeekFrameIndex); - } - // Synthesize the samples from the frame which should be discard to reach the requested position - if (cur != NULL) { //the "if" prevents crashes on bad files. - discardFrames(frameIndex - cur->pos); - } + // Guess position of frame in m_seekFrameList based on average frame size + SoundSourceMp3::MadSeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; + if (seekFrameIndex >= m_seekFrameList.size()) { + seekFrameIndex = m_seekFrameList.size() - 1; } - // Unfortunately we don't know the exact file position. The returned position is thus an - // approximation only: - return frameIndex; + // binary search starting at seekFrameIndex + SoundSourceMp3::MadSeekFrameList::size_type lowerBound = 0; + SoundSourceMp3::MadSeekFrameList::size_type upperBound = m_seekFrameList.size(); + while ((upperBound - lowerBound) > 1) { + Q_ASSERT(seekFrameIndex >= lowerBound); + Q_ASSERT(seekFrameIndex < upperBound); + if (m_seekFrameList[seekFrameIndex].frameIndex > frameIndex) { + upperBound = seekFrameIndex; + } else { + lowerBound = seekFrameIndex; + } + seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; + } + Q_ASSERT(m_seekFrameList.size() > seekFrameIndex); + Q_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); + Q_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); + return seekFrameIndex; } -/* - decode the chosen number of samples and discard - */ -Mixxx::AudioSource::size_type SoundSourceMp3::discardFrames( - size_type frameCount) { - size_type framesDiscarded = 0; - - while (framesDiscarded < frameCount) { - if (0 == m_madSynthOffset) { - if (mad_frame_decode(&m_madFrame, &m_madStream)) { - if (MAD_RECOVERABLE(m_madStream.error)) { - continue; - } else if (m_madStream.error == MAD_ERROR_BUFLEN) { - break; - } else { - break; - } - } - mad_synth_frame(&m_madSynth, &m_madFrame); - } - size_type no = math_min(size_type(m_madSynth.pcm.length - m_madSynthOffset), frameCount - framesDiscarded); - m_madSynthOffset += no; - if (m_madSynthOffset >= m_madSynth.pcm.length) { - m_madSynthOffset = 0; +Mixxx::AudioSource::diff_type SoundSourceMp3::seekFrame(diff_type frameIndex) { + qDebug() << "seekFrame()" << frameIndex; + if (m_curFrameIndex == frameIndex) { + return m_curFrameIndex; + } + if (0 > frameIndex) { + return seekFrame(0); + } + // simply skip frames when jumping no more than kMaxSkipFrameSamplesWhenSeeking frames forward + if ((frameIndex < m_curFrameIndex) || ((frameIndex - m_curFrameIndex) > kMaxSkipFrameSamplesWhenSeeking)) { + MadSeekFrameList::size_type seekFrameIndex = findSeekFrameIndex(frameIndex); + if (seekFrameIndex <= kSeekFramePrefetchCount) { + seekFrameIndex = 0; + } else { + seekFrameIndex -= kSeekFramePrefetchCount; } - framesDiscarded += no; + Q_ASSERT(seekFrameIndex < m_seekFrameList.size()); + const MadSeekFrameType& seekFrame(m_seekFrameList[seekFrameIndex]); + // restart decoder + mad_synth_finish(&m_madSynth); + mad_frame_finish(&m_madFrame); + mad_stream_finish(&m_madStream); + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + mad_stream_buffer(&m_madStream, seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); + mad_frame_init(&m_madFrame); + mad_synth_init(&m_madSynth); + m_curFrameIndex = seekFrame.frameIndex; + m_madSynthCount = 0; } - - return framesDiscarded; + // decode and discard prefetch data + Q_ASSERT(m_curFrameIndex <= frameIndex); + skipFrameSamples(frameIndex - m_curFrameIndex); + Q_ASSERT(m_curFrameIndex == frameIndex); + return m_curFrameIndex; } Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved( @@ -353,32 +332,23 @@ Mixxx::AudioSource::size_type SoundSourceMp3::readStereoFrameSamplesInterleaved( Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved( size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples) { - size_type framesRead = 0; - sample_type* sampleBufferPtr = sampleBuffer; - -// qDebug() << "Decoding"; - while (framesRead < frameCount) { - // qDebug() << "no " << framesRead; - if (0 == m_madSynthOffset) { - if (mad_frame_decode(&m_madFrame, &m_madStream)) { + size_type framesRemaining = frameCount; + sample_type* pSampleBuffer = sampleBuffer; + while (0 < framesRemaining) { + if (0 >= m_madSynthCount) { + if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { - if (m_madStream.error == MAD_ERROR_LOSTSYNC) { + if (MAD_ERROR_LOSTSYNC == m_madStream.error) { // Ignore LOSTSYNC due to ID3 tags - int tagsize = id3_tag_query(m_madStream.this_frame, - m_madStream.bufend - m_madStream.this_frame); - if (tagsize > 0) { - //qDebug() << "SSMP3::Read Skipping ID3 tag size: " << tagsize; - mad_stream_skip(&m_madStream, tagsize); - } - continue; + mad_skip_id3_tag(&m_madStream); + } else { + qDebug() << "Recoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); } - //qDebug() << "MAD: Recoverable frame level ERR (" << mad_stream_errorstr(Stream) << ")"; continue; - } else if (m_madStream.error == MAD_ERROR_BUFLEN) { - // qDebug() << "MAD: buflen ERR"; - break; } else { - // qDebug() << "MAD: Unrecoverable frame level ERR (" << mad_stream_errorstr(Stream) << ")."; + if (MAD_ERROR_BUFLEN != m_madStream.error) { + qWarning() << "Unrecoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); + } break; } } @@ -386,42 +356,41 @@ Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved( * are reported by mad_synth_frame(); */ mad_synth_frame(&m_madSynth, &m_madFrame); - //qDebug() << "synthlen " << m_madSynth.pcm.length << ", remain " << (frameCount - framesRead); + m_madSynthCount = m_madSynth.pcm.length; } - - size_type no = math_min(size_type(m_madSynth.pcm.length - m_madSynthOffset), frameCount - framesRead); - if (isChannelCountMono()) { - for (size_type i = 0; i < no; ++i) { - const sample_type sampleValue = madScale( - m_madSynth.pcm.samples[0][m_madSynthOffset + i]); - *(sampleBufferPtr++) = sampleValue; - if (readStereoSamples) { - *(sampleBufferPtr++) = sampleValue; + const size_type madSynthOffset = m_madSynth.pcm.length - m_madSynthCount; + const size_type framesRead = math_min(m_madSynthCount, framesRemaining); + m_madSynthCount -= framesRead; + m_curFrameIndex += framesRead; + framesRemaining -= framesRead; + if (NULL != pSampleBuffer) { + if (isChannelCountMono()) { + for (size_type i = 0; i < framesRead; ++i) { + const sample_type sampleValue = madScale( + m_madSynth.pcm.samples[0][madSynthOffset + i]); + *(pSampleBuffer++) = sampleValue; + if (readStereoSamples) { + *(pSampleBuffer++) = sampleValue; + } } - } - } else if (isChannelCountStereo() || readStereoSamples) { - for (size_type i = 0; i < no; ++i) { - *(sampleBufferPtr++) = madScale( - m_madSynth.pcm.samples[0][m_madSynthOffset + i]); - *(sampleBufferPtr++) = madScale( - m_madSynth.pcm.samples[1][m_madSynthOffset + i]); - } - } else { - for (size_type i = 0; i < no; ++i) { - for (size_type j = 0; j < getChannelCount(); ++j) { - *(sampleBufferPtr++) = madScale( - m_madSynth.pcm.samples[j][m_madSynthOffset + i]); + } else if (isChannelCountStereo() || readStereoSamples) { + for (size_type i = 0; i < framesRead; ++i) { + *(pSampleBuffer++) = madScale( + m_madSynth.pcm.samples[0][madSynthOffset + i]); + *(pSampleBuffer++) = madScale( + m_madSynth.pcm.samples[1][madSynthOffset + i]); + } + } else { + for (size_type i = 0; i < framesRead; ++i) { + for (size_type j = 0; j < getChannelCount(); ++j) { + *(pSampleBuffer++) = madScale( + m_madSynth.pcm.samples[j][madSynthOffset + i]); + } } } } - m_madSynthOffset += no; - if (m_madSynthOffset >= m_madSynth.pcm.length) { - m_madSynthOffset = 0; - } - framesRead += no; - //qDebug() << "decoded: " << framesRead << ", wanted: " << frameCount; } - return framesRead; + return frameCount - framesRemaining; } Result SoundSourceMp3::parseHeader() { @@ -469,33 +438,3 @@ QImage SoundSourceMp3::parseCoverArt() { } return coverArt; } - -int SoundSourceMp3::findFrame(int pos) { - // Guess position of frame in m_seekFrameList based on average frame size - m_currentSeekFrameIndex = math_min((unsigned int) m_seekFrameList.size() - 1, - m_iAvgFrameSize ? (unsigned int)(pos/m_iAvgFrameSize) : 0); - - // Ensure that the list element is not at a greater position than pos - const MadSeekFrameType* temp = getSeekFrame(m_currentSeekFrameIndex); - while (temp != NULL && temp->pos > pos) { - temp = getSeekFrame(--m_currentSeekFrameIndex); - } - - // Ensure that the following position is also not smaller than pos - if (temp != NULL) { - temp = getSeekFrame(m_currentSeekFrameIndex); - while (temp != NULL && temp->pos < pos) { - temp = getSeekFrame(++m_currentSeekFrameIndex); - } - if (temp == NULL) { - m_currentSeekFrameIndex = m_seekFrameList.size(); - } - temp = getSeekFrame(--m_currentSeekFrameIndex); - } - - if (temp != NULL) { - return temp->pos; - } else { - return 0; - } -} diff --git a/src/soundsourcemp3.h b/src/soundsourcemp3.h index 87768284abd..d6dfc0b5d9a 100644 --- a/src/soundsourcemp3.h +++ b/src/soundsourcemp3.h @@ -62,13 +62,11 @@ class SoundSourceMp3 : public Mixxx::SoundSource { private: void close(); + inline size_type skipFrameSamples(size_type frameCount) { + return readFrameSamplesInterleaved(frameCount, NULL); + } size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); - /** Returns the position of the frame which was found. The found frame is set to - * the current element in m_qSeekList */ - int findFrame(int pos); - size_type discardFrames(size_type frameCount); - QFile m_file; quint64 m_fileSize; unsigned char* m_pFileData; @@ -77,8 +75,8 @@ class SoundSourceMp3 : public Mixxx::SoundSource { /** Struct used to store mad frames for seeking */ struct MadSeekFrameType { - const unsigned char *pFrameData; - long int pos; + diff_type frameIndex; + const unsigned char* pFileData; }; /** It is not possible to make a precise seek in an mp3 file without decoding the whole stream. @@ -87,24 +85,19 @@ class SoundSourceMp3 : public Mixxx::SoundSource { * range of frames we keep track of a precise seek occours, otherwise an unprecise seek is performed */ typedef std::vector MadSeekFrameList; - MadSeekFrameList m_seekFrameList; - int m_iAvgFrameSize; - /** Index iterator for m_qSeekList. Helps us keep track of where we are in the file. */ - MadSeekFrameList::size_type m_currentSeekFrameIndex; - - inline - const MadSeekFrameType* getSeekFrame(MadSeekFrameList::size_type frameIndex) const { - if (m_seekFrameList.size() > frameIndex) { - return &m_seekFrameList[frameIndex]; - } else { - return NULL; - } - } + MadSeekFrameList m_seekFrameList; // ordered-by frameIndex + size_type m_avgSeekFrameCount; // avg. samples frames per MP3 frame + + /** Returns the position of the frame which was found. The found frame is set to + * the current element in m_qSeekList */ + MadSeekFrameList::size_type findSeekFrameIndex(diff_type frameIndex) const; + + diff_type m_curFrameIndex; // current play position mad_frame m_madFrame; mad_synth m_madSynth; - unsigned short m_madSynthOffset; // left overs from the previous read + size_type m_madSynthCount; // left overs from the previous read }; #endif From 1d44152450535f8dd4e0690e6710f48f8073ff8e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 14 Dec 2014 14:56:01 +0100 Subject: [PATCH 017/481] Move track metadata properties from SoundSource into separate DTO class --- build/depends.py | 5 +- plugins/soundsourcem4a/SConscript | 3 +- plugins/soundsourcem4a/soundsourcem4a.cpp | 21 +-- plugins/soundsourcem4a/soundsourcem4a.h | 2 +- plugins/soundsourcemediafoundation/SConscript | 2 +- .../soundsourcemediafoundation.cpp | 44 ++--- .../soundsourcemediafoundation.h | 6 +- plugins/soundsourcewv/SConscript | 3 +- plugins/soundsourcewv/soundsourcewv.cpp | 13 +- plugins/soundsourcewv/soundsourcewv.h | 4 +- src/soundsource.cpp | 76 +------- src/soundsource.h | 151 +++------------ src/soundsourcecoreaudio.cpp | 32 ++-- src/soundsourcecoreaudio.h | 2 +- src/soundsourceffmpeg.cpp | 46 ++--- src/soundsourceffmpeg.h | 2 +- src/soundsourceflac.cpp | 17 +- src/soundsourceflac.h | 6 +- src/soundsourcemodplug.cpp | 16 +- src/soundsourcemodplug.h | 2 +- src/soundsourcemp3.cpp | 25 +-- src/soundsourcemp3.h | 2 +- src/soundsourceoggvorbis.cpp | 13 +- src/soundsourceoggvorbis.h | 2 +- src/soundsourceopus.cpp | 41 +++-- src/soundsourceopus.h | 2 +- src/soundsourceproxy.cpp | 31 ++-- src/soundsourcesndfile.cpp | 35 ++-- src/soundsourcesndfile.h | 2 +- src/test/soundproxy_test.cpp | 23 ++- src/trackinfoobject.cpp | 138 +++++++------- src/trackinfoobject.h | 1 - src/trackmetadata.cpp | 49 +++++ src/trackmetadata.h | 166 +++++++++++++++++ ...urcetaglib.cpp => trackmetadatataglib.cpp} | 173 ++++++++---------- ...ndsourcetaglib.h => trackmetadatataglib.h} | 23 +-- 36 files changed, 599 insertions(+), 580 deletions(-) create mode 100644 src/trackmetadata.cpp create mode 100644 src/trackmetadata.h rename src/{soundsourcetaglib.cpp => trackmetadatataglib.cpp} (64%) rename src/{soundsourcetaglib.h => trackmetadatataglib.h} (57%) diff --git a/build/depends.py b/build/depends.py index f1d2032d6c4..5e08b43fd18 100644 --- a/build/depends.py +++ b/build/depends.py @@ -656,9 +656,10 @@ def sources(self, build): "errordialoghandler.cpp", "upgrade.cpp", - "audiosource.cpp", "soundsource.cpp", - "soundsourcetaglib.cpp", + "audiosource.cpp", + "trackmetadata.cpp", + "trackmetadatataglib.cpp", "sharedglcontext.cpp", "widget/controlwidgetconnection.cpp", diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index 515fa1d7b5a..3c686c797eb 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -12,9 +12,10 @@ Import('build') m4a_sources = [ "soundsourcem4a.cpp", # MP4/M4A Support through FAAD/libmp4v2 - "soundsourcetaglib.cpp", # TagLib dependencies "soundsource.cpp", # required to subclass SoundSource "audiosource.cpp", # required to subclass AudioSource + "trackmetadatataglib.cpp", # TagLib dependencies + "trackmetadata.cpp", "sampleutil.cpp", # utility functions ] diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 105f59dd553..1c93fb73005 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -15,7 +15,8 @@ ***************************************************************************/ #include "soundsourcem4a.h" -#include "soundsourcetaglib.h" + +#include "trackmetadatataglib.h" #include "sampleutil.h" #include @@ -178,14 +179,6 @@ Result SoundSourceM4A::open() { setFrameRate(sampleRate); setFrameCount(m_maxSampleId * kFramesPerSampleBlock); - const u_int32_t timeScale = MP4GetTrackTimeScale(m_hFile, m_trackId); - const MP4Duration duration = MP4GetTrackDuration(m_hFile, m_trackId); - setDuration(duration / timeScale); - - qDebug() << "#channels" << getChannelCount() << "frame rate" - << getFrameRate() << "#frames" << getFrameCount() << "duration" - << getDuration(); - const SampleBuffer::size_type prefetchSampleBufferSize = (kSampleIdPrefetchCount + 1) * frames2samples(kFramesPerSampleBlock); SampleBuffer(prefetchSampleBufferSize).swap(m_prefetchSampleBuffer); @@ -320,21 +313,21 @@ AudioSource::size_type SoundSourceM4A::readFrameSamplesInterleaved( return readFrameCount; } -Result SoundSourceM4A::parseHeader() { +Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { - readMP4Tag(this, *mp4); + readMP4Tag(pMetadata, *mp4); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } @@ -347,7 +340,7 @@ QImage SoundSourceM4A::parseCoverArt() { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { - return getCoverInMP4Tag(*mp4); + return readMP4TagCover(*mp4); } else { return QImage(); } diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 95039c35e4e..56029136238 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -50,7 +50,7 @@ class SoundSourceM4A : public SoundSource { explicit SoundSourceM4A(QString qFileName); ~SoundSourceM4A(); - Result parseHeader() /*override*/; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; QImage parseCoverArt() /*override*/; diff --git a/plugins/soundsourcemediafoundation/SConscript b/plugins/soundsourcemediafoundation/SConscript index 24af5ea17fc..f915cb399ab 100644 --- a/plugins/soundsourcemediafoundation/SConscript +++ b/plugins/soundsourcemediafoundation/SConscript @@ -18,7 +18,7 @@ if int(build.flags['mediafoundation']): else: env["LINKFLAGS"].remove("/subsystem:windows,5.01") ssmediafoundation_bin = env.SharedLibrary('soundsourcemediafoundation', - ['soundsourcemediafoundation.cpp', 'soundsourcetaglib.cpp', 'soundsource.cpp', 'audiosource.cpp', 'sampleutil.cpp'], + ['soundsourcemediafoundation.cpp', 'soundsource.cpp', 'audiosource.cpp', 'trackmetadatataglib.cpp', 'trackmetadata.cpp', 'sampleutil.cpp'], LINKCOM = [env['LINKCOM'], 'mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;1']) Return("ssmediafoundation_bin") diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index ec023090923..9026b35f575 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -20,8 +20,12 @@ * * ***************************************************************************/ -#include +#include "soundsourcemediafoundation.h" + +#include "trackmetadatataglib.h" + #include + #include #include #include @@ -29,8 +33,7 @@ #include #include -#include "soundsourcemediafoundation.h" -#include "soundsourcetaglib.h" +#include const int kNumChannels = 2; const int kSampleRate = 44100; @@ -89,9 +92,6 @@ SoundSourceMediaFoundation::SoundSourceMediaFoundation(QString filename) // AudioSource properties setChannelCount(kNumChannels); setFrameRate(kSampleRate); - // SoundSource properties - setChannels(kNumChannels); - setSampleRate(kSampleRate); // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for // presentation descriptors, one of which MFSourceReader is not. @@ -396,22 +396,22 @@ Mixxx::AudioSource::size_type SoundSourceMediaFoundation::readFrameSamplesInterl return framesRead; } -Result SoundSourceMediaFoundation::parseHeader() { +Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata) { // Must be toLocal8Bit since Windows fopen does not do UTF-8 TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { - readMP4Tag(this, *mp4); + readMP4Tag(pMetadata, *mp4); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } @@ -424,7 +424,7 @@ QImage SoundSourceMediaFoundation::parseCoverArt() { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { - return Mixxx::getCoverInMP4Tag(*mp4); + return Mixxx::readMP4TagCover(*mp4); } else { return QImage(); } @@ -576,28 +576,6 @@ bool SoundSourceMediaFoundation::configureAudioStream() { return true; } -bool SoundSourceMediaFoundation::readProperties() { - PROPVARIANT prop; - HRESULT hr = S_OK; - - //Get the duration, provided as a 64-bit integer of 100-nanosecond units - hr = m_pReader->GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE, - MF_PD_DURATION, &prop); - if (FAILED(hr)) { - qWarning() << "SSMF: error getting duration"; - return false; - } - // QuadPart isn't available on compilers that don't support _int64. Visual - // Studio 6.0 introduced the type in 1998, so I think we're safe here - // -bkgood - setDuration(secondsFromMF(prop.hVal.QuadPart)); - m_mfDuration = prop.hVal.QuadPart; - qDebug() << "SSMF: Duration:" << getDuration(); - PropVariantClear(&prop); - - return true; -} - /** * Copies min(destFrames, srcFrames) frames to dest from src. Anything leftover * is moved to the beginning of m_leftoverBuffer, so empty it first (possibly diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 49a5f1f1e22..8d3cdcf787b 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -46,8 +46,8 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSource { explicit SoundSourceMediaFoundation(QString filename); ~SoundSourceMediaFoundation(); - Result parseHeader(); - QImage parseCoverArt(); + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; + QImage parseCoverArt() /*override*/; Result open(); @@ -57,7 +57,7 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSource { private: bool configureAudioStream(); - bool readProperties(); + void copyFrames(sample_type *dest, size_t *destFrames, const sample_type *src, size_t srcFrames); diff --git a/plugins/soundsourcewv/SConscript b/plugins/soundsourcewv/SConscript index c6c68a560fb..82e904c6f9c 100644 --- a/plugins/soundsourcewv/SConscript +++ b/plugins/soundsourcewv/SConscript @@ -12,9 +12,10 @@ Import('build') wv_sources = [ "soundsourcewv.cpp", # Wavpack support - "soundsourcetaglib.cpp", # TagLib dependencies "soundsource.cpp", # required to subclass SoundSource "audiosource.cpp", # required to subclass AudioSource + "trackmetadatataglib.cpp", # TagLib dependencies + "trackmetadata.cpp", "sampleutil.cpp", # utility functions ] diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index f1debe22427..32d9e7c28c7 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -1,5 +1,6 @@ #include "soundsourcewv.h" -#include "soundsourcetaglib.h" + +#include "trackmetadatataglib.h" #include "sampleutil.h" #include @@ -75,22 +76,22 @@ AudioSource::size_type SoundSourceWV::readFrameSamplesInterleaved( return unpackCount; } -Result SoundSourceWV::parseHeader() { +Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) { const QByteArray qBAFilename(getFilename().toLocal8Bit()); TagLib::WavPack::File f(qBAFilename.constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::APE::Tag *ape = f.APETag(); if (ape) { - readAPETag(this, *ape); + readAPETag(pMetadata, *ape); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } @@ -103,7 +104,7 @@ QImage SoundSourceWV::parseCoverArt() { TagLib::WavPack::File f(getFilename().toLocal8Bit().constData()); TagLib::APE::Tag *ape = f.APETag(); if (ape) { - return Mixxx::getCoverInAPETag(*ape); + return Mixxx::readAPETagCover(*ape); } else { return QImage(); } diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 321bbfb0f94..3b975e6e591 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -25,8 +25,8 @@ class SoundSourceWV: public SoundSource { explicit SoundSourceWV(QString qFilename); ~SoundSourceWV(); - Result parseHeader(); - QImage parseCoverArt(); + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; + QImage parseCoverArt() /*override*/; Result open(); diff --git a/src/soundsource.cpp b/src/soundsource.cpp index 8a0e021dca9..e86f2b7b47c 100644 --- a/src/soundsource.cpp +++ b/src/soundsource.cpp @@ -16,89 +16,19 @@ ***************************************************************************/ #include "soundsource.h" +#include "trackmetadata.h" namespace Mixxx { -namespace { -const float BPM_ZERO = 0.0f; -const float BPM_MAX = 300.0f; - -float parseBpmString(const QString& sBpm) { - float bpm = sBpm.toFloat(); - while (bpm > BPM_MAX) { - bpm /= 10.0f; - } - return bpm; -} - -float parseReplayGainString(QString sReplayGain) { - QString ReplayGainstring = sReplayGain.remove(" dB"); - float fReplayGain = db2ratio(ReplayGainstring.toFloat()); - // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. - // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by setting value to 0 (i.e. rescan via analyserrg) - if (fReplayGain == 1.0f) { - fReplayGain = 0.0f; - } - return fReplayGain; -} - -} - SoundSource::SoundSource(QString sFilename) - : m_sFilename(sFilename), m_sType( - m_sFilename.section(".", -1).toLower()), m_fReplayGain(0.0f), m_fBpm( - BPM_ZERO), m_iChannels(0), m_iSampleRate(0), m_iBitrate(0), m_iDuration(0) { + : m_sFilename(sFilename), m_sType(m_sFilename.section(".", -1).toLower()), m_bitrate(0) { } SoundSource::SoundSource(QString sFilename, QString sType) - : m_sFilename(sFilename), m_sType(sType), m_fReplayGain(0.0f), m_fBpm( - BPM_ZERO), m_iChannels(0), m_iSampleRate(0), m_iBitrate(0), m_iDuration(0) { + : m_sFilename(sFilename), m_sType(sType), m_bitrate(0) { } SoundSource::~SoundSource() { } -void SoundSource::setBpmString(QString sBpm) { - if (!sBpm.isEmpty()) { - float fBpm = parseBpmString(sBpm); - if (BPM_ZERO < fBpm) { - setBpm(fBpm); - } - } -} - -void SoundSource::setReplayGainString(QString sReplayGain) { - setReplayGain(parseReplayGainString(sReplayGain)); -} - -int SoundSource::getChannels() const { - if (isChannelCountValid()) { - // from AudioSource - return getChannelCount(); - } else { - // from metadata - return m_iChannels; - } -} - -int SoundSource::getSampleRate() const { - if (isFrameRateValid()) { - // from AudioSource - return getFrameRate(); - } else { - // from metadata - return m_iSampleRate; - } -} - -int SoundSource::getDuration() const { - if (isFrameRateValid() && !isFrameCountEmpty()) { - // from AudioSource - return getFrameCount() / getFrameRate(); - } else { - // from metadata - return m_iDuration; - } -} - } //namespace Mixxx diff --git a/src/soundsource.h b/src/soundsource.h index 895691be099..47942b1365c 100644 --- a/src/soundsource.h +++ b/src/soundsource.h @@ -30,14 +30,15 @@ */ #include "audiosource.h" + #include "util/defs.h" #include -#include /** Getter function to be declared by all SoundSource plugins */ namespace Mixxx { class SoundSource; +class TrackMetadata; } typedef Mixxx::SoundSource* (*getSoundSourceFunc)(QString filename); @@ -68,111 +69,11 @@ class SoundSource: public AudioSource { // The implementation is free to set inaccurate estimated // values here that are overwritten when the AudioSource is // actually opened for reading. - virtual Result parseHeader() = 0; + virtual Result parseMetadata(Mixxx::TrackMetadata* pMetadata) = 0; // Returns the first cover art image embedded within the file (if any). virtual QImage parseCoverArt() = 0; - inline const QString& getArtist() const { - return m_sArtist; - } - inline const QString& getTitle() const { - return m_sTitle; - } - inline const QString& getAlbum() const { - return m_sAlbum; - } - inline const QString& getAlbumArtist() const { - return m_sAlbumArtist; - } - inline const QString& getComment() const { - return m_sComment; - } - inline const QString& getYear() const { - return m_sYear; - } - inline const QString& getGenre() const { - return m_sGenre; - } - inline const QString& getComposer() const { - return m_sComposer; - } - inline const QString& getGrouping() const { - return m_sGrouping; - } - inline const QString& getTrackNumber() const { - return m_sTrackNumber; - } - inline float getReplayGain() const { - return m_fReplayGain; - } - inline const QString& getKey() const { - return m_sKey; - } - inline float getBPM() const { - return m_fBpm; - } - inline int getBitrate() const { - return m_iBitrate; - } - int getChannels() const; - int getSampleRate() const; - int getDuration() const; - - inline void setArtist(QString artist) { - m_sArtist = artist; - } - inline void setTitle(QString title) { - m_sTitle = title; - } - inline void setAlbum(QString album) { - m_sAlbum = album; - } - inline void setAlbumArtist(QString albumArtist) { - m_sAlbumArtist = albumArtist; - } - inline void setComment(QString comment) { - m_sComment = comment; - } - inline void setYear(QString year) { - m_sYear = year; - } - inline void setGenre(QString genre) { - m_sGenre = genre; - } - inline void setComposer(QString composer) { - m_sComposer = composer; - } - inline void setGrouping(QString grouping) { - m_sGrouping = grouping; - } - inline void setTrackNumber(QString trackNumber) { - m_sTrackNumber = trackNumber; - } - inline void setKey(QString key) { - m_sKey = key; - } - inline void setBpm(float bpm) { - m_fBpm = bpm; - } - void setBpmString(QString sBpm); - inline void setReplayGain(float replayGain) { - m_fReplayGain = replayGain; - } - void setReplayGainString(QString sReplayGain); - inline void setChannels(int channels) { - m_iChannels = channels; - } - inline void setSampleRate(int sampleRate) { - m_iSampleRate = sampleRate; - } - inline void setBitrate(int bitrate) { - m_iBitrate = bitrate; - } - inline void setDuration(int duration) { - m_iDuration = duration; - } - /** * Opens the SoundSource for reading audio data. * @@ -181,37 +82,37 @@ class SoundSource: public AudioSource { */ virtual Result open() = 0; + // The bitrate in kbit/s (optional). + // The actual bitrate might be determined and set when the file is opened. + inline bool hasBitrate() const { + return 0 < m_bitrate; + } + inline size_type getBitrate() const { + return m_bitrate; + } + + // The actual duration in seconds. + // The actual duration can be calculated after the file has been opened. + inline bool hasDuration() const { + return !isFrameCountEmpty() && isFrameRateValid(); + } + inline size_type getDuration() const { + return getFrameCount() / getFrameRate(); + } + protected: explicit SoundSource(QString sFilename); SoundSource(QString sFilename, QString sType); + inline void setBitrate(size_type bitrate) { + m_bitrate = bitrate; + } + private: const QString m_sFilename; const QString m_sType; - QString m_sArtist; - QString m_sTitle; - QString m_sAlbum; - QString m_sAlbumArtist; - QString m_sComment; - QString m_sYear; - QString m_sGenre; - QString m_sComposer; - QString m_sGrouping; - QString m_sTrackNumber; - QString m_sKey; - - // The following members need to be initialized - // explicitly in the constructor! Otherwise their - // value is undefined. - float m_fReplayGain; - float m_fBpm; // beats / minute - - // Audio properties (from metadata) - int m_iChannels; // #channels - int m_iSampleRate; // Hz - int m_iBitrate; // kbit / s - int m_iDuration; // #seconds + size_type m_bitrate; }; typedef QSharedPointer SoundSourcePointer; diff --git a/src/soundsourcecoreaudio.cpp b/src/soundsourcecoreaudio.cpp index 5f3988183f6..5f18ba64d0a 100644 --- a/src/soundsourcecoreaudio.cpp +++ b/src/soundsourcecoreaudio.cpp @@ -13,13 +13,15 @@ * * ***************************************************************************/ -#include +#include "soundsourcecoreaudio.h" + +#include "trackmetadatataglib.h" +#include "util/math.h" + #include #include -#include "soundsourcecoreaudio.h" -#include "soundsourcetaglib.h" -#include "util/math.h" +#include namespace { @@ -156,41 +158,41 @@ Mixxx::AudioSource::size_type SoundSourceCoreAudio::readFrameSamplesInterleaved( return numFramesRead; } -Result SoundSourceCoreAudio::parseHeader() { +Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) { if (getType() == "m4a") { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { - readMP4Tag(this, *mp4); + readMP4Tag(pMetadata, *mp4); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } } } else if (getType() == "mp3") { TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { - readID3v2Tag(this, *id3v2); + readID3v2Tag(pMetadata, *id3v2); } else { TagLib::APE::Tag *ape = f.APETag(); if (ape) { - readAPETag(this, *ape); + readAPETag(pMetadata, *ape); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } @@ -211,7 +213,7 @@ QImage SoundSourceCoreAudio::parseCoverArt() { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { - return Mixxx::getCoverInMP4Tag(*mp4); + return Mixxx::readMP4TagCover(*mp4); } else { return QImage(); } @@ -219,12 +221,12 @@ QImage SoundSourceCoreAudio::parseCoverArt() { TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { - coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); + coverArt = Mixxx::readID3v2TagCover(*id3v2); } if (coverArt.isNull()) { TagLib::APE::Tag *ape = f.APETag(); if (ape) { - coverArt = Mixxx::getCoverInAPETag(*ape); + coverArt = Mixxx::readAPETagCover(*ape); } } return coverArt; diff --git a/src/soundsourcecoreaudio.h b/src/soundsourcecoreaudio.h index daef90e3933..08d166caf5b 100644 --- a/src/soundsourcecoreaudio.h +++ b/src/soundsourcecoreaudio.h @@ -50,7 +50,7 @@ class SoundSourceCoreAudio : public Mixxx::SoundSource { explicit SoundSourceCoreAudio(QString filename); ~SoundSourceCoreAudio(); - Result parseHeader(); + Result parseMetadata(Mixxx::TrackMetadata* pMetadata); QImage parseCoverArt(); Result open(); diff --git a/src/soundsourceffmpeg.cpp b/src/soundsourceffmpeg.cpp index 7ca9f83726e..cbe4e48db3d 100644 --- a/src/soundsourceffmpeg.cpp +++ b/src/soundsourceffmpeg.cpp @@ -23,8 +23,10 @@ #include "soundsourceffmpeg.h" -#include +#include "trackmetadata.h" + #include +#include #include @@ -518,8 +520,8 @@ Mixxx::AudioSource::size_type SoundSourceFFmpeg::readFrameSamplesInterleaved(siz return samples2frames(readSamples); } -Result SoundSourceFFmpeg::parseHeader() { - qDebug() << "ffmpeg: SoundSourceFFmpeg::parseHeader" << getFilename(); +Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) { + qDebug() << "ffmpeg: SoundSourceFFmpeg::parseMetadata" << getFilename(); QByteArray qBAFilename = getFilename().toLocal8Bit(); AVFormatContext *FmtCtx = avformat_alloc_context(); @@ -571,23 +573,23 @@ Result SoundSourceFFmpeg::parseHeader() { // TODO: More sophisticated metadata reading if (!strncmp(FmtTag->key, "artist", 7)) { - this->setArtist(strValue); + pMetadata->setArtist(strValue); } else if (!strncmp(FmtTag->key, "title", 5)) { - this->setTitle(strValue); + pMetadata->setTitle(strValue); } else if (!strncmp(FmtTag->key, "album_artist", 12)) { - this->setAlbumArtist(strValue); + pMetadata->setAlbumArtist(strValue); } else if (!strncmp(FmtTag->key, "albumartist", 11)) { - this->setAlbumArtist(strValue); + pMetadata->setAlbumArtist(strValue); } else if (!strncmp(FmtTag->key, "album", 5)) { - this->setAlbum(strValue); + pMetadata->setAlbum(strValue); } else if (!strncmp(FmtTag->key, "TOAL", 4)) { - this->setAlbum(strValue); + pMetadata->setAlbum(strValue); } else if (!strncmp(FmtTag->key, "date", 4)) { - this->setYear(strValue); + pMetadata->setYear(strValue); } else if (!strncmp(FmtTag->key, "genre", 5)) { - this->setGenre(strValue); + pMetadata->setGenre(strValue); } else if (!strncmp(FmtTag->key, "comment", 7)) { - this->setComment(strValue); + pMetadata->setComment(strValue); } @@ -599,18 +601,18 @@ Result SoundSourceFFmpeg::parseHeader() { QString strValue (QString::fromUtf8 (FmtTag->value)); if (!strncmp(FmtTag->key, "ARTIST", 7)) { - this->setArtist(strValue); + pMetadata->setArtist(strValue); } else if (!strncmp(FmtTag->key, "ALBUM", 5)) { - this->setAlbum(strValue); + pMetadata->setAlbum(strValue); } else if (!strncmp(FmtTag->key, "YEAR", 4)) { - this->setYear(strValue); + pMetadata->setYear(strValue); } else if (!strncmp(FmtTag->key, "GENRE", 5)) { - this->setGenre(strValue); + pMetadata->setGenre(strValue); } else if (!strncmp(FmtTag->key, "TITLE", 5)) { - this->setTitle(strValue); + pMetadata->setTitle(strValue); } else if (!strncmp(FmtTag->key, "REPLAYGAIN_TRACK_PEAK", 20)) { } else if (!strncmp(FmtTag->key, "REPLAYGAIN_TRACK_GAIN", 20)) { - this->setReplayGainString (strValue); + pMetadata->setReplayGainString (strValue); } else if (!strncmp(FmtTag->key, "REPLAYGAIN_ALBUM_PEAK", 20)) { } else if (!strncmp(FmtTag->key, "REPLAYGAIN_ALBUM_GAIN", 20)) { } @@ -618,10 +620,10 @@ Result SoundSourceFFmpeg::parseHeader() { } - this->setChannels(CodecCtx->channels); - this->setSampleRate(CodecCtx->sample_rate); - this->setBitrate(CodecCtx->bit_rate / 1000); - this->setDuration(FmtCtx->duration / AV_TIME_BASE); + pMetadata->setChannels(CodecCtx->channels); + pMetadata->setSampleRate(CodecCtx->sample_rate); + pMetadata->setBitrate(CodecCtx->bit_rate / 1000); + pMetadata->setDuration(FmtCtx->duration / AV_TIME_BASE); avcodec_close(CodecCtx); avformat_close_input(&FmtCtx); diff --git a/src/soundsourceffmpeg.h b/src/soundsourceffmpeg.h index 849eac7c664..d2816a6366d 100644 --- a/src/soundsourceffmpeg.h +++ b/src/soundsourceffmpeg.h @@ -72,7 +72,7 @@ class SoundSourceFFmpeg : public Mixxx::SoundSource { explicit SoundSourceFFmpeg(QString qFilename); ~SoundSourceFFmpeg(); - Result parseHeader() /*override*/; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; QImage parseCoverArt() /*override*/; Result open() /*override*/; diff --git a/src/soundsourceflac.cpp b/src/soundsourceflac.cpp index 3e4380523eb..7c5c1d10c08 100644 --- a/src/soundsourceflac.cpp +++ b/src/soundsourceflac.cpp @@ -14,7 +14,8 @@ ***************************************************************************/ #include "soundsourceflac.h" -#include "soundsourcetaglib.h" + +#include "trackmetadatataglib.h" #include "sampleutil.h" #include "util/math.h" @@ -165,26 +166,26 @@ Mixxx::AudioSource::size_type SoundSourceFLAC::readFrameSamplesInterleaved( return frameCount - framesRemaining; } -Result SoundSourceFLAC::parseHeader() { +Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) { const QByteArray qBAFilename(getFilename().toLocal8Bit()); TagLib::FLAC::File f(qBAFilename.constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::Ogg::XiphComment *xiph(f.xiphComment()); if (xiph) { - readXiphComment(this, *xiph); + readXiphComment(pMetadata, *xiph); } else { TagLib::ID3v2::Tag *id3v2(f.ID3v2Tag()); if (id3v2) { - readID3v2Tag(this, *id3v2); + readID3v2Tag(pMetadata, *id3v2); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } @@ -200,12 +201,12 @@ QImage SoundSourceFLAC::parseCoverArt() { QImage coverArt; TagLib::Ogg::XiphComment *xiph(f.xiphComment()); if (xiph) { - coverArt = Mixxx::getCoverInXiphComment(*xiph); + coverArt = Mixxx::readXiphCommentCover(*xiph); } if (coverArt.isNull()) { TagLib::ID3v2::Tag *id3v2(f.ID3v2Tag()); if (id3v2) { - coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); + coverArt = Mixxx::readID3v2TagCover(*id3v2); } if (coverArt.isNull()) { TagLib::List covers = f.pictureList(); diff --git a/src/soundsourceflac.h b/src/soundsourceflac.h index 2772bad92c1..0224a14887a 100644 --- a/src/soundsourceflac.h +++ b/src/soundsourceflac.h @@ -19,13 +19,10 @@ #define SOUNDSOURCEFLAC_H #include "soundsource.h" -#include "util/defs.h" -#include "util/types.h" #include #include -#include #include @@ -38,8 +35,7 @@ class SoundSourceFLAC : public Mixxx::SoundSource { explicit SoundSourceFLAC(QString filename); ~SoundSourceFLAC(); - Result parseHeader() /*override*/; - + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; QImage parseCoverArt() /*override*/; Result open() /*override*/; diff --git a/src/soundsourcemodplug.cpp b/src/soundsourcemodplug.cpp index 1ee473a426a..0859449db4a 100644 --- a/src/soundsourcemodplug.cpp +++ b/src/soundsourcemodplug.cpp @@ -1,13 +1,14 @@ #include "soundsourcemodplug.h" +#include "trackmetadata.h" +#include "util/timer.h" + #include #include #include #include -#include "util/timer.h" - /* read files in 512k chunks */ #define CHUNKSIZE (1 << 18) @@ -160,17 +161,18 @@ Mixxx::AudioSource::size_type SoundSourceModPlug::readFrameSamplesInterleaved( return readFrames; } -Result SoundSourceModPlug::parseHeader() { +Result SoundSourceModPlug::parseMetadata(Mixxx::TrackMetadata* pMetadata) { if (m_pModFile == NULL) { // an error occured qDebug() << "Could not parse module header of " << getFilename(); return ERR; } - setComment(QString(ModPlug::ModPlug_GetMessage(m_pModFile))); - setTitle(QString(ModPlug::ModPlug_GetName(m_pModFile))); - setDuration(ModPlug::ModPlug_GetLength(m_pModFile) / 1000); - setBitrate(8); // not really, but fill in something... + pMetadata->setComment(QString(ModPlug::ModPlug_GetMessage(m_pModFile))); + pMetadata->setTitle(QString(ModPlug::ModPlug_GetName(m_pModFile))); + pMetadata->setDuration(ModPlug::ModPlug_GetLength(m_pModFile) / 1000); + pMetadata->setBitrate(8); // not really, but fill in something... + return OK; } diff --git a/src/soundsourcemodplug.h b/src/soundsourcemodplug.h index d0b98f06447..bf2c1f99410 100644 --- a/src/soundsourcemodplug.h +++ b/src/soundsourcemodplug.h @@ -25,7 +25,7 @@ class SoundSourceModPlug: public Mixxx::SoundSource { explicit SoundSourceModPlug(QString qFilename); ~SoundSourceModPlug(); - Result parseHeader() /*override*/; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; QImage parseCoverArt() /*override*/; Result open() /*override*/; diff --git a/src/soundsourcemp3.cpp b/src/soundsourcemp3.cpp index d171b1affde..5a43c31262d 100644 --- a/src/soundsourcemp3.cpp +++ b/src/soundsourcemp3.cpp @@ -15,7 +15,8 @@ ***************************************************************************/ #include "soundsourcemp3.h" -#include "soundsourcetaglib.h" + +#include "trackmetadatataglib.h" #include "util/math.h" #include @@ -224,9 +225,6 @@ Result SoundSourceMp3::open() { return ERR; } - // set the actual duration - setDuration(getFrameCount() / getFrameRate()); - // restart decoding m_curFrameIndex = getFrameCount(); seekFrame(0); @@ -251,18 +249,14 @@ void SoundSourceMp3::close() { } SoundSourceMp3::MadSeekFrameList::size_type SoundSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { - qDebug() << "findSeekFrameIndex()" << frameIndex; - if ((0 >= frameIndex) || m_seekFrameList.empty()) { return 0; } - // Guess position of frame in m_seekFrameList based on average frame size SoundSourceMp3::MadSeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; if (seekFrameIndex >= m_seekFrameList.size()) { seekFrameIndex = m_seekFrameList.size() - 1; } - // binary search starting at seekFrameIndex SoundSourceMp3::MadSeekFrameList::size_type lowerBound = 0; SoundSourceMp3::MadSeekFrameList::size_type upperBound = m_seekFrameList.size(); @@ -283,7 +277,6 @@ SoundSourceMp3::MadSeekFrameList::size_type SoundSourceMp3::findSeekFrameIndex(d } Mixxx::AudioSource::diff_type SoundSourceMp3::seekFrame(diff_type frameIndex) { - qDebug() << "seekFrame()" << frameIndex; if (m_curFrameIndex == frameIndex) { return m_curFrameIndex; } @@ -393,27 +386,27 @@ Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved( return frameCount - framesRemaining; } -Result SoundSourceMp3::parseHeader() { +Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) { QByteArray qBAFilename(getFilename().toLocal8Bit()); TagLib::MPEG::File f(qBAFilename.constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } // Now look for MP3 specific metadata (e.g. BPM) TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { - readID3v2Tag(this, *id3v2); + readID3v2Tag(pMetadata, *id3v2); } else { TagLib::APE::Tag *ape = f.APETag(); if (ape) { - readAPETag(this, *ape); + readAPETag(pMetadata, *ape); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } @@ -428,12 +421,12 @@ QImage SoundSourceMp3::parseCoverArt() { TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { - coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); + coverArt = Mixxx::readID3v2TagCover(*id3v2); } if (coverArt.isNull()) { TagLib::APE::Tag *ape = f.APETag(); if (ape) { - coverArt = Mixxx::getCoverInAPETag(*ape); + coverArt = Mixxx::readAPETagCover(*ape); } } return coverArt; diff --git a/src/soundsourcemp3.h b/src/soundsourcemp3.h index d6dfc0b5d9a..32383d93ee8 100644 --- a/src/soundsourcemp3.h +++ b/src/soundsourcemp3.h @@ -50,7 +50,7 @@ class SoundSourceMp3 : public Mixxx::SoundSource { explicit SoundSourceMp3(QString qFilename); ~SoundSourceMp3(); - Result parseHeader() /*override*/; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; QImage parseCoverArt() /*override*/; Result open() /*override*/; diff --git a/src/soundsourceoggvorbis.cpp b/src/soundsourceoggvorbis.cpp index ff7d701093e..a66cb1d51bd 100644 --- a/src/soundsourceoggvorbis.cpp +++ b/src/soundsourceoggvorbis.cpp @@ -15,7 +15,8 @@ ***************************************************************************/ #include "soundsourceoggvorbis.h" -#include "soundsourcetaglib.h" + +#include "trackmetadatataglib.h" #include @@ -144,22 +145,22 @@ Mixxx::AudioSource::size_type SoundSourceOggVorbis::readFrameSamplesInterleaved( /* Parse the the file to get metadata */ -Result SoundSourceOggVorbis::parseHeader() { +Result SoundSourceOggVorbis::parseMetadata(Mixxx::TrackMetadata* pMetadata) { QByteArray qBAFilename = getFilename().toLocal8Bit(); TagLib::Ogg::Vorbis::File f(qBAFilename.constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { - readXiphComment(this, *xiph); + readXiphComment(pMetadata, *xiph); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } @@ -172,7 +173,7 @@ QImage SoundSourceOggVorbis::parseCoverArt() { TagLib::Ogg::Vorbis::File f(getFilename().toLocal8Bit().constData()); TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { - return Mixxx::getCoverInXiphComment(*xiph); + return Mixxx::readXiphCommentCover(*xiph); } else { return QImage(); } diff --git a/src/soundsourceoggvorbis.h b/src/soundsourceoggvorbis.h index 868b9ca7669..784df556c94 100644 --- a/src/soundsourceoggvorbis.h +++ b/src/soundsourceoggvorbis.h @@ -31,7 +31,7 @@ class SoundSourceOggVorbis: public Mixxx::SoundSource { explicit SoundSourceOggVorbis(QString qFilename); ~SoundSourceOggVorbis(); - Result parseHeader() /*override*/; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; QImage parseCoverArt() /*override*/; Result open() /*override*/; diff --git a/src/soundsourceopus.cpp b/src/soundsourceopus.cpp index d05cb4fd71b..5ab8c450c51 100644 --- a/src/soundsourceopus.cpp +++ b/src/soundsourceopus.cpp @@ -1,5 +1,6 @@ #include "soundsourceopus.h" -#include "soundsourcetaglib.h" + +#include "trackmetadatataglib.h" // Include this if taglib if new enough (version 1.9.1 have opusfile) #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) @@ -115,34 +116,34 @@ Mixxx::AudioSource::size_type SoundSourceOpus::readStereoFrameSamplesInterleaved /* Parse the the file to get metadata */ -Result SoundSourceOpus::parseHeader() { +Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) { int error = 0; QByteArray qBAFilename = getFilename().toLocal8Bit(); OggOpusFile *l_ptrOpusFile = op_open_file(qBAFilename.constData(), &error); - setChannels(op_channel_count(l_ptrOpusFile, -1)); - setSampleRate(kOpusSampleRate); - setBitrate(op_bitrate(l_ptrOpusFile, -1) / 1000); - setDuration(op_pcm_total(l_ptrOpusFile, -1) / getSampleRate()); + pMetadata->setChannels(op_channel_count(l_ptrOpusFile, -1)); + pMetadata->setSampleRate(kOpusSampleRate); + pMetadata->setBitrate(op_bitrate(l_ptrOpusFile, -1) / 1000); + pMetadata->setDuration(op_pcm_total(l_ptrOpusFile, -1) / pMetadata->getSampleRate()); // If we don't have new enough Taglib we use libopusfile parser! #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) TagLib::Ogg::Opus::File f(qBAFilename.constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { - readXiphComment(this, *xiph); + readXiphComment(pMetadata, *xiph); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } @@ -161,26 +162,26 @@ Result SoundSourceOpus::parseHeader() { QString l_SPayload = l_SWholeTag.right((l_SWholeTag.length() - l_SWholeTag.indexOf("=")) - 1); if (!l_STag.compare("ARTIST")) { - this->setArtist(l_SPayload); + pMetadata->setArtist(l_SPayload); } else if (!l_STag.compare("ALBUM")) { - this->setAlbum(l_SPayload); + pMetadata->setAlbum(l_SPayload); } else if (!l_STag.compare("BPM")) { - this->setBpm(l_SPayload.toFloat()); + pMetadata->setBpm(l_SPayload.toFloat()); } else if (!l_STag.compare("YEAR") || !l_STag.compare("DATE")) { - this->setYear(l_SPayload); + pMetadata->setYear(l_SPayload); } else if (!l_STag.compare("GENRE")) { - this->setGenre(l_SPayload); + pMetadata->setGenre(l_SPayload); } else if (!l_STag.compare("TRACKNUMBER")) { - this->setTrackNumber(l_SPayload); + pMetadata->setTrackNumber(l_SPayload); } else if (!l_STag.compare("COMPOSER")) { - this->setComposer(l_SPayload); + pMetadata->setComposer(l_SPayload); } else if (!l_STag.compare("ALBUMARTIST")) { - this->setAlbumArtist(l_SPayload); + pMetadata->setAlbumArtist(l_SPayload); } else if (!l_STag.compare("TITLE")) { - this->setTitle(l_SPayload); + pMetadata->setTitle(l_SPayload); } else if (!l_STag.compare("REPLAYGAIN_TRACK_PEAK")) { } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) { - this->setReplayGainString (l_SPayload); + pMetadata->setReplayGainString (l_SPayload); } else if (!l_STag.compare("REPLAYGAIN_ALBUM_PEAK")) { } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) { } @@ -211,7 +212,7 @@ QImage SoundSourceOpus::parseCoverArt() { TagLib::Ogg::Opus::File f(getFilename().toLocal8Bit().constData()); TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { - return Mixxx::getCoverInXiphComment(*xiph); + return Mixxx::readXiphCommentCover(*xiph); } #endif return QImage(); diff --git a/src/soundsourceopus.h b/src/soundsourceopus.h index cab046bfc89..82bfc6ff571 100644 --- a/src/soundsourceopus.h +++ b/src/soundsourceopus.h @@ -15,7 +15,7 @@ class SoundSourceOpus: public Mixxx::SoundSource { explicit SoundSourceOpus(QString qFilename); ~SoundSourceOpus(); - Result parseHeader() /*override*/; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; QImage parseCoverArt() /*override*/; Result open() /*override*/; diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index a782389ad22..1eefa6b913f 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -311,27 +311,28 @@ Mixxx::SoundSourcePointer SoundSourceProxy::open() const { } if (!m_pSoundSource->isChannelCountValid()) { - qWarning() << "Invalid number of channels" << m_pSoundSource->getChannelCount(); + qWarning() << "Invalid number of channels" << m_pSoundSource->getFilename() << m_pSoundSource->getChannelCount(); return Mixxx::SoundSourcePointer(); } if (!m_pSoundSource->isFrameRateValid()) { - qWarning() << "Invalid frame rate:" << m_pSoundSource->getFrameRate(); + qWarning() << "Invalid frame rate:" << m_pSoundSource->getFilename() << m_pSoundSource->getFrameRate(); + return Mixxx::SoundSourcePointer(); + } + if (m_pSoundSource->isFrameCountEmpty()) { + qWarning() << "Empty file:" << m_pSoundSource->getFilename(); return Mixxx::SoundSourcePointer(); } - //Update some metadata (currently only the duration) - //after a song is open()'d. Eg. We don't know the length - //of VBR MP3s until we've seeked through and counted all - //the frames. We don't do that in ParseHeader() to keep - //library scanning fast. - // .... but only do this if the song doesn't already - // have a duration parsed. (Some SoundSources don't - // parse metadata on open(), so they won't have the - // duration.) - // SSMP3 will set duration to -1 on VBR files, - // so we must look for that here too - if (m_pTrack && m_pTrack->getDuration() <= 0) { - m_pTrack->setDuration(m_pSoundSource->getDuration()); + // Overwrite metadata with actual audio properties + if (m_pTrack) { + m_pTrack->setChannels(m_pSoundSource->getChannelCount()); + m_pTrack->setSampleRate(m_pSoundSource->getFrameRate()); + if (m_pSoundSource->hasDuration()) { + m_pTrack->setDuration(m_pSoundSource->getDuration()); + } + if (m_pSoundSource->hasBitrate()) { + m_pTrack->setBitrate(m_pSoundSource->getBitrate()); + } } return m_pSoundSource; diff --git a/src/soundsourcesndfile.cpp b/src/soundsourcesndfile.cpp index 59c8a5db1a0..c0b25fa2a1c 100644 --- a/src/soundsourcesndfile.cpp +++ b/src/soundsourcesndfile.cpp @@ -1,5 +1,6 @@ #include "soundsourcesndfile.h" -#include "soundsourcetaglib.h" + +#include "trackmetadatataglib.h" #include #include @@ -92,26 +93,26 @@ Mixxx::AudioSource::size_type SoundSourceSndFile::readFrameSamplesInterleaved( } } -Result SoundSourceSndFile::parseHeader() { +Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) { const QByteArray qBAFilename(getFilename().toLocal8Bit()); if (getType() == "flac") { TagLib::FLAC::File f(qBAFilename.constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::Ogg::XiphComment* xiph = f.xiphComment(); if (xiph) { - readXiphComment(this, *xiph); + readXiphComment(pMetadata, *xiph); } else { TagLib::ID3v2::Tag *id3v2(f.ID3v2Tag()); if (id3v2) { - readID3v2Tag(this, *id3v2); + readID3v2Tag(pMetadata, *id3v2); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } @@ -119,23 +120,23 @@ Result SoundSourceSndFile::parseHeader() { } } else if (getType() == "wav") { TagLib::RIFF::WAV::File f(qBAFilename.constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::ID3v2::Tag *id3v2(f.ID3v2Tag()); if (id3v2) { - readID3v2Tag(this, *id3v2); + readID3v2Tag(pMetadata, *id3v2); } else { // fallback const TagLib::Tag *tag(f.tag()); if (tag) { - readTag(this, *tag); + readTag(pMetadata, *tag); } else { return ERR; } } - if (getDuration() <= 0) { + if (pMetadata->getDuration() <= 0) { // we're using a taglib version which doesn't know how to do wav // durations, set it with m_sfInfo from sndfile -bkgood // XXX remove this when ubuntu ships with an sufficiently @@ -147,7 +148,7 @@ Result SoundSourceSndFile::parseHeader() { } if (m_sfInfo.samplerate > 0) { - setDuration(m_sfInfo.frames / m_sfInfo.samplerate); + pMetadata->setDuration(m_sfInfo.frames / m_sfInfo.samplerate); } else { qDebug() << "WARNING: WAV file with invalid samplerate." << "Can't get duration using libsndfile."; @@ -156,12 +157,12 @@ Result SoundSourceSndFile::parseHeader() { } else if (getType().startsWith("aif")) { // Try AIFF TagLib::RIFF::AIFF::File f(qBAFilename.constData()); - if (!readFileHeader(this, f)) { + if (!readAudioProperties(pMetadata, f)) { return ERR; } TagLib::ID3v2::Tag *id3v2(f.tag()); if (id3v2) { - readID3v2Tag(this, *id3v2); + readID3v2Tag(pMetadata, *id3v2); } else { return ERR; } @@ -180,12 +181,12 @@ QImage SoundSourceSndFile::parseCoverArt() { TagLib::FLAC::File f(qBAFilename.constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { - coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); + coverArt = Mixxx::readID3v2TagCover(*id3v2); } if (coverArt.isNull()) { TagLib::Ogg::XiphComment *xiph = f.xiphComment(); if (xiph) { - coverArt = Mixxx::getCoverInXiphComment(*xiph); + coverArt = Mixxx::readXiphCommentCover(*xiph); } } if (coverArt.isNull()) { @@ -201,14 +202,14 @@ QImage SoundSourceSndFile::parseCoverArt() { TagLib::RIFF::WAV::File f(qBAFilename.constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { - coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); + coverArt = Mixxx::readID3v2TagCover(*id3v2); } } else if (getType().startsWith("aif")) { // Try AIFF TagLib::RIFF::AIFF::File f(qBAFilename.constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { - coverArt = Mixxx::getCoverInID3v2Tag(*id3v2); + coverArt = Mixxx::readID3v2TagCover(*id3v2); } } diff --git a/src/soundsourcesndfile.h b/src/soundsourcesndfile.h index 238f767fb53..54ddad02586 100644 --- a/src/soundsourcesndfile.h +++ b/src/soundsourcesndfile.h @@ -20,7 +20,7 @@ class SoundSourceSndFile: public Mixxx::SoundSource { explicit SoundSourceSndFile(QString qFilename); ~SoundSourceSndFile(); - Result parseHeader() /*override*/; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; QImage parseCoverArt() /*override*/; Result open() /*override*/; diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 0543f8704d3..9a17f5bdce7 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -5,8 +5,9 @@ #include #include "test/mixxxtest.h" -#include "soundsourceproxy.h" +#include "soundsourceproxy.h" +#include "trackmetadata.h" class SoundSourceProxyTest : public MixxxTest { protected: @@ -33,9 +34,9 @@ TEST_F(SoundSourceProxyTest, ProxyCanOpen) { Mixxx::SoundSourcePointer pSoundSource(loadProxy(filePath)); ASSERT_TRUE(!pSoundSource.isNull()); EXPECT_EQ(OK, pSoundSource->open()); - EXPECT_LT(0, pSoundSource->getChannels()); - EXPECT_LT(0UL, pSoundSource->getSampleRate()); - EXPECT_LT(0UL, pSoundSource->length()); + EXPECT_LT(0UL, pSoundSource->getChannelCount()); + EXPECT_LT(0UL, pSoundSource->getFrameRate()); + EXPECT_LT(0UL, pSoundSource->getFrameCount()); } } @@ -43,16 +44,18 @@ TEST_F(SoundSourceProxyTest, readArtist) { Mixxx::SoundSourcePointer p(loadProxy( QDir::currentPath().append("/src/test/id3-test-data/artist.mp3"))); ASSERT_TRUE(!p.isNull()); - EXPECT_EQ(OK, p->parseHeader()); - EXPECT_EQ("Test Artist", p->getArtist()); + Mixxx::TrackMetadata trackMetadata; + EXPECT_EQ(OK, p->parseMetadata(&trackMetadata)); + EXPECT_EQ("Test Artist", trackMetadata.getArtist()); } TEST_F(SoundSourceProxyTest, TOAL_TPE2) { Mixxx::SoundSourcePointer p(loadProxy( QDir::currentPath().append("/src/test/id3-test-data/TOAL_TPE2.mp3"))); ASSERT_TRUE(!p.isNull()); - EXPECT_EQ(OK, p->parseHeader()); - EXPECT_EQ("TITLE2", p->getArtist()); - EXPECT_EQ("ARTIST", p->getAlbum()); - EXPECT_EQ("TITLE", p->getAlbumArtist()); + Mixxx::TrackMetadata trackMetadata; + EXPECT_EQ(OK, p->parseMetadata(&trackMetadata)); + EXPECT_EQ("TITLE2", trackMetadata.getArtist()); + EXPECT_EQ("ARTIST", trackMetadata.getAlbum()); + EXPECT_EQ("TITLE", trackMetadata.getAlbumArtist()); } diff --git a/src/trackinfoobject.cpp b/src/trackinfoobject.cpp index 34c020e5d71..29368756899 100644 --- a/src/trackinfoobject.cpp +++ b/src/trackinfoobject.cpp @@ -30,6 +30,7 @@ #include "controlobject.h" #include "soundsourceproxy.h" +#include "trackmetadata.h" #include "xmlparse.h" #include "track/beatfactory.h" #include "track/keyfactory.h" @@ -167,86 +168,91 @@ void TrackInfoObject::parse(bool parseCoverArt) { qDebug() << "TrackInfoObject::parse()" << canonicalLocation; } - // Parse the information stored in the sound file. SoundSourceProxy proxy(canonicalLocation, m_pSecurityToken); Mixxx::SoundSourcePointer pSoundSource(proxy.getSoundSource()); - if (pSoundSource && pSoundSource->parseHeader() == OK) { - - // Dump the metadata extracted from the file into the track. - - // TODO(XXX): This involves locking the mutex for every setXXX - // method. We should figure out an optimization where there are private - // setters that don't lock the mutex. + if (pSoundSource) { + setType(pSoundSource->getType()); + + // Parse the information stored in the sound file. + Mixxx::TrackMetadata trackMetadata; + if (pSoundSource->parseMetadata(&trackMetadata) == OK) { + + // Dump the metadata extracted from the file into the track. + + // TODO(XXX): This involves locking the mutex for every setXXX + // method. We should figure out an optimization where there are private + // setters that don't lock the mutex. + + // If Artist, Title and Type fields are not blank, modify them. + // Otherwise, keep their current values. + // TODO(rryan): Should we re-visit this decision? + if (!(trackMetadata.getArtist().isEmpty())) { + setArtist(trackMetadata.getArtist()); + } else { + parseArtist(); + } - // If Artist, Title and Type fields are not blank, modify them. - // Otherwise, keep their current values. - // TODO(rryan): Should we re-visit this decision? - if (!(pSoundSource->getArtist().isEmpty())) { - setArtist(pSoundSource->getArtist()); - } else { - parseArtist(); - } + if (!(trackMetadata.getTitle().isEmpty())) { + setTitle(trackMetadata.getTitle()); + } else { + parseTitle(); + } - if (!(pSoundSource->getTitle().isEmpty())) { - setTitle(pSoundSource->getTitle()); - } else { - parseTitle(); - } + setAlbum(trackMetadata.getAlbum()); + setAlbumArtist(trackMetadata.getAlbumArtist()); + setYear(trackMetadata.getYear()); + setGenre(trackMetadata.getGenre()); + setComposer(trackMetadata.getComposer()); + setGrouping(trackMetadata.getGrouping()); + setComment(trackMetadata.getComment()); + setTrackNumber(trackMetadata.getTrackNumber()); + setChannels(trackMetadata.getChannels()); + setSampleRate(trackMetadata.getSampleRate()); + setDuration(trackMetadata.getDuration()); + setBitrate(trackMetadata.getBitrate()); + + float replayGain = trackMetadata.getReplayGain(); + if (replayGain != 0.0f) { + setReplayGain(replayGain); + } - if (!(pSoundSource->getType().isEmpty())) { - setType(pSoundSource->getType()); - } + // Need to set BPM after sample rate since beat grid creation depends on + // knowing the sample rate. Bug #1020438. + float bpm = trackMetadata.getBPM(); + if (bpm > 0) { + // do not delete beat grid if bpm is not set in file + setBpm(bpm); + } - setAlbum(pSoundSource->getAlbum()); - setAlbumArtist(pSoundSource->getAlbumArtist()); - setYear(pSoundSource->getYear()); - setGenre(pSoundSource->getGenre()); - setComposer(pSoundSource->getComposer()); - setGrouping(pSoundSource->getGrouping()); - setComment(pSoundSource->getComment()); - setTrackNumber(pSoundSource->getTrackNumber()); - setChannels(pSoundSource->getChannels()); - setSampleRate(pSoundSource->getSampleRate()); - setDuration(pSoundSource->getDuration()); - setBitrate(pSoundSource->getBitrate()); - - float replayGain = pSoundSource->getReplayGain(); - if (replayGain != 0.0f) { - setReplayGain(replayGain); - } + QString key = trackMetadata.getKey(); + if (!key.isEmpty()) { + setKeyText(key, mixxx::track::io::key::FILE_METADATA); + } - // Need to set BPM after sample rate since beat grid creation depends on - // knowing the sample rate. Bug #1020438. - float bpm = pSoundSource->getBPM(); - if (bpm > 0) { - // do not delete beat grid if bpm is not set in file - setBpm(bpm); - } + if (parseCoverArt) { + m_coverArt.image = pSoundSource->parseCoverArt(); + if (!m_coverArt.image.isNull()) { + m_coverArt.info.hash = CoverArtUtils::calculateHash( + m_coverArt.image); + m_coverArt.info.coverLocation = QString(); + m_coverArt.info.type = CoverInfo::METADATA; + m_coverArt.info.source = CoverInfo::GUESSED; + } + } - QString key = pSoundSource->getKey(); - if (!key.isEmpty()) { - setKeyText(key, mixxx::track::io::key::FILE_METADATA); - } + setHeaderParsed(true); + } else { + qDebug() << "TrackInfoObject::parse() error at file" + << canonicalLocation; + setHeaderParsed(false); - if (parseCoverArt) { - m_coverArt.image = pSoundSource->parseCoverArt(); - if (!m_coverArt.image.isNull()) { - m_coverArt.info.hash = CoverArtUtils::calculateHash( - m_coverArt.image); - m_coverArt.info.coverLocation = QString(); - m_coverArt.info.type = CoverInfo::METADATA; - m_coverArt.info.source = CoverInfo::GUESSED; - } + // Add basic information derived from the filename: + parseFilename(); } - - setHeaderParsed(true); } else { qDebug() << "TrackInfoObject::parse() error at file" << canonicalLocation; setHeaderParsed(false); - - // Add basic information derived from the filename: - parseFilename(); } } diff --git a/src/trackinfoobject.h b/src/trackinfoobject.h index bb36d50f215..59202e42a52 100644 --- a/src/trackinfoobject.h +++ b/src/trackinfoobject.h @@ -28,7 +28,6 @@ #include #include #include -#include #include "library/dao/cue.h" #include "library/coverart.h" diff --git a/src/trackmetadata.cpp b/src/trackmetadata.cpp new file mode 100644 index 00000000000..f1c891a80a7 --- /dev/null +++ b/src/trackmetadata.cpp @@ -0,0 +1,49 @@ +#include "trackmetadata.h" + +#include "util/math.h" + +namespace Mixxx { + +namespace { +const float BPM_ZERO = 0.0f; +const float BPM_MAX = 300.0f; + +float parseBpmString(const QString& sBpm) { + float bpm = sBpm.toFloat(); + while (bpm > BPM_MAX) { + bpm /= 10.0f; + } + return bpm; +} + +float parseReplayGainString(QString sReplayGain) { + QString ReplayGainstring = sReplayGain.remove(" dB"); + float fReplayGain = db2ratio(ReplayGainstring.toFloat()); + // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. + // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by setting value to 0 (i.e. rescan via analyserrg) + if (fReplayGain == 1.0f) { + fReplayGain = 0.0f; + } + return fReplayGain; +} + +} + +TrackMetadata::TrackMetadata() + : m_channels(0), m_sampleRate(0), m_bitrate(0), m_duration(0), m_replayGain(0.0f), m_bpm(BPM_ZERO) { +} + +void TrackMetadata::setBpmString(QString sBpm) { + if (!sBpm.isEmpty()) { + float fBpm = parseBpmString(sBpm); + if (BPM_ZERO < fBpm) { + setBpm(fBpm); + } + } +} + +void TrackMetadata::setReplayGainString(QString sReplayGain) { + setReplayGain(parseReplayGainString(sReplayGain)); +} + +} //namespace Mixxx diff --git a/src/trackmetadata.h b/src/trackmetadata.h new file mode 100644 index 00000000000..10fb0794371 --- /dev/null +++ b/src/trackmetadata.h @@ -0,0 +1,166 @@ +#ifndef TRACKMETADATA_H +#define TRACKMETADATA_H + +#include + +namespace Mixxx { + +// DTO for track metadata properties. Must not be subclassed (no virtual destructor)! +class TrackMetadata { +public: + TrackMetadata(); + + inline const QString& getArtist() const { + return m_artist; + } + inline void setArtist(QString artist) { + m_artist = artist; + } + + inline const QString& getTitle() const { + return m_title; + } + inline void setTitle(QString title) { + m_title = title; + } + + inline const QString& getAlbum() const { + return m_album; + } + inline void setAlbum(QString album) { + m_album = album; + } + + inline const QString& getAlbumArtist() const { + return m_albumArtist; + } + inline void setAlbumArtist(QString albumArtist) { + m_albumArtist = albumArtist; + } + + inline const QString& getGenre() const { + return m_genre; + } + inline void setGenre(QString genre) { + m_genre = genre; + } + + inline const QString& getComment() const { + return m_comment; + } + inline void setComment(QString comment) { + m_comment = comment; + } + + // year, date or date/time + inline const QString& getYear() const { + return m_year; + } + inline void setYear(QString year) { + m_year = year; + } + + inline const QString& getTrackNumber() const { + return m_trackNumber; + } + inline void setTrackNumber(QString trackNumber) { + m_trackNumber = trackNumber; + } + + inline const QString& getComposer() const { + return m_composer; + } + inline void setComposer(QString composer) { + m_composer = composer; + } + + inline const QString& getGrouping() const { + return m_grouping; + } + inline void setGrouping(QString grouping) { + m_grouping = grouping; + } + + inline const QString& getKey() const { + return m_key; + } + inline void setKey(QString key) { + m_key = key; + } + + // #channels + inline int getChannels() const { + return m_channels; + } + inline void setChannels(int channels) { + m_channels = channels; + } + + // Hz + inline int getSampleRate() const { + return m_sampleRate; + } + inline void setSampleRate(int sampleRate) { + m_sampleRate = sampleRate; + } + + // kbit / s + inline int getBitrate() const { + return m_bitrate; + } + inline void setBitrate(int bitrate) { + m_bitrate = bitrate; + } + + // #seconds + inline int getDuration() const { + return m_duration; + } + inline void setDuration(int duration) { + m_duration = duration; + } + + // beats / minute + inline float getBPM() const { + return m_bpm; + } + inline void setBpm(float bpm) { + m_bpm = bpm; + } + void setBpmString(QString sBpm); + + inline float getReplayGain() const { + return m_replayGain; + } + inline void setReplayGain(float replayGain) { + m_replayGain = replayGain; + } + void setReplayGainString(QString sReplayGain); + +private: + QString m_artist; + QString m_title; + QString m_album; + QString m_albumArtist; + QString m_genre; + QString m_comment; + QString m_year; + QString m_trackNumber; + QString m_composer; + QString m_grouping; + QString m_key; + + // The following members need to be initialized + // explicitly in the constructor! Otherwise their + // value is undefined. + int m_channels; + int m_sampleRate; + int m_bitrate; + int m_duration; + float m_replayGain; + float m_bpm; +}; + +} + +#endif diff --git a/src/soundsourcetaglib.cpp b/src/trackmetadatataglib.cpp similarity index 64% rename from src/soundsourcetaglib.cpp rename to src/trackmetadatataglib.cpp index cb42025a711..c2d81d38d38 100644 --- a/src/soundsourcetaglib.cpp +++ b/src/trackmetadatataglib.cpp @@ -1,13 +1,4 @@ -/*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -***************************************************************************/ - -#include "soundsourcetaglib.h" +#include "trackmetadatataglib.h" #include #include @@ -22,9 +13,7 @@ #include #include - -#include - +#include namespace Mixxx { @@ -32,7 +21,7 @@ namespace Mixxx // static namespace { - const bool s_bDebugMetadata = false; + const bool kDebugMetadata = false; // Taglib strings can be NULL and using it could cause some segfaults, // so in this case it will return a QString() @@ -66,25 +55,25 @@ namespace } -bool readFileHeader(SoundSource* pSoundSource, const TagLib::File& f) { - if (s_bDebugMetadata) { - qDebug() << "Parsing" << pSoundSource->getFilename(); +bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& f) { + if (kDebugMetadata) { + qDebug() << "Parsing" << f.name(); } if (f.isValid()) { const TagLib::AudioProperties *properties = f.audioProperties(); if (properties) { - pSoundSource->setChannels(properties->channels()); - pSoundSource->setSampleRate(properties->sampleRate()); - pSoundSource->setDuration(properties->length()); - pSoundSource->setBitrate(properties->bitrate()); + pTrackMetadata->setChannels(properties->channels()); + pTrackMetadata->setSampleRate(properties->sampleRate()); + pTrackMetadata->setDuration(properties->length()); + pTrackMetadata->setBitrate(properties->bitrate()); - if (s_bDebugMetadata) { + if (kDebugMetadata) { qDebug() << "TagLib" - << "channels" << pSoundSource->getChannels() - << "sampleRate" << pSoundSource->getSampleRate() - << "bitrate" << pSoundSource->getBitrate() - << "duration" << pSoundSource->getDuration(); + << "channels" << pTrackMetadata->getChannels() + << "sampleRate" << pTrackMetadata->getSampleRate() + << "bitrate" << pTrackMetadata->getBitrate() + << "duration" << pTrackMetadata->getDuration(); } return true; @@ -93,39 +82,39 @@ bool readFileHeader(SoundSource* pSoundSource, const TagLib::File& f) { return false; } -void readTag(SoundSource* pSoundSource, const TagLib::Tag& tag) { - pSoundSource->setTitle(toQString(tag.title())); - pSoundSource->setArtist(toQString(tag.artist())); - pSoundSource->setAlbum(toQString(tag.album())); - pSoundSource->setComment(toQString(tag.comment())); - pSoundSource->setGenre(toQString(tag.genre())); +void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { + pTrackMetadata->setTitle(toQString(tag.title())); + pTrackMetadata->setArtist(toQString(tag.artist())); + pTrackMetadata->setAlbum(toQString(tag.album())); + pTrackMetadata->setComment(toQString(tag.comment())); + pTrackMetadata->setGenre(toQString(tag.genre())); int iYear = tag.year(); if (iYear > 0) { - pSoundSource->setYear(QString("%1").arg(iYear)); + pTrackMetadata->setYear(QString("%1").arg(iYear)); } int iTrack = tag.track(); if (iTrack > 0) { - pSoundSource->setTrackNumber(QString("%1").arg(iTrack)); + pTrackMetadata->setTrackNumber(QString("%1").arg(iTrack)); } - if (s_bDebugMetadata) { + if (kDebugMetadata) { qDebug() << "TagLib" - << "title" << pSoundSource->getTitle() - << "artist" << pSoundSource->getArtist() - << "album" << pSoundSource->getAlbum() - << "comment" << pSoundSource->getComment() - << "genre" << pSoundSource->getGenre() - << "year" << pSoundSource->getYear() - << "trackNumber" << pSoundSource->getTrackNumber(); + << "title" << pTrackMetadata->getTitle() + << "artist" << pTrackMetadata->getArtist() + << "album" << pTrackMetadata->getAlbum() + << "comment" << pTrackMetadata->getComment() + << "genre" << pTrackMetadata->getGenre() + << "year" << pTrackMetadata->getYear() + << "trackNumber" << pTrackMetadata->getTrackNumber(); } } -void readID3v2Tag(SoundSource* pSoundSource, const TagLib::ID3v2::Tag& id3v2) { +void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& id3v2) { // Print every frame in the file. - if (s_bDebugMetadata) { + if (kDebugMetadata) { TagLib::ID3v2::FrameList::ConstIterator it = id3v2.frameList().begin(); for(; it != id3v2.frameList().end(); it++) { qDebug() << "ID3V2" << (*it)->frameID().data() << "-" @@ -133,18 +122,18 @@ void readID3v2Tag(SoundSource* pSoundSource, const TagLib::ID3v2::Tag& id3v2) { } } - readTag(pSoundSource, id3v2); + readTag(pTrackMetadata, id3v2); TagLib::ID3v2::FrameList bpmFrame = id3v2.frameListMap()["TBPM"]; if (!bpmFrame.isEmpty()) { QString sBpm = toQString(bpmFrame.front()->toString()); - pSoundSource->setBpmString(sBpm); + pTrackMetadata->setBpmString(sBpm); } TagLib::ID3v2::FrameList keyFrame = id3v2.frameListMap()["TKEY"]; if (!keyFrame.isEmpty()) { QString sKey = toQString(keyFrame.front()->toString()); - pSoundSource->setKey(sKey); + pTrackMetadata->setKey(sKey); } // Foobar2000-style ID3v2.3.0 tags @@ -157,12 +146,12 @@ void readID3v2Tag(SoundSource* pSoundSource, const TagLib::ID3v2::Tag& id3v2) { QString desc = toQString(ReplayGainframe->description()).toLower(); if (desc == "replaygain_album_gain") { QString sReplayGain = toQString(ReplayGainframe->fieldList()[1]); - pSoundSource->setReplayGainString(sReplayGain); + pTrackMetadata->setReplayGainString(sReplayGain); } //Prefer track gain over album gain. if (desc == "replaygain_track_gain") { QString sReplayGain = toQString(ReplayGainframe->fieldList()[1]); - pSoundSource->setReplayGainString(sReplayGain); + pTrackMetadata->setReplayGainString(sReplayGain); } } } @@ -170,92 +159,92 @@ void readID3v2Tag(SoundSource* pSoundSource, const TagLib::ID3v2::Tag& id3v2) { TagLib::ID3v2::FrameList albumArtistFrame = id3v2.frameListMap()["TPE2"]; if (!albumArtistFrame.isEmpty()) { QString sAlbumArtist = toQString(albumArtistFrame.front()->toString()); - pSoundSource->setAlbumArtist(sAlbumArtist); + pTrackMetadata->setAlbumArtist(sAlbumArtist); - if (pSoundSource->getArtist().length() == 0) { - pSoundSource->setArtist(sAlbumArtist); + if (pTrackMetadata->getArtist().length() == 0) { + pTrackMetadata->setArtist(sAlbumArtist); } } TagLib::ID3v2::FrameList originalAlbumFrame = id3v2.frameListMap()["TOAL"]; - if (pSoundSource->getAlbum().length() == 0 && !originalAlbumFrame.isEmpty()) { + if (pTrackMetadata->getAlbum().length() == 0 && !originalAlbumFrame.isEmpty()) { QString sOriginalAlbum = TStringToQString(originalAlbumFrame.front()->toString()); - pSoundSource->setAlbum(sOriginalAlbum); + pTrackMetadata->setAlbum(sOriginalAlbum); } TagLib::ID3v2::FrameList composerFrame = id3v2.frameListMap()["TCOM"]; if (!composerFrame.isEmpty()) { QString sComposer = toQString(composerFrame.front()->toString()); - pSoundSource->setComposer(sComposer); + pTrackMetadata->setComposer(sComposer); } TagLib::ID3v2::FrameList groupingFrame = id3v2.frameListMap()["TIT1"]; if (!groupingFrame.isEmpty()) { QString sGrouping = toQString(groupingFrame.front()->toString()); - pSoundSource->setGrouping(sGrouping); + pTrackMetadata->setGrouping(sGrouping); } } -void readAPETag(SoundSource* pSoundSource, const TagLib::APE::Tag& ape) { - if (s_bDebugMetadata) { +void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& ape) { + if (kDebugMetadata) { for(TagLib::APE::ItemListMap::ConstIterator it = ape.itemListMap().begin(); it != ape.itemListMap().end(); ++it) { qDebug() << "APE" << toQString((*it).first) << "-" << toQString((*it).second.toString()); } } - readTag(pSoundSource, ape); + readTag(pTrackMetadata, ape); if (ape.itemListMap().contains("BPM")) { - pSoundSource->setBpmString(toQString(ape.itemListMap()["BPM"])); + pTrackMetadata->setBpmString(toQString(ape.itemListMap()["BPM"])); } if (ape.itemListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pSoundSource->setReplayGainString(toQString(ape.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); + pTrackMetadata->setReplayGainString(toQString(ape.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. if (ape.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pSoundSource->setReplayGainString(toQString(ape.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); + pTrackMetadata->setReplayGainString(toQString(ape.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); } if (ape.itemListMap().contains("Album Artist")) { - pSoundSource->setAlbumArtist(toQString(ape.itemListMap()["Album Artist"])); + pTrackMetadata->setAlbumArtist(toQString(ape.itemListMap()["Album Artist"])); } if (ape.itemListMap().contains("Composer")) { - pSoundSource->setComposer(toQString(ape.itemListMap()["Composer"])); + pTrackMetadata->setComposer(toQString(ape.itemListMap()["Composer"])); } if (ape.itemListMap().contains("Grouping")) { - pSoundSource->setGrouping(toQString(ape.itemListMap()["Grouping"])); + pTrackMetadata->setGrouping(toQString(ape.itemListMap()["Grouping"])); } } -void readXiphComment(SoundSource* pSoundSource, const TagLib::Ogg::XiphComment& xiph) { - if (s_bDebugMetadata) { +void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& xiph) { + if (kDebugMetadata) { for (TagLib::Ogg::FieldListMap::ConstIterator it = xiph.fieldListMap().begin(); it != xiph.fieldListMap().end(); ++it) { qDebug() << "XIPH" << toQString((*it).first) << "-" << toQString((*it).second.toString()); } } - readTag(pSoundSource, xiph); + readTag(pTrackMetadata, xiph); // Some tags use "BPM" so check for that. if (xiph.fieldListMap().contains("BPM")) { - pSoundSource->setBpmString(toQString(xiph.fieldListMap()["BPM"])); + pTrackMetadata->setBpmString(toQString(xiph.fieldListMap()["BPM"])); } // Give preference to the "TEMPO" tag which seems to be more standard if (xiph.fieldListMap().contains("TEMPO")) { - pSoundSource->setBpmString(toQString(xiph.fieldListMap()["TEMPO"])); + pTrackMetadata->setBpmString(toQString(xiph.fieldListMap()["TEMPO"])); } if (xiph.fieldListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pSoundSource->setReplayGainString(toQString(xiph.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); + pTrackMetadata->setReplayGainString(toQString(xiph.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. if (xiph.fieldListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pSoundSource->setReplayGainString(toQString(xiph.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); + pTrackMetadata->setReplayGainString(toQString(xiph.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); } /* @@ -267,32 +256,32 @@ void readXiphComment(SoundSource* pSoundSource, const TagLib::Ogg::XiphComment& * or a "KEY" vorbis comment. */ if (xiph.fieldListMap().contains("KEY")) { - pSoundSource->setKey(toQString(xiph.fieldListMap()["KEY"])); + pTrackMetadata->setKey(toQString(xiph.fieldListMap()["KEY"])); } - if (pSoundSource->getKey().isEmpty() && xiph.fieldListMap().contains("INITIALKEY")) { - pSoundSource->setKey(toQString(xiph.fieldListMap()["INITIALKEY"])); + if (pTrackMetadata->getKey().isEmpty() && xiph.fieldListMap().contains("INITIALKEY")) { + pTrackMetadata->setKey(toQString(xiph.fieldListMap()["INITIALKEY"])); } if (xiph.fieldListMap().contains("ALBUMARTIST")) { - pSoundSource->setAlbumArtist(toQString(xiph.fieldListMap()["ALBUMARTIST"])); + pTrackMetadata->setAlbumArtist(toQString(xiph.fieldListMap()["ALBUMARTIST"])); } else { // try alternative field name if (xiph.fieldListMap().contains("ALBUM_ARTIST")) { - pSoundSource->setAlbumArtist(toQString(xiph.fieldListMap()["ALBUM_ARTIST"])); + pTrackMetadata->setAlbumArtist(toQString(xiph.fieldListMap()["ALBUM_ARTIST"])); } } if (xiph.fieldListMap().contains("COMPOSER")) { - pSoundSource->setComposer(toQString(xiph.fieldListMap()["COMPOSER"])); + pTrackMetadata->setComposer(toQString(xiph.fieldListMap()["COMPOSER"])); } if (xiph.fieldListMap().contains("GROUPING")) { - pSoundSource->setGrouping(toQString(xiph.fieldListMap()["GROUPING"])); + pTrackMetadata->setGrouping(toQString(xiph.fieldListMap()["GROUPING"])); } } -void readMP4Tag(SoundSource* pSoundSource, /*const*/ TagLib::MP4::Tag& mp4) { - if (s_bDebugMetadata) { +void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& mp4) { + if (kDebugMetadata) { for(TagLib::MP4::ItemListMap::ConstIterator it = mp4.itemListMap().begin(); it != mp4.itemListMap().end(); ++it) { qDebug() << "MP4" << toQString((*it).first) << "-" @@ -300,36 +289,36 @@ void readMP4Tag(SoundSource* pSoundSource, /*const*/ TagLib::MP4::Tag& mp4) { } } - readTag(pSoundSource, mp4); + readTag(pTrackMetadata, mp4); // Get BPM if (mp4.itemListMap().contains("tmpo")) { - pSoundSource->setBpmString(toQString(mp4.itemListMap()["tmpo"])); + pTrackMetadata->setBpmString(toQString(mp4.itemListMap()["tmpo"])); } else if (mp4.itemListMap().contains("----:com.apple.iTunes:BPM")) { // This is an alternate way of storing BPM. - pSoundSource->setBpmString(toQString(mp4.itemListMap()["----:com.apple.iTunes:BPM"])); + pTrackMetadata->setBpmString(toQString(mp4.itemListMap()["----:com.apple.iTunes:BPM"])); } // Get Album Artist if (mp4.itemListMap().contains("aART")) { - pSoundSource->setAlbumArtist(toQString(mp4.itemListMap()["aART"])); + pTrackMetadata->setAlbumArtist(toQString(mp4.itemListMap()["aART"])); } // Get Composer if (mp4.itemListMap().contains("\251wrt")) { - pSoundSource->setComposer(toQString(mp4.itemListMap()["\251wrt"])); + pTrackMetadata->setComposer(toQString(mp4.itemListMap()["\251wrt"])); } // Get Grouping if (mp4.itemListMap().contains("\251grp")) { - pSoundSource->setGrouping(toQString(mp4.itemListMap()["\251grp"])); + pTrackMetadata->setGrouping(toQString(mp4.itemListMap()["\251grp"])); } // Get KEY (conforms to Rapid Evolution) if (mp4.itemListMap().contains("----:com.apple.iTunes:KEY")) { QString key = toQString( mp4.itemListMap()["----:com.apple.iTunes:KEY"]); - pSoundSource->setKey(key); + pTrackMetadata->setKey(key); } // Apparently iTunes stores replaygain in this property. @@ -346,7 +335,7 @@ void readMP4Tag(SoundSource* pSoundSource, /*const*/ TagLib::MP4::Tag& mp4) { } } -QImage getCoverInID3v2Tag(const TagLib::ID3v2::Tag& id3v2) { +QImage readID3v2TagCover(const TagLib::ID3v2::Tag& id3v2) { QImage coverArt; TagLib::ID3v2::FrameList covertArtFrame = id3v2.frameListMap()["APIC"]; if (!covertArtFrame.isEmpty()) { @@ -359,7 +348,7 @@ QImage getCoverInID3v2Tag(const TagLib::ID3v2::Tag& id3v2) { return coverArt; } -QImage getCoverInAPETag(const TagLib::APE::Tag& ape) { +QImage readAPETagCover(const TagLib::APE::Tag& ape) { QImage coverArt; if (ape.itemListMap().contains("COVER ART (FRONT)")) { @@ -375,7 +364,7 @@ QImage getCoverInAPETag(const TagLib::APE::Tag& ape) { return coverArt; } -QImage getCoverInXiphComment(const TagLib::Ogg::XiphComment& xiph) { +QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& xiph) { QImage coverArt; if (xiph.fieldListMap().contains("METADATA_BLOCK_PICTURE")) { QByteArray data(QByteArray::fromBase64( @@ -392,7 +381,7 @@ QImage getCoverInXiphComment(const TagLib::Ogg::XiphComment& xiph) { return coverArt; } -QImage getCoverInMP4Tag(/*const*/ TagLib::MP4::Tag& mp4) { +QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& mp4) { QImage coverArt; if (mp4.itemListMap().contains("covr")) { TagLib::MP4::CoverArtList coverArtList = mp4.itemListMap()["covr"] diff --git a/src/soundsourcetaglib.h b/src/trackmetadatataglib.h similarity index 57% rename from src/soundsourcetaglib.h rename to src/trackmetadatataglib.h index ca204ab20d3..0577e5c6893 100644 --- a/src/soundsourcetaglib.h +++ b/src/trackmetadatataglib.h @@ -10,7 +10,7 @@ #ifndef SOUNDSOURCETAGLIB_H #define SOUNDSOURCETAGLIB_H -#include "soundsource.h" +#include "trackmetadata.h" #include #include @@ -18,26 +18,27 @@ #include #include +#include namespace Mixxx { - bool readFileHeader(SoundSource* pSoundSource, const TagLib::File& taglibFile); + bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& taglibFile); // Read generic tags (will implicitly be invoked from the specialized functions) - void readTag(SoundSource* pSoundSource, const TagLib::Tag& tag); + void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag); // Read specific tags - void readID3v2Tag(SoundSource* pSoundSource, const TagLib::ID3v2::Tag& id3v2Tag); - void readAPETag(SoundSource* pSoundSource, const TagLib::APE::Tag& ape); - void readXiphComment(SoundSource* pSoundSource, const TagLib::Ogg::XiphComment& xiph); - void readMP4Tag(SoundSource* pSoundSource, /*const*/ TagLib::MP4::Tag& mp4); + void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& id3v2Tag); + void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& ape); + void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& xiph); + void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& mp4); // In order to avoid processing images when it's not // needed (TIO building), we must process it separately. - QImage getCoverInID3v2Tag(const TagLib::ID3v2::Tag& id3v2); - QImage getCoverInAPETag(const TagLib::APE::Tag& ape); - QImage getCoverInXiphComment(const TagLib::Ogg::XiphComment& xiph); - QImage getCoverInMP4Tag(/*const*/ TagLib::MP4::Tag& mp4); + QImage readID3v2TagCover(const TagLib::ID3v2::Tag& id3v2); + QImage readAPETagCover(const TagLib::APE::Tag& ape); + QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& xiph); + QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& mp4); } //namespace Mixxx From 966e6780e22059e9e399107975291a60d2223884 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 14 Dec 2014 15:14:07 +0100 Subject: [PATCH 018/481] Fix missing FFmpeg initialization in unit tests --- src/test/mixxxtest.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/mixxxtest.cpp b/src/test/mixxxtest.cpp index 18b10d95e14..35face26320 100644 --- a/src/test/mixxxtest.cpp +++ b/src/test/mixxxtest.cpp @@ -1,6 +1,13 @@ #include "test/mixxxtest.h" #include "util/singleton.h" +#ifdef __FFMPEGFILE__ +extern "C" { +#include +#include +} +#endif + // Specialize the Singleton template for QApplication because it doesn't have a // 0-args constructor. @@ -9,6 +16,10 @@ QApplication* Singleton::create() { if (!m_instance) { static int argc = 1; static char* argv[1] = { strdup("test") }; +#ifdef __FFMPEGFILE__ + av_register_all(); + avcodec_register_all(); +#endif m_instance = new QApplication(argc, argv); } return m_instance; From 3ce7dbd1adb2d1804bfa317ef53c320163da29f4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 16 Dec 2014 17:18:49 +0100 Subject: [PATCH 019/481] Move audio properties from SoundSource to AudioSource --- .../soundsourcemediafoundation.cpp | 17 +++------ src/audiosource.cpp | 3 +- src/audiosource.h | 38 +++++++++++++++++++ src/cachingreaderworker.cpp | 4 -- src/musicbrainz/chromaprinter.cpp | 4 -- src/soundsource.cpp | 4 +- src/soundsource.h | 24 ------------ src/soundsourcemp3.cpp | 2 + src/soundsourceproxy.cpp | 12 +++--- 9 files changed, 55 insertions(+), 53 deletions(-) diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 9026b35f575..39280877b94 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -35,15 +35,15 @@ #include +namespace +{ + +const bool sDebug = false; + const int kNumChannels = 2; const int kSampleRate = 44100; const int kLeftoverSize = 4096; // in sample_type's, this seems to be the size MF AAC -const int kBitsPerSampleForBitrate = 16; // for bitrate calculation -// decoder likes to give - -const static bool sDebug = false; - -namespace { +const int kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give /** * Convert a 100ns Media Foundation value to a number of seconds. @@ -151,11 +151,6 @@ Result SoundSourceMediaFoundation::open() { return ERR; } - if (!readProperties()) { - qWarning() << "SSMF::readProperties failed"; - return ERR; - } - //Seek to position 0, which forces us to skip over all the header frames. //This makes sure we're ready to just let the Analyser rip and it'll //get the number of samples it expects (ie. no header frames). diff --git a/src/audiosource.cpp b/src/audiosource.cpp index b6d18b1416a..6d8451c3f95 100644 --- a/src/audiosource.cpp +++ b/src/audiosource.cpp @@ -13,7 +13,7 @@ namespace Mixxx { AudioSource::AudioSource() : m_channelCount(kChannelCountDefault), m_frameRate(kFrameRateDefault), m_frameCount( - kFrameCountDefault) { + kFrameCountDefault), m_bitrate(kBitrateDefault) { } AudioSource::~AudioSource() { @@ -33,6 +33,7 @@ void AudioSource::reset() { m_channelCount = kChannelCountDefault; m_frameRate = kFrameRateDefault; m_frameCount = kFrameCountDefault; + m_bitrate = kBitrateDefault; } AudioSource::size_type AudioSource::readStereoFrameSamplesInterleaved( diff --git a/src/audiosource.h b/src/audiosource.h index 5734e5d2a86..ba94a4b9ec8 100644 --- a/src/audiosource.h +++ b/src/audiosource.h @@ -36,6 +36,9 @@ class AudioSource { static const sample_type kSampleValueZero; static const sample_type kSampleValuePeak; + static const size_type kBitrateZero = 0; + static const size_type kBitrateDefault = kBitrateZero; + virtual ~AudioSource(); // Returns the number of channels. The number of channels @@ -72,6 +75,35 @@ class AudioSource { return kFrameCountZero >= getFrameCount(); } + inline bool isValid() const { + return isChannelCountValid() && isFrameRateValid(); + } + + inline bool isEmpty() const { + return isFrameCountEmpty(); + } + + // The bitrate in kbit/s (optional). + // Derived classes may set the actual (average) bitrate when + // opening the file. The bitrate is not needed for decoding, + // it is only used for informational purposes. + inline bool hasBitrate() const { + return kBitrateZero < m_bitrate; + } + inline size_type getBitrate() const { + return m_bitrate; + } + + // The actual duration in seconds. + // Only avalailable for valid files! + inline bool hasDuration() const { + return isValid(); + } + inline size_type getDuration() const { + Q_ASSERT(hasDuration()); // prevents division by zero + return getFrameCount() / getFrameRate(); + } + // #frames -> #samples template inline T frames2samples(T frameCount) const { @@ -134,12 +166,18 @@ class AudioSource { void setFrameRate(size_type frameRate); void setFrameCount(size_type frameCount); + inline void setBitrate(size_type bitrate) { + m_bitrate = bitrate; + } + void reset(); private: size_type m_channelCount; size_type m_frameRate; size_type m_frameCount; + + size_type m_bitrate; }; typedef QSharedPointer AudioSourcePointer; diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 8fe6254b75d..60f5d789326 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -122,10 +122,6 @@ namespace qWarning() << "Failed to open file:" << pTrack->getLocation(); return Mixxx::AudioSourcePointer(); } - if (pAudioSource->isFrameCountEmpty()) { - qWarning() << "File is empty:" << pTrack->getLocation(); - return Mixxx::AudioSourcePointer(); - } // successfully opened and readable return pAudioSource; } diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 1bdd62f9421..078b4f52e23 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -100,9 +100,5 @@ QString ChromaPrinter::getFingerPrint(TrackPointer pTrack) { qDebug() << "Skipping invalid file:" << pTrack->getLocation(); return QString(); } - if (pAudioSource->isFrameCountEmpty()) { - qDebug() << "Skipping empty file:" << pTrack->getLocation(); - return QString(); - } return calcFingerPrint(pAudioSource); } diff --git a/src/soundsource.cpp b/src/soundsource.cpp index e86f2b7b47c..9b045c9f218 100644 --- a/src/soundsource.cpp +++ b/src/soundsource.cpp @@ -21,11 +21,11 @@ namespace Mixxx { SoundSource::SoundSource(QString sFilename) - : m_sFilename(sFilename), m_sType(m_sFilename.section(".", -1).toLower()), m_bitrate(0) { + : m_sFilename(sFilename), m_sType(m_sFilename.section(".", -1).toLower()) { } SoundSource::SoundSource(QString sFilename, QString sType) - : m_sFilename(sFilename), m_sType(sType), m_bitrate(0) { + : m_sFilename(sFilename), m_sType(sType) { } SoundSource::~SoundSource() { diff --git a/src/soundsource.h b/src/soundsource.h index 47942b1365c..7d0c821d411 100644 --- a/src/soundsource.h +++ b/src/soundsource.h @@ -82,37 +82,13 @@ class SoundSource: public AudioSource { */ virtual Result open() = 0; - // The bitrate in kbit/s (optional). - // The actual bitrate might be determined and set when the file is opened. - inline bool hasBitrate() const { - return 0 < m_bitrate; - } - inline size_type getBitrate() const { - return m_bitrate; - } - - // The actual duration in seconds. - // The actual duration can be calculated after the file has been opened. - inline bool hasDuration() const { - return !isFrameCountEmpty() && isFrameRateValid(); - } - inline size_type getDuration() const { - return getFrameCount() / getFrameRate(); - } - protected: explicit SoundSource(QString sFilename); SoundSource(QString sFilename, QString sType); - inline void setBitrate(size_type bitrate) { - m_bitrate = bitrate; - } - private: const QString m_sFilename; const QString m_sType; - - size_type m_bitrate; }; typedef QSharedPointer SoundSourcePointer; diff --git a/src/soundsourcemp3.cpp b/src/soundsourcemp3.cpp index 5a43c31262d..b1ec4eea83f 100644 --- a/src/soundsourcemp3.cpp +++ b/src/soundsourcemp3.cpp @@ -246,6 +246,8 @@ void SoundSourceMp3::close() { m_fileSize = 0; m_pFileData = NULL; m_file.close(); + + Super::reset(); } SoundSourceMp3::MadSeekFrameList::size_type SoundSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index 1eefa6b913f..a5050df358c 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -310,15 +310,13 @@ Mixxx::SoundSourcePointer SoundSourceProxy::open() const { return Mixxx::SoundSourcePointer(); } - if (!m_pSoundSource->isChannelCountValid()) { - qWarning() << "Invalid number of channels" << m_pSoundSource->getFilename() << m_pSoundSource->getChannelCount(); + if (!m_pSoundSource->isValid()) { + qWarning() << "Invalid file:" << m_pSoundSource->getFilename() + << "channels" << m_pSoundSource->getChannelCount() + << "frame rate" << m_pSoundSource->getChannelCount(); return Mixxx::SoundSourcePointer(); } - if (!m_pSoundSource->isFrameRateValid()) { - qWarning() << "Invalid frame rate:" << m_pSoundSource->getFilename() << m_pSoundSource->getFrameRate(); - return Mixxx::SoundSourcePointer(); - } - if (m_pSoundSource->isFrameCountEmpty()) { + if (m_pSoundSource->isEmpty()) { qWarning() << "Empty file:" << m_pSoundSource->getFilename(); return Mixxx::SoundSourcePointer(); } From f5848ec6cea13cd77ded7c8166b94f820ed27dd1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 16 Dec 2014 17:20:58 +0100 Subject: [PATCH 020/481] Fix potential memory leak in SoundSourceOpus --- src/soundsourceopus.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/soundsourceopus.cpp b/src/soundsourceopus.cpp index 5ab8c450c51..e18bf4f9bb7 100644 --- a/src/soundsourceopus.cpp +++ b/src/soundsourceopus.cpp @@ -113,6 +113,24 @@ Mixxx::AudioSource::size_type SoundSourceOpus::readStereoFrameSamplesInterleaved return readCount; } +namespace +{ + class OggOpusFileOwner { + public: + explicit OggOpusFileOwner(OggOpusFile* pFile): m_pFile(pFile) { + } + ~OggOpusFileOwner() { + op_free(m_pFile); + } + operator OggOpusFile*() const { + return m_pFile; + } + private: + OggOpusFileOwner(const OggOpusFileOwner&); // disable copy constructor + OggOpusFile* const m_pFile; + }; +} + /* Parse the the file to get metadata */ @@ -121,7 +139,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) { QByteArray qBAFilename = getFilename().toLocal8Bit(); - OggOpusFile *l_ptrOpusFile = op_open_file(qBAFilename.constData(), &error); + OggOpusFileOwner l_ptrOpusFile(op_open_file(qBAFilename.constData(), &error)); pMetadata->setChannels(op_channel_count(l_ptrOpusFile, -1)); pMetadata->setSampleRate(kOpusSampleRate); @@ -190,8 +208,6 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) { //qDebug() << "Comment" << i << l_ptrOpusTags->comment_lengths[i] << //" (" << l_ptrOpusTags->user_comments[i] << ")" << l_STag << "*" << l_SPayload; } - - op_free(l_ptrOpusFile); #endif return OK; From 87210dbf826e642439258ff4c3ee3be340b42ba2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 16 Dec 2014 17:21:38 +0100 Subject: [PATCH 021/481] Delete obsolete #include directives and other cosmetics --- plugins/soundsourcem4a/soundsourcem4a.h | 1 - plugins/soundsourcewv/soundsourcewv.h | 2 +- src/soundsourcecoreaudio.cpp | 28 ++++++++++++------------- src/soundsourcecoreaudio.h | 9 ++------ src/soundsourceffmpeg.h | 8 ++----- src/soundsourceflac.cpp | 13 ++++++------ src/soundsourcemodplug.h | 2 ++ src/soundsourcemp3.cpp | 18 ++++++++-------- src/soundsourcemp3.h | 2 -- src/soundsourceoggvorbis.cpp | 16 ++++++-------- src/soundsourceopus.cpp | 16 ++++++-------- src/soundsourcesndfile.cpp | 21 ++++++++----------- 12 files changed, 56 insertions(+), 80 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 56029136238..2565144cc70 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -18,7 +18,6 @@ #define SOUNDSOURCEM4A_H #include "soundsource.h" - #include "defs_version.h" #ifdef __MP4V2__ diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 3b975e6e591..95eaa5b4b91 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -1,8 +1,8 @@ #ifndef SOUNDSOURCEWV_H #define SOUNDSOURCEWV_H -#include "defs_version.h" #include "soundsource.h" +#include "defs_version.h" #include "wavpack/wavpack.h" diff --git a/src/soundsourcecoreaudio.cpp b/src/soundsourcecoreaudio.cpp index 5f18ba64d0a..0b6d697d884 100644 --- a/src/soundsourcecoreaudio.cpp +++ b/src/soundsourcecoreaudio.cpp @@ -16,7 +16,6 @@ #include "soundsourcecoreaudio.h" #include "trackmetadatataglib.h" -#include "util/math.h" #include #include @@ -28,6 +27,19 @@ namespace Mixxx::AudioSource::size_type kChannelCount = 2; } +QList SoundSourceCoreAudio::supportedFileExtensions() { + QList list; + list.push_back("m4a"); + list.push_back("mp3"); + list.push_back("mp2"); + //Can add mp3, mp2, ac3, and others here if you want. + //See: + // http://developer.apple.com/library/mac/documentation/MusicAudio/Reference/AudioFileConvertRef/Reference/reference.html#//apple_ref/doc/c_ref/AudioFileTypeID + + //XXX: ... but make sure you implement handling for any new format in ParseHeader!!!!!! -- asantoni + return list; +} + SoundSourceCoreAudio::SoundSourceCoreAudio(QString filename) : Super(filename) , m_headerFrames(0) { @@ -233,17 +245,3 @@ QImage SoundSourceCoreAudio::parseCoverArt() { } return coverArt; } - -// static -QList SoundSourceCoreAudio::supportedFileExtensions() { - QList list; - list.push_back("m4a"); - list.push_back("mp3"); - list.push_back("mp2"); - //Can add mp3, mp2, ac3, and others here if you want. - //See: - // http://developer.apple.com/library/mac/documentation/MusicAudio/Reference/AudioFileConvertRef/Reference/reference.html#//apple_ref/doc/c_ref/AudioFileTypeID - - //XXX: ... but make sure you implement handling for any new format in ParseHeader!!!!!! -- asantoni - return list; -} diff --git a/src/soundsourcecoreaudio.h b/src/soundsourcecoreaudio.h index 08d166caf5b..e5a33c6c830 100644 --- a/src/soundsourcecoreaudio.h +++ b/src/soundsourcecoreaudio.h @@ -19,7 +19,8 @@ #ifndef SOUNDSOURCECOREAUDIO_H #define SOUNDSOURCECOREAUDIO_H -#include +#include "soundsource.h" + #include //In our tree at lib/apple/ #include "CAStreamBasicDescription.h" @@ -35,12 +36,6 @@ #include "AudioFormat.h" #endif -#include - -#include "soundsource.h" - -#include "util/defs.h" - class SoundSourceCoreAudio : public Mixxx::SoundSource { typedef SoundSource Super; diff --git a/src/soundsourceffmpeg.h b/src/soundsourceffmpeg.h index d2816a6366d..73731314ddf 100644 --- a/src/soundsourceffmpeg.h +++ b/src/soundsourceffmpeg.h @@ -18,14 +18,10 @@ #ifndef SOUNDSOURCEFFMPEG_H #define SOUNDSOURCEFFMPEG_H -#include +#include "soundsource.h" -#include -#include -#include -#include +#include -#include "soundsource.h" extern "C" { // Needed to ensure that macros in get defined. #ifndef __STDC_CONSTANT_MACROS diff --git a/src/soundsourceflac.cpp b/src/soundsourceflac.cpp index 7c5c1d10c08..0551fb398b9 100644 --- a/src/soundsourceflac.cpp +++ b/src/soundsourceflac.cpp @@ -23,6 +23,12 @@ #include +QList SoundSourceFLAC::supportedFileExtensions() { + QList list; + list.push_back("flac"); + return list; +} + SoundSourceFLAC::SoundSourceFLAC(QString filename) : Super(filename, "flac"), m_file(filename), m_decoder(NULL), m_minBlocksize( 0), m_maxBlocksize(0), m_minFramesize(0), m_maxFramesize(0), m_sampleScale( @@ -221,13 +227,6 @@ QImage SoundSourceFLAC::parseCoverArt() { return coverArt; } -// static -QList SoundSourceFLAC::supportedFileExtensions() { - QList list; - list.push_back("flac"); - return list; -} - // flac callback methods FLAC__StreamDecoderReadStatus SoundSourceFLAC::flacRead(FLAC__byte buffer[], size_t *bytes) { diff --git a/src/soundsourcemodplug.h b/src/soundsourcemodplug.h index bf2c1f99410..efc2b9c4291 100644 --- a/src/soundsourcemodplug.h +++ b/src/soundsourcemodplug.h @@ -7,6 +7,8 @@ namespace ModPlug { #include } +#include + #include // Class for reading tracker files using libmodplug. diff --git a/src/soundsourcemp3.cpp b/src/soundsourcemp3.cpp index b1ec4eea83f..a21364f23b1 100644 --- a/src/soundsourcemp3.cpp +++ b/src/soundsourcemp3.cpp @@ -19,6 +19,8 @@ #include "trackmetadatataglib.h" #include "util/math.h" +#include + #include namespace @@ -56,6 +58,12 @@ namespace } } +QList SoundSourceMp3::supportedFileExtensions() { + QList list; + list.push_back("mp3"); + return list; +} + SoundSourceMp3::SoundSourceMp3(QString qFilename) : Super(qFilename, "mp3"), m_file(qFilename), @@ -69,18 +77,10 @@ SoundSourceMp3::SoundSourceMp3(QString qFilename) mad_synth_init(&m_madSynth); } -SoundSourceMp3::~SoundSourceMp3() -{ +SoundSourceMp3::~SoundSourceMp3() { close(); } -QList SoundSourceMp3::supportedFileExtensions() -{ - QList list; - list.push_back("mp3"); - return list; -} - Result SoundSourceMp3::open() { if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file:" << getFilename(); diff --git a/src/soundsourcemp3.h b/src/soundsourcemp3.h index 32383d93ee8..4410356e257 100644 --- a/src/soundsourcemp3.h +++ b/src/soundsourcemp3.h @@ -20,8 +20,6 @@ #include "soundsource.h" -#include -#include #ifdef _MSC_VER // So mad.h doesn't try to use inline assembly which MSVC doesn't support. // Notably, FPM_64BIT does not require a 64-bit machine. It merely requires a diff --git a/src/soundsourceoggvorbis.cpp b/src/soundsourceoggvorbis.cpp index a66cb1d51bd..4b6ea4904c4 100644 --- a/src/soundsourceoggvorbis.cpp +++ b/src/soundsourceoggvorbis.cpp @@ -22,6 +22,12 @@ #include +QList SoundSourceOggVorbis::supportedFileExtensions() { + QList list; + list.push_back("ogg"); + return list; +} + SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) : Super(qFilename, "ogg") { memset(&m_vf, 0, sizeof(m_vf)); @@ -178,13 +184,3 @@ QImage SoundSourceOggVorbis::parseCoverArt() { return QImage(); } } - -/* - Return the length of the file in samples. - */ - -QList SoundSourceOggVorbis::supportedFileExtensions() { - QList list; - list.push_back("ogg"); - return list; -} diff --git a/src/soundsourceopus.cpp b/src/soundsourceopus.cpp index e18bf4f9bb7..896582b3b49 100644 --- a/src/soundsourceopus.cpp +++ b/src/soundsourceopus.cpp @@ -12,6 +12,12 @@ namespace { Mixxx::AudioSource::size_type kOpusSampleRate = 48000; } +QList SoundSourceOpus::supportedFileExtensions() { + QList list; + list.push_back("opus"); + return list; +} + SoundSourceOpus::SoundSourceOpus(QString qFilename) : Super(qFilename, "opus"), m_pOggOpusFile(NULL) { } @@ -213,16 +219,6 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -/* - Return the length of the file in samples. - */ - -QList SoundSourceOpus::supportedFileExtensions() { - QList list; - list.push_back("opus"); - return list; -} - QImage SoundSourceOpus::parseCoverArt() { #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) TagLib::Ogg::Opus::File f(getFilename().toLocal8Bit().constData()); diff --git a/src/soundsourcesndfile.cpp b/src/soundsourcesndfile.cpp index c0b25fa2a1c..2b9c162a70d 100644 --- a/src/soundsourcesndfile.cpp +++ b/src/soundsourcesndfile.cpp @@ -7,18 +7,6 @@ #include #include -/* - Class for reading files using libsndfile - */ -SoundSourceSndFile::SoundSourceSndFile(QString qFilename) - : Super(qFilename), m_pSndFile(NULL) { - memset(&m_sfInfo, 0, sizeof(m_sfInfo)); -} - -SoundSourceSndFile::~SoundSourceSndFile() { - close(); -} - QList SoundSourceSndFile::supportedFileExtensions() { QList list; list.push_back("aiff"); @@ -28,6 +16,15 @@ QList SoundSourceSndFile::supportedFileExtensions() { return list; } +SoundSourceSndFile::SoundSourceSndFile(QString qFilename) + : Super(qFilename), m_pSndFile(NULL) { + memset(&m_sfInfo, 0, sizeof(m_sfInfo)); +} + +SoundSourceSndFile::~SoundSourceSndFile() { + close(); +} + Result SoundSourceSndFile::open() { #ifdef __WINDOWS__ // Pointer valid until string changed From 8ad85cd0eb5c8c8f5b6c9c35bd40ef5fa4fd14d1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 16 Dec 2014 17:22:19 +0100 Subject: [PATCH 022/481] Eliminate unnecessary local variables This is safe according to the C++ standard: "Temporary objects are destroyed as the last step in evaluating the full-expression (1.9) that (lexically) contains the point where they were created. [12.2/3]" --- plugins/soundsourcem4a/soundsourcem4a.cpp | 5 ++--- plugins/soundsourcewv/soundsourcewv.cpp | 11 ++++----- src/engine/sidechain/enginerecord.cpp | 3 +-- src/soundsourceffmpeg.cpp | 27 ++++++++++------------- src/soundsourceflac.cpp | 6 ++--- src/soundsourcemodplug.h | 2 -- src/soundsourcemp3.cpp | 3 +-- src/soundsourceoggvorbis.cpp | 6 ++--- src/soundsourceopus.cpp | 12 +++++----- src/soundsourcesndfile.cpp | 17 ++++++-------- 10 files changed, 36 insertions(+), 56 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 1c93fb73005..45125aa1cfb 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -101,11 +101,10 @@ Result SoundSourceM4A::open() { } /* open MP4 file, check for >= ver 1.9.1 */ - const QByteArray qbaFilename(getFilename().toLocal8Bit()); #if MP4V2_PROJECT_version_hex <= 0x00010901 - m_hFile = MP4Read(qbaFilename.constData(), 0); + m_hFile = MP4Read(getFilename().toLocal8Bit().constData(), 0); #else - m_hFile = MP4Read(qbaFilename.constData()); + m_hFile = MP4Read(getFilename().toLocal8Bit().constData()); #endif if (MP4_INVALID_FILE_HANDLE == m_hFile) { close(); diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 32d9e7c28c7..f837ad97fad 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -24,11 +24,9 @@ SoundSourceWV::~SoundSourceWV() { } Result SoundSourceWV::open() { - QByteArray qBAFilename(getFilename().toLocal8Bit()); - char msg[80]; //hold possible error message - - m_wpc = WavpackOpenFileInput(qBAFilename.constData(), msg, - OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); + char msg[80]; // hold possible error message + m_wpc = WavpackOpenFileInput(getFilename().toLocal8Bit().constData(), msg, + OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); if (!m_wpc) { qDebug() << "SSWV::open: failed to open file : " << msg; return ERR; @@ -77,8 +75,7 @@ AudioSource::size_type SoundSourceWV::readFrameSamplesInterleaved( } Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) { - const QByteArray qBAFilename(getFilename().toLocal8Bit()); - TagLib::WavPack::File f(qBAFilename.constData()); + TagLib::WavPack::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; diff --git a/src/engine/sidechain/enginerecord.cpp b/src/engine/sidechain/enginerecord.cpp index cac3cdb78e4..e95baea7a99 100644 --- a/src/engine/sidechain/enginerecord.cpp +++ b/src/engine/sidechain/enginerecord.cpp @@ -291,8 +291,7 @@ bool EngineRecord::openFile() { LPCWSTR lpcwFilename = (LPCWSTR)m_fileName.utf16(); m_pSndfile = sf_wchar_open(lpcwFilename, SFM_WRITE, &m_sfInfo); #else - QByteArray qbaFilename = m_fileName.toLocal8Bit(); - m_pSndfile = sf_open(qbaFilename.constData(), SFM_WRITE, &m_sfInfo); + m_pSndfile = sf_open(m_fileName.toLocal8Bit().constData(), SFM_WRITE, &m_sfInfo); #endif if (m_pSndfile) { sf_command(m_pSndfile, SFC_SET_NORM_FLOAT, NULL, SF_TRUE); diff --git a/src/soundsourceffmpeg.cpp b/src/soundsourceffmpeg.cpp index cbe4e48db3d..0d3a1339aae 100644 --- a/src/soundsourceffmpeg.cpp +++ b/src/soundsourceffmpeg.cpp @@ -342,9 +342,7 @@ Result SoundSourceFFmpeg::open() { unsigned int i; AVDictionary *l_iFormatOpts = NULL; - QByteArray qBAFilename = getFilename().toLocal8Bit(); - - qDebug() << "New SoundSourceFFmpeg :" << qBAFilename; + qDebug() << "New SoundSourceFFmpeg :" << getFilename(); m_pFormatCtx = avformat_alloc_context(); @@ -353,9 +351,9 @@ Result SoundSourceFFmpeg::open() { #endif // Open file and make m_pFormatCtx - if (avformat_open_input(&m_pFormatCtx, qBAFilename.constData(), NULL, + if (avformat_open_input(&m_pFormatCtx, getFilename().toLocal8Bit().constData(), NULL, &l_iFormatOpts)!=0) { - qDebug() << "av_open_input_file: cannot open" << qBAFilename; + qDebug() << "av_open_input_file: cannot open" << getFilename(); return ERR; } @@ -367,13 +365,13 @@ Result SoundSourceFFmpeg::open() { // Retrieve stream information if (avformat_find_stream_info(m_pFormatCtx, NULL)<0) { - qDebug() << "av_find_stream_info: cannot open" << qBAFilename; + qDebug() << "av_find_stream_info: cannot open" << getFilename(); return ERR; } //debug only (Enable if needed) - //av_dump_format(m_pFormatCtx, 0, qBAFilename.constData(), false); + //av_dump_format(m_pFormatCtx, 0, getFilename().toLocal8Bit().constData(), false); // Find the first video stream m_iAudioStream=-1; @@ -385,7 +383,7 @@ Result SoundSourceFFmpeg::open() { } if (m_iAudioStream==-1) { qDebug() << "ffmpeg: cannot find an audio stream: cannot open" - << qBAFilename; + << getFilename(); return ERR; } @@ -394,12 +392,12 @@ Result SoundSourceFFmpeg::open() { // Find the decoder for the audio stream if (!(m_pCodec=avcodec_find_decoder(m_pCodecCtx->codec_id))) { - qDebug() << "ffmpeg: cannot find a decoder for" << qBAFilename; + qDebug() << "ffmpeg: cannot find a decoder for" << getFilename(); return ERR; } if (avcodec_open2(m_pCodecCtx, m_pCodec, NULL)<0) { - qDebug() << "ffmpeg: cannot open" << qBAFilename; + qDebug() << "ffmpeg: cannot open" << getFilename(); return ERR; } @@ -522,7 +520,6 @@ Mixxx::AudioSource::size_type SoundSourceFFmpeg::readFrameSamplesInterleaved(siz Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) { qDebug() << "ffmpeg: SoundSourceFFmpeg::parseMetadata" << getFilename(); - QByteArray qBAFilename = getFilename().toLocal8Bit(); AVFormatContext *FmtCtx = avformat_alloc_context(); AVCodecContext *CodecCtx; @@ -530,9 +527,9 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) { unsigned int i; AVDictionary *l_iFormatOpts = NULL; - if (avformat_open_input(&FmtCtx, qBAFilename.constData(), NULL, + if (avformat_open_input(&FmtCtx, getFilename().toLocal8Bit().constData(), NULL, &l_iFormatOpts) !=0) { - qDebug() << "av_open_input_file: cannot open" << qBAFilename.constData(); + qDebug() << "av_open_input_file: cannot open" << getFilename(); return ERR; } @@ -549,7 +546,7 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) { // Retrieve stream information if (avformat_find_stream_info(FmtCtx, NULL)<0) { qDebug() << "av_find_stream_info: Can't find metadata" << - qBAFilename.constData(); + getFilename(); return ERR; } @@ -560,7 +557,7 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) { } if (m_iAudioStream==-1) { qDebug() << "cannot find an audio stream: Can't find stream" << - qBAFilename.constData(); + getFilename(); return ERR; } diff --git a/src/soundsourceflac.cpp b/src/soundsourceflac.cpp index 0551fb398b9..12542e8dd0f 100644 --- a/src/soundsourceflac.cpp +++ b/src/soundsourceflac.cpp @@ -173,8 +173,7 @@ Mixxx::AudioSource::size_type SoundSourceFLAC::readFrameSamplesInterleaved( } Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) { - const QByteArray qBAFilename(getFilename().toLocal8Bit()); - TagLib::FLAC::File f(qBAFilename.constData()); + TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -202,8 +201,7 @@ Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) { } QImage SoundSourceFLAC::parseCoverArt() { - const QByteArray qBAFilename(getFilename().toLocal8Bit()); - TagLib::FLAC::File f(qBAFilename.constData()); + TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); QImage coverArt; TagLib::Ogg::XiphComment *xiph(f.xiphComment()); if (xiph) { diff --git a/src/soundsourcemodplug.h b/src/soundsourcemodplug.h index efc2b9c4291..bf2c1f99410 100644 --- a/src/soundsourcemodplug.h +++ b/src/soundsourcemodplug.h @@ -7,8 +7,6 @@ namespace ModPlug { #include } -#include - #include // Class for reading tracker files using libmodplug. diff --git a/src/soundsourcemp3.cpp b/src/soundsourcemp3.cpp index a21364f23b1..bde92428a8d 100644 --- a/src/soundsourcemp3.cpp +++ b/src/soundsourcemp3.cpp @@ -389,8 +389,7 @@ Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved( } Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) { - QByteArray qBAFilename(getFilename().toLocal8Bit()); - TagLib::MPEG::File f(qBAFilename.constData()); + TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; diff --git a/src/soundsourceoggvorbis.cpp b/src/soundsourceoggvorbis.cpp index 4b6ea4904c4..66138e32cb6 100644 --- a/src/soundsourceoggvorbis.cpp +++ b/src/soundsourceoggvorbis.cpp @@ -40,9 +40,8 @@ SoundSourceOggVorbis::~SoundSourceOggVorbis() { } Result SoundSourceOggVorbis::open() { - const QByteArray qBAFilename(getFilename().toLocal8Bit()); - if (0 != ov_fopen(qBAFilename.constData(), &m_vf)) { + if (0 != ov_fopen(getFilename().toLocal8Bit().constData(), &m_vf)) { qWarning() << "Failed to open OggVorbis file:" << getFilename(); return ERR; } @@ -152,8 +151,7 @@ Mixxx::AudioSource::size_type SoundSourceOggVorbis::readFrameSamplesInterleaved( Parse the the file to get metadata */ Result SoundSourceOggVorbis::parseMetadata(Mixxx::TrackMetadata* pMetadata) { - QByteArray qBAFilename = getFilename().toLocal8Bit(); - TagLib::Ogg::Vorbis::File f(qBAFilename.constData()); + TagLib::Ogg::Vorbis::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; diff --git a/src/soundsourceopus.cpp b/src/soundsourceopus.cpp index 896582b3b49..09623d963b7 100644 --- a/src/soundsourceopus.cpp +++ b/src/soundsourceopus.cpp @@ -30,10 +30,9 @@ SoundSourceOpus::~SoundSourceOpus() { } Result SoundSourceOpus::open() { - const QByteArray qBAFilename(getFilename().toLocal8Bit()); int errorCode = 0; - m_pOggOpusFile = op_open_file(qBAFilename.constData(), &errorCode); + m_pOggOpusFile = op_open_file(getFilename().toLocal8Bit().constData(), &errorCode); if (!m_pOggOpusFile) { qDebug() << "Failed to open OggOpus file:" << getFilename() << "errorCode" << errorCode; @@ -141,11 +140,10 @@ namespace Parse the the file to get metadata */ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) { - int error = 0; - - QByteArray qBAFilename = getFilename().toLocal8Bit(); + const QByteArray qbaFilename(getFilename().toLocal8Bit()); - OggOpusFileOwner l_ptrOpusFile(op_open_file(qBAFilename.constData(), &error)); + int error = 0; + OggOpusFileOwner l_ptrOpusFile(op_open_file(qbaFilename.constData(), &error)); pMetadata->setChannels(op_channel_count(l_ptrOpusFile, -1)); pMetadata->setSampleRate(kOpusSampleRate); @@ -154,7 +152,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) { // If we don't have new enough Taglib we use libopusfile parser! #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) - TagLib::Ogg::Opus::File f(qBAFilename.constData()); + TagLib::Ogg::Opus::File f(qbaFilename.constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; diff --git a/src/soundsourcesndfile.cpp b/src/soundsourcesndfile.cpp index 2b9c162a70d..9e8b4282708 100644 --- a/src/soundsourcesndfile.cpp +++ b/src/soundsourcesndfile.cpp @@ -31,8 +31,7 @@ Result SoundSourceSndFile::open() { LPCWSTR lpcwFilename = (LPCWSTR)getFilename().utf16(); m_pSndFile = sf_wchar_open(lpcwFilename, SFM_READ, &m_sfInfo); #else - const QByteArray qbaFilename(getFilename().toLocal8Bit()); - m_pSndFile = sf_open(qbaFilename.constData(), SFM_READ, &m_sfInfo); + m_pSndFile = sf_open(getFilename().toLocal8Bit().constData(), SFM_READ, &m_sfInfo); #endif if (m_pSndFile == NULL) { // sf_format_check is only for writes @@ -91,10 +90,9 @@ Mixxx::AudioSource::size_type SoundSourceSndFile::readFrameSamplesInterleaved( } Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) { - const QByteArray qBAFilename(getFilename().toLocal8Bit()); if (getType() == "flac") { - TagLib::FLAC::File f(qBAFilename.constData()); + TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -116,7 +114,7 @@ Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) { } } } else if (getType() == "wav") { - TagLib::RIFF::WAV::File f(qBAFilename.constData()); + TagLib::RIFF::WAV::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -153,7 +151,7 @@ Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) { } } else if (getType().startsWith("aif")) { // Try AIFF - TagLib::RIFF::AIFF::File f(qBAFilename.constData()); + TagLib::RIFF::AIFF::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -172,10 +170,9 @@ Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) { QImage SoundSourceSndFile::parseCoverArt() { QImage coverArt; - const QByteArray qBAFilename(getFilename().toLocal8Bit()); if (getType() == "flac") { - TagLib::FLAC::File f(qBAFilename.constData()); + TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); @@ -196,14 +193,14 @@ QImage SoundSourceSndFile::parseCoverArt() { } } } else if (getType() == "wav") { - TagLib::RIFF::WAV::File f(qBAFilename.constData()); + TagLib::RIFF::WAV::File f(getFilename().toLocal8Bit().constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); } } else if (getType().startsWith("aif")) { // Try AIFF - TagLib::RIFF::AIFF::File f(qBAFilename.constData()); + TagLib::RIFF::AIFF::File f(getFilename().toLocal8Bit().constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); From 64bdea22f13bf456d300b85c48b79b259fe3e301 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 16 Dec 2014 17:23:01 +0100 Subject: [PATCH 023/481] Fix incomplete close() for SoundSourceSndFile --- src/soundsourcesndfile.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/soundsourcesndfile.cpp b/src/soundsourcesndfile.cpp index 9e8b4282708..c126a0cf124 100644 --- a/src/soundsourcesndfile.cpp +++ b/src/soundsourcesndfile.cpp @@ -56,13 +56,15 @@ Result SoundSourceSndFile::open() { void SoundSourceSndFile::close() { if (m_pSndFile) { - if (0 != sf_close(m_pSndFile)) { + if (0 == sf_close(m_pSndFile)) { + m_pSndFile = NULL; + memset(&m_sfInfo, 0, sizeof(m_sfInfo)); + Super::reset(); + } else { qWarning() << "Failed to close file:" << getFilename() << sf_strerror(m_pSndFile); } - memset(&m_sfInfo, 0, sizeof(m_sfInfo)); } - Super::reset(); } Mixxx::AudioSource::diff_type SoundSourceSndFile::seekFrame( From afb4b83cf004c25020e2df3a9c6227c9d711bfdf Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 18 Dec 2014 00:06:57 +0100 Subject: [PATCH 024/481] Split AudioSource from SoundSource --- build/depends.py | 6 +- build/features.py | 11 +- plugins/soundsourcem4a/SConscript | 5 +- plugins/soundsourcem4a/audiosourcem4a.cpp | 297 +++++++++ plugins/soundsourcem4a/audiosourcem4a.h | 68 +++ plugins/soundsourcem4a/soundsourcem4a.cpp | 341 ++--------- plugins/soundsourcem4a/soundsourcem4a.h | 92 +-- plugins/soundsourcemediafoundation/SConscript | 2 +- .../audiosourcemediafoundation.cpp | 560 +++++++++++++++++ .../audiosourcemediafoundation.h | 63 ++ .../soundsourcemediafoundation.cpp | 543 +---------------- .../soundsourcemediafoundation.h | 46 +- plugins/soundsourcewv/SConscript | 3 +- plugins/soundsourcewv/audiosourcewv.cpp | 86 +++ plugins/soundsourcewv/audiosourcewv.h | 43 ++ plugins/soundsourcewv/soundsourcewv.cpp | 107 ++-- plugins/soundsourcewv/soundsourcewv.h | 61 +- src/analyserqueue.cpp | 14 +- src/audiosource.h | 6 + src/audiosourcecoreaudio.cpp | 159 +++++ src/audiosourcecoreaudio.h | 51 ++ src/audiosourceffmpeg.cpp | 504 ++++++++++++++++ src/audiosourceffmpeg.h | 93 +++ src/audiosourceflac.cpp | 352 +++++++++++ src/audiosourceflac.h | 70 +++ src/audiosourcemodplug.cpp | 157 +++++ src/audiosourcemodplug.h | 56 ++ src/audiosourcemp3.cpp | 370 ++++++++++++ src/audiosourcemp3.h | 82 +++ src/audiosourceoggvorbis.cpp | 134 +++++ src/audiosourceoggvorbis.h | 41 ++ src/audiosourceopus.cpp | 115 ++++ src/audiosourceopus.h | 41 ++ src/audiosourcesndfile.cpp | 93 +++ src/audiosourcesndfile.h | 43 ++ src/cachingreaderworker.cpp | 2 +- src/dlgprefmodplug.cpp | 4 +- src/musicbrainz/chromaprinter.cpp | 2 +- src/soundsource.h | 22 +- src/soundsourcecoreaudio.cpp | 144 +---- src/soundsourcecoreaudio.h | 35 +- src/soundsourceffmpeg.cpp | 562 ++---------------- src/soundsourceffmpeg.h | 78 +-- src/soundsourceflac.cpp | 342 +---------- src/soundsourceflac.h | 63 +- src/soundsourcemodplug.cpp | 179 ++---- src/soundsourcemodplug.h | 47 +- src/soundsourcemp3.cpp | 369 +----------- src/soundsourcemp3.h | 66 +- src/soundsourceoggvorbis.cpp | 126 +--- src/soundsourceoggvorbis.h | 24 +- src/soundsourceopus.cpp | 114 +--- src/soundsourceopus.h | 21 +- src/soundsourceproxy.cpp | 36 +- src/soundsourceproxy.h | 6 +- src/soundsourcesndfile.cpp | 102 +--- src/soundsourcesndfile.h | 25 +- src/test/soundproxy_test.cpp | 34 +- 58 files changed, 3846 insertions(+), 3272 deletions(-) create mode 100644 plugins/soundsourcem4a/audiosourcem4a.cpp create mode 100644 plugins/soundsourcem4a/audiosourcem4a.h create mode 100644 plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp create mode 100644 plugins/soundsourcemediafoundation/audiosourcemediafoundation.h create mode 100644 plugins/soundsourcewv/audiosourcewv.cpp create mode 100644 plugins/soundsourcewv/audiosourcewv.h create mode 100644 src/audiosourcecoreaudio.cpp create mode 100644 src/audiosourcecoreaudio.h create mode 100644 src/audiosourceffmpeg.cpp create mode 100644 src/audiosourceffmpeg.h create mode 100644 src/audiosourceflac.cpp create mode 100644 src/audiosourceflac.h create mode 100644 src/audiosourcemodplug.cpp create mode 100644 src/audiosourcemodplug.h create mode 100644 src/audiosourcemp3.cpp create mode 100644 src/audiosourcemp3.h create mode 100644 src/audiosourceoggvorbis.cpp create mode 100644 src/audiosourceoggvorbis.h create mode 100644 src/audiosourceopus.cpp create mode 100644 src/audiosourceopus.h create mode 100644 src/audiosourcesndfile.cpp create mode 100644 src/audiosourcesndfile.h diff --git a/build/depends.py b/build/depends.py index 5e08b43fd18..f8644619896 100644 --- a/build/depends.py +++ b/build/depends.py @@ -126,7 +126,7 @@ def configure(self, build, conf): 'Did not find libvorbisenc.a, libvorbisenc.lib, or the libvorbisenc development headers.') def sources(self, build): - return ['soundsourceoggvorbis.cpp'] + return ['audiosourceoggvorbis.cpp','soundsourceoggvorbis.cpp'] class SndFile(Dependence): @@ -139,7 +139,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='__SNDFILE__') def sources(self, build): - return ['soundsourcesndfile.cpp'] + return ['audiosourcesndfile.cpp','soundsourcesndfile.cpp'] class FLAC(Dependence): @@ -154,7 +154,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='FLAC__NO_DLL') def sources(self, build): - return ['soundsourceflac.cpp', ] + return ['audiosourceflac.cpp','soundsourceflac.cpp',] class Qt(Dependence): diff --git a/build/features.py b/build/features.py index 4b2c8ddac49..16176e38dcc 100644 --- a/build/features.py +++ b/build/features.py @@ -179,7 +179,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='__MAD__') def sources(self, build): - return ['soundsourcemp3.cpp'] + return ['audiosourcemp3.cpp','soundsourcemp3.cpp'] class CoreAudio(Feature): @@ -214,7 +214,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='__COREAUDIO__') def sources(self, build): - return ['soundsourcecoreaudio.cpp', + return ['audiosourcecoreaudio.cpp','soundsourcecoreaudio.cpp', '#lib/apple/CAStreamBasicDescription.cpp'] @@ -429,7 +429,7 @@ def configure(self, build, conf): def sources(self, build): depends.Qt.uic(build)('dlgprefmodplugdlg.ui') - return ['soundsourcemodplug.cpp', 'dlgprefmodplug.cpp'] + return ['audiosourcemodplug.cpp','soundsourcemodplug.cpp','dlgprefmodplug.cpp'] class FAAD(Feature): @@ -802,7 +802,7 @@ def configure(self, build, conf): build.env.ParseConfig('pkg-config opusfile opus --silence-errors --cflags --libs') def sources(self, build): - return ['soundsourceopus.cpp'] + return ['audiosourceopus.cpp','soundsourceopus.cpp'] class FFMPEG(Feature): @@ -930,7 +930,8 @@ def configure(self, build, conf): 'include', 'ffmpeg')) def sources(self, build): - return ['soundsourceffmpeg.cpp', + return ['audiosourceffmpeg.cpp', + 'soundsourceffmpeg.cpp', 'encoder/encoderffmpegresample.cpp', 'encoder/encoderffmpegcore.cpp', 'encoder/encoderffmpegmp3.cpp', diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index 3c686c797eb..76aa5ce98b3 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -11,9 +11,10 @@ Import('build') # On Posix default SCons.LIBPREFIX = 'lib', on Windows default SCons.LIBPREFIX = '' m4a_sources = [ - "soundsourcem4a.cpp", # MP4/M4A Support through FAAD/libmp4v2 - "soundsource.cpp", # required to subclass SoundSource + "audiosourcem4a.cpp", # MP4/M4A audio support through FAAD/libmp4v2 + "soundsourcem4a.cpp", # MP4/M4A tag support "audiosource.cpp", # required to subclass AudioSource + "soundsource.cpp", # required to subclass SoundSource "trackmetadatataglib.cpp", # TagLib dependencies "trackmetadata.cpp", "sampleutil.cpp", # utility functions diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp new file mode 100644 index 00000000000..3f0ed3d27cd --- /dev/null +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -0,0 +1,297 @@ +#include "audiosourcem4a.h" + +#include "sampleutil.h" + +#ifdef __WINDOWS__ +#include +#include +#endif + +#ifdef _MSC_VER +#define S_ISDIR(mode) (mode & _S_IFDIR) +#define strcasecmp stricmp +#define strncasecmp strnicmp +#endif + +#ifdef __M4AHACK__ +typedef uint32_t SAMPLERATE_TYPE; +#else +typedef unsigned long SAMPLERATE_TYPE; +#endif + +namespace Mixxx { + +namespace +{ + +// AAC: "... each block decodes to 1024 time-domain samples." +const AudioSource::size_type kFramesPerSampleBlock = 1024; + +// MP4SampleId is 1-based +const MP4SampleId kMinSampleId = 1; + +// Decoding will be restarted one or more blocks of samples +// before the actual position to avoid audible glitches. +// One block of samples seems to be enough here. +const MP4SampleId kSampleIdPrefetchCount = 1; + +MP4TrackId findAacTrackId(MP4FileHandle hFile) { + const MP4TrackId maxTrackId = MP4GetNumberOfTracks(hFile, NULL, 0); + for (MP4TrackId trackId = 1; trackId <= maxTrackId; ++trackId) { + const char* trackType = MP4GetTrackType(hFile, trackId); + if ((NULL == trackType) || !MP4_IS_AUDIO_TRACK_TYPE(trackType)) { + continue; + } + const char* mediaDataName = MP4GetTrackMediaDataName(hFile, trackId); + if (0 != strcasecmp(mediaDataName, "mp4a")) { + continue; + } + const u_int8_t audioType = MP4GetTrackEsdsObjectTypeId(hFile, trackId); + if (MP4_INVALID_AUDIO_TYPE == audioType) { + continue; + } + if (MP4_MPEG4_AUDIO_TYPE == audioType) { + const u_int8_t audioMpeg4Type = MP4GetTrackAudioMpeg4Type(hFile, + trackId); + if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(audioMpeg4Type)) { + return trackId; + } + } else if (MP4_IS_AAC_AUDIO_TYPE(audioType)) { + return trackId; + } + } + return MP4_INVALID_TRACK_ID; +} + +} + +AudioSourceM4A::AudioSourceM4A() + : m_hFile(MP4_INVALID_FILE_HANDLE), m_trackId( + MP4_INVALID_TRACK_ID), m_maxSampleId(MP4_INVALID_SAMPLE_ID), m_curSampleId( + MP4_INVALID_SAMPLE_ID), m_inputBufferOffset(0), m_inputBufferLength( + 0), m_hDecoder(NULL), m_curFrameIndex(0) { +} + +AudioSourceM4A::~AudioSourceM4A() { + close(); +} + +AudioSourcePointer AudioSourceM4A::open(QString fileName) { + AudioSourceM4A* pAudioSourceM4A(new AudioSourceM4A); + AudioSourcePointer pAudioSource(pAudioSourceM4A); // take ownership + if (OK == pAudioSourceM4A->postConstruct(fileName)) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceM4A::postConstruct(QString fileName) { + /* open MP4 file, check for >= ver 1.9.1 */ +#if MP4V2_PROJECT_version_hex <= 0x00010901 + m_hFile = MP4Read(fileName.toLocal8Bit().constData(), 0); +#else + m_hFile = MP4Read(fileName.toLocal8Bit().constData()); +#endif + if (MP4_INVALID_FILE_HANDLE == m_hFile) { + qWarning() << "Failed to open file for reading:" << fileName; + return ERR; + } + + m_trackId = findAacTrackId(m_hFile); + if (MP4_INVALID_TRACK_ID == m_trackId) { + qWarning() << "No AAC track found in file:" << fileName; + return ERR; + } + + m_maxSampleId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); + if (MP4_INVALID_SAMPLE_ID == m_maxSampleId) { + qWarning() << "Failed to read file structure:" << fileName; + return ERR; + } + m_curSampleId = MP4_INVALID_SAMPLE_ID; + + m_inputBuffer.resize(MP4GetTrackMaxSampleSize(m_hFile, m_trackId), 0); + m_inputBufferOffset = 0; + m_inputBufferLength = 0; + + if (!m_hDecoder) { + // lazy initialization of the AAC decoder + m_hDecoder = faacDecOpen(); + if (!m_hDecoder) { + qWarning() << "Failed to open the AAC decoder!"; + return ERR; + } + faacDecConfigurationPtr pDecoderConfig = faacDecGetCurrentConfiguration( + m_hDecoder); + pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ + pDecoderConfig->downMatrix = 1; /* 5.1 -> stereo */ + pDecoderConfig->defObjectType = LC; + if (!faacDecSetConfiguration(m_hDecoder, pDecoderConfig)) { + qWarning() << "Failed to configure AAC decoder!"; + return ERR; + } + } + + u_int8_t* configBuffer = NULL; + u_int32_t configBufferSize = 0; + if (!MP4GetTrackESConfiguration(m_hFile, m_trackId, &configBuffer, + &configBufferSize)) { + /* failed to get mpeg-4 audio config... this is ok. + * faacDecInit2() will simply use default values instead. + */ + qWarning() + << "Failed to read the MP4 audio configuration. Continuing with default values."; + } + + SAMPLERATE_TYPE sampleRate; + unsigned char channelCount; + if (0 > faacDecInit2(m_hDecoder, configBuffer, configBufferSize, + &sampleRate, &channelCount)) { + free(configBuffer); + qWarning() << "Failed to initialize the AAC decoder!"; + return ERR; + } else { + free(configBuffer); + } + + setChannelCount(channelCount); + setFrameRate(sampleRate); + setFrameCount(m_maxSampleId * kFramesPerSampleBlock); + + const SampleBuffer::size_type prefetchSampleBufferSize = (kSampleIdPrefetchCount + 1) * frames2samples(kFramesPerSampleBlock); + SampleBuffer(prefetchSampleBufferSize).swap(m_prefetchSampleBuffer); + + // invalidate current frame index + m_curFrameIndex = getFrameCount(); + // seek to beginning of file + if (0 != seekFrame(0)) { + qWarning() << "Failed to seek to the beginning of the file!"; + return ERR; + } + + return OK; +} + +void AudioSourceM4A::close() throw() { + if (m_hDecoder) { + NeAACDecClose(m_hDecoder); + m_hDecoder = NULL; + } + if (MP4_INVALID_FILE_HANDLE != m_hFile) { + MP4Close(m_hFile); + m_hFile = MP4_INVALID_FILE_HANDLE; + } + m_trackId = MP4_INVALID_TRACK_ID; + m_maxSampleId = MP4_INVALID_SAMPLE_ID; + m_curSampleId = MP4_INVALID_SAMPLE_ID; + m_inputBuffer.clear(); + m_inputBufferOffset = 0; + m_inputBufferLength = 0; + m_curFrameIndex = 0; + Super::reset(); +} + +bool AudioSourceM4A::isValidSampleId(MP4SampleId sampleId) const { + return (sampleId >= kMinSampleId) && (sampleId <= m_maxSampleId); +} + +AudioSource::diff_type AudioSourceM4A::seekFrame(diff_type frameIndex) { + if (m_curFrameIndex != frameIndex) { + const MP4SampleId sampleId = kMinSampleId + + (frameIndex / kFramesPerSampleBlock); + if (!isValidSampleId(sampleId)) { + return m_curFrameIndex; + } + if ((m_curSampleId != sampleId) || (frameIndex < m_curFrameIndex)) { + // restart decoding one or more blocks of samples + // backwards to avoid audible glitches + m_curSampleId = math_max(sampleId - kSampleIdPrefetchCount, kMinSampleId); + m_curFrameIndex = (m_curSampleId - kMinSampleId) * kFramesPerSampleBlock; + // rryan 9/2009 -- the documentation is sketchy on this, but I think that + // it tells the decoder that you are seeking so it should flush its state + faacDecPostSeekReset(m_hDecoder, m_curSampleId); + // discard input buffer + m_inputBufferOffset = 0; + m_inputBufferLength = 0; + } + // decoding starts before the actual target position + Q_ASSERT(m_curFrameIndex <= frameIndex); + const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; + // prefetch (decode and discard) all samples up to the target position + Q_ASSERT(frames2samples(prefetchFrameCount) <= m_prefetchSampleBuffer.size()); + readFrameSamplesInterleaved(prefetchFrameCount, &m_prefetchSampleBuffer[0]); + } + Q_ASSERT(m_curFrameIndex == frameIndex); + return frameIndex; +} + +AudioSource::size_type AudioSourceM4A::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + sample_type* pSampleBuffer = sampleBuffer; + const diff_type readFrameIndex = m_curFrameIndex; + size_type readFrameCount = m_curFrameIndex - readFrameIndex; + while ((readFrameCount = m_curFrameIndex - readFrameIndex) < frameCount) { + if (isValidSampleId(m_curSampleId) && (0 == m_inputBufferLength)) { + // fill input buffer with block of samples + InputBuffer::value_type* pInputBuffer = &m_inputBuffer[0]; + m_inputBufferOffset = 0; + m_inputBufferLength = m_inputBuffer.size(); // in/out parameter + if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleId++, + &pInputBuffer, &m_inputBufferLength, + NULL, NULL, NULL, NULL)) { + qWarning() << "Failed to read MP4 sample data" << m_curSampleId; + break; // abort + } + } + if (0 == m_inputBufferLength) { + // no more input data available (EOF) + break;// done + } + faacDecFrameInfo decFrameInfo; + decFrameInfo.bytesconsumed = 0; + decFrameInfo.samples = 0; + // decode samples into sampleBuffer + const size_type readFrameCount = m_curFrameIndex - readFrameIndex; + const size_type decodeBufferCapacityInBytes = frames2samples( + frameCount - readFrameCount) * sizeof(*sampleBuffer); + Q_ASSERT(0 < decodeBufferCapacityInBytes); + void* pDecodeBuffer = pSampleBuffer; + NeAACDecDecode2(m_hDecoder, &decFrameInfo, + &m_inputBuffer[m_inputBufferOffset], + m_inputBufferLength / sizeof(m_inputBuffer[0]), + &pDecodeBuffer, decodeBufferCapacityInBytes); + // samples should have been decoded into our own buffer + Q_ASSERT(pSampleBuffer == pDecodeBuffer); + pSampleBuffer += decFrameInfo.samples; + // only the input data that is available should have been read + Q_ASSERT( + decFrameInfo.bytesconsumed + <= (m_inputBufferLength / sizeof(m_inputBuffer[0]))); + // consume input data + m_inputBufferOffset += decFrameInfo.bytesconsumed + * sizeof(m_inputBuffer[0]); + m_inputBufferLength -= decFrameInfo.bytesconsumed + * sizeof(m_inputBuffer[0]); + m_curFrameIndex += samples2frames(decFrameInfo.samples); + if (0 != decFrameInfo.error) { + qWarning() << "AAC decoding error:" + << faacDecGetErrorMessage(decFrameInfo.error); + break; // abort + } + if (getChannelCount() != decFrameInfo.channels) { + qWarning() << "Unexpected number of channels:" + << decFrameInfo.channels; + break; // abort + } + if (getFrameRate() != decFrameInfo.samplerate) { + qWarning() << "Unexpected sample rate:" << decFrameInfo.samplerate; + break; // abort + } + } + return readFrameCount; +} + +} // namespace Mixxx diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h new file mode 100644 index 00000000000..c2d7a9fd9d6 --- /dev/null +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -0,0 +1,68 @@ +#ifndef AUDIOSOURCEM4A_H +#define AUDIOSOURCEM4A_H + +#include "audiosource.h" +#include "util/defs.h" + +#ifdef __MP4V2__ + #include +#else + #include +#endif + +#include + +#include + +//As per QLibrary docs: http://doc.trolltech.com/4.6/qlibrary.html#resolve +#ifdef Q_OS_WIN +#define MY_EXPORT __declspec(dllexport) +#else +#define MY_EXPORT +#endif + + +namespace Mixxx { + +class AudioSourceM4A : public AudioSource { + typedef AudioSource Super; + +public: + static AudioSourcePointer open(QString fileName); + + ~AudioSourceM4A(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /* override*/; + +private: + AudioSourceM4A(); + + Result postConstruct(QString fileName); + + bool isValidSampleId(MP4SampleId sampleId) const; + + MP4FileHandle m_hFile; + MP4TrackId m_trackId; + MP4SampleId m_maxSampleId; + MP4SampleId m_curSampleId; + + typedef std::vector InputBuffer; + InputBuffer m_inputBuffer; + InputBuffer::size_type m_inputBufferOffset; + u_int32_t m_inputBufferLength; + + faacDecHandle m_hDecoder; + + typedef std::vector SampleBuffer; + SampleBuffer m_prefetchSampleBuffer; + + diff_type m_curFrameIndex; +}; + +} + +#endif diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 45125aa1cfb..0cbd057350e 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -16,303 +16,25 @@ #include "soundsourcem4a.h" +#include "audiosourcem4a.h" #include "trackmetadatataglib.h" -#include "sampleutil.h" #include -#ifdef __WINDOWS__ -#include -#include -#endif - -#ifdef _MSC_VER -#define S_ISDIR(mode) (mode & _S_IFDIR) -#define strcasecmp stricmp -#define strncasecmp strnicmp -#endif - -#ifdef __M4AHACK__ -typedef uint32_t SAMPLERATE_TYPE; -#else -typedef unsigned long SAMPLERATE_TYPE; -#endif - namespace Mixxx { -namespace { -// AAC: "... each block decodes to 1024 time-domain samples." -const AudioSource::size_type kFramesPerSampleBlock = 1024; - -// MP4SampleId is 1-based -const MP4SampleId kMinSampleId = 1; - -// Decoding will be restarted one or more blocks of samples -// before the actual position to avoid audible glitches. -// One block of samples seems to be enough here. -const MP4SampleId kSampleIdPrefetchCount = 1; -} - -SoundSourceM4A::SoundSourceM4A(QString qFileName) - : Super(qFileName, "m4a"), m_hFile(MP4_INVALID_FILE_HANDLE), m_trackId( - MP4_INVALID_TRACK_ID), m_maxSampleId(MP4_INVALID_SAMPLE_ID), m_curSampleId( - MP4_INVALID_SAMPLE_ID), m_inputBufferOffset(0), m_inputBufferLength( - 0), m_hDecoder(NULL), m_curFrameIndex(0) { -} - -SoundSourceM4A::~SoundSourceM4A() { - close(); -} - -namespace { -MP4TrackId findAacTrackId(MP4FileHandle hFile) { - const MP4TrackId maxTrackId = MP4GetNumberOfTracks(hFile, NULL, 0); - for (MP4TrackId trackId = 1; trackId <= maxTrackId; ++trackId) { - const char* trackType = MP4GetTrackType(hFile, trackId); - if ((NULL == trackType) || !MP4_IS_AUDIO_TRACK_TYPE(trackType)) { - continue; - } - const char* mediaDataName = MP4GetTrackMediaDataName(hFile, trackId); - if (0 != strcasecmp(mediaDataName, "mp4a")) { - continue; - } - const u_int8_t audioType = MP4GetTrackEsdsObjectTypeId(hFile, trackId); - if (MP4_INVALID_AUDIO_TYPE == audioType) { - continue; - } - if (MP4_MPEG4_AUDIO_TYPE == audioType) { - const u_int8_t audioMpeg4Type = MP4GetTrackAudioMpeg4Type(hFile, - trackId); - if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(audioMpeg4Type)) { - return trackId; - } - } else if (MP4_IS_AAC_AUDIO_TYPE(audioType)) { - return trackId; - } - } - return MP4_INVALID_TRACK_ID; -} -} - -Result SoundSourceM4A::open() { - if (MP4_INVALID_FILE_HANDLE != m_hFile) { - qWarning() << "File has already been opened!"; - return ERR; - } - - /* open MP4 file, check for >= ver 1.9.1 */ -#if MP4V2_PROJECT_version_hex <= 0x00010901 - m_hFile = MP4Read(getFilename().toLocal8Bit().constData(), 0); -#else - m_hFile = MP4Read(getFilename().toLocal8Bit().constData()); -#endif - if (MP4_INVALID_FILE_HANDLE == m_hFile) { - close(); - qWarning() << "Failed to open file for reading:" << getFilename(); - return ERR; - } - - m_trackId = findAacTrackId(m_hFile); - if (MP4_INVALID_TRACK_ID == m_trackId) { - close(); - qWarning() << "No AAC track found in file:" << getFilename(); - return ERR; - } - - m_maxSampleId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); - if (MP4_INVALID_SAMPLE_ID == m_maxSampleId) { - close(); - qWarning() << "Failed to read file structure:" << getFilename(); - return ERR; - } - m_curSampleId = MP4_INVALID_SAMPLE_ID; - - m_inputBuffer.resize(MP4GetTrackMaxSampleSize(m_hFile, m_trackId), 0); - m_inputBufferOffset = 0; - m_inputBufferLength = 0; - - if (!m_hDecoder) { - // lazy initialization of the AAC decoder - m_hDecoder = faacDecOpen(); - if (!m_hDecoder) { - qWarning() << "Failed to open the AAC decoder!"; - close(); - return ERR; - } - faacDecConfigurationPtr pDecoderConfig = faacDecGetCurrentConfiguration( - m_hDecoder); - pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ - pDecoderConfig->downMatrix = 1; /* 5.1 -> stereo */ - pDecoderConfig->defObjectType = LC; - if (!faacDecSetConfiguration(m_hDecoder, pDecoderConfig)) { - close(); - qWarning() << "Failed to configure AAC decoder!"; - return ERR; - } - } - - u_int8_t* configBuffer = NULL; - u_int32_t configBufferSize = 0; - if (!MP4GetTrackESConfiguration(m_hFile, m_trackId, &configBuffer, - &configBufferSize)) { - /* failed to get mpeg-4 audio config... this is ok. - * faacDecInit2() will simply use default values instead. - */ - qWarning() - << "Failed to read the MP4 audio configuration. Continuing with default values."; - } - - SAMPLERATE_TYPE sampleRate; - unsigned char channelCount; - if (0 > faacDecInit2(m_hDecoder, configBuffer, configBufferSize, - &sampleRate, &channelCount)) { - free(configBuffer); - close(); - qWarning() << "Failed to initialize the AAC decoder!"; - return ERR; - } else { - free(configBuffer); - } - - setChannelCount(channelCount); - setFrameRate(sampleRate); - setFrameCount(m_maxSampleId * kFramesPerSampleBlock); - - const SampleBuffer::size_type prefetchSampleBufferSize = (kSampleIdPrefetchCount + 1) * frames2samples(kFramesPerSampleBlock); - SampleBuffer(prefetchSampleBufferSize).swap(m_prefetchSampleBuffer); - - // invalidate current frame index - m_curFrameIndex = getFrameCount(); - // seek to beginning of file - if (0 != seekFrame(0)) { - close(); - qWarning() << "Failed to seek to the beginning of the file!"; - return ERR; - } - - return OK; -} - -void SoundSourceM4A::close() { - if (m_hDecoder) { - NeAACDecClose(m_hDecoder); - m_hDecoder = NULL; - } - if (MP4_INVALID_FILE_HANDLE != m_hFile) { - MP4Close(m_hFile); - m_hFile = MP4_INVALID_FILE_HANDLE; - } - m_trackId = MP4_INVALID_TRACK_ID; - m_maxSampleId = MP4_INVALID_SAMPLE_ID; - m_curSampleId = MP4_INVALID_SAMPLE_ID; - m_inputBuffer.clear(); - m_inputBufferOffset = 0; - m_inputBufferLength = 0; - m_curFrameIndex = 0; - Super::reset(); -} - -bool SoundSourceM4A::isValidSampleId(MP4SampleId sampleId) const { - return (sampleId >= kMinSampleId) && (sampleId <= m_maxSampleId); +QList SoundSourceM4A::supportedFileExtensions() { + QList list; + list.push_back("m4a"); + list.push_back("mp4"); + return list; } -AudioSource::diff_type SoundSourceM4A::seekFrame(diff_type frameIndex) { - if (m_curFrameIndex != frameIndex) { - const MP4SampleId sampleId = kMinSampleId - + (frameIndex / kFramesPerSampleBlock); - if (!isValidSampleId(sampleId)) { - return m_curFrameIndex; - } - if ((m_curSampleId != sampleId) || (frameIndex < m_curFrameIndex)) { - // restart decoding one or more blocks of samples - // backwards to avoid audible glitches - m_curSampleId = math_max(sampleId - kSampleIdPrefetchCount, kMinSampleId); - m_curFrameIndex = (m_curSampleId - kMinSampleId) * kFramesPerSampleBlock; - // rryan 9/2009 -- the documentation is sketchy on this, but I think that - // it tells the decoder that you are seeking so it should flush its state - faacDecPostSeekReset(m_hDecoder, m_curSampleId); - // discard input buffer - m_inputBufferOffset = 0; - m_inputBufferLength = 0; - } - // decoding starts before the actual target position - Q_ASSERT(m_curFrameIndex <= frameIndex); - const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; - // prefetch (decode and discard) all samples up to the target position - Q_ASSERT(frames2samples(prefetchFrameCount) <= m_prefetchSampleBuffer.size()); - readFrameSamplesInterleaved(prefetchFrameCount, &m_prefetchSampleBuffer[0]); - } - Q_ASSERT(m_curFrameIndex == frameIndex); - return frameIndex; +SoundSourceM4A::SoundSourceM4A(QString fileName) + : Super(fileName, "m4a") { } -AudioSource::size_type SoundSourceM4A::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - sample_type* pSampleBuffer = sampleBuffer; - const diff_type readFrameIndex = m_curFrameIndex; - size_type readFrameCount = m_curFrameIndex - readFrameIndex; - while ((readFrameCount = m_curFrameIndex - readFrameIndex) < frameCount) { - if (isValidSampleId(m_curSampleId) && (0 == m_inputBufferLength)) { - // fill input buffer with block of samples - InputBuffer::value_type* pInputBuffer = &m_inputBuffer[0]; - m_inputBufferOffset = 0; - m_inputBufferLength = m_inputBuffer.size(); // in/out parameter - if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleId++, - &pInputBuffer, &m_inputBufferLength, - NULL, NULL, NULL, NULL)) { - qWarning() << "Failed to read MP4 sample data" << m_curSampleId; - break; // abort - } - } - if (0 == m_inputBufferLength) { - // no more input data available (EOF) - break;// done - } - faacDecFrameInfo decFrameInfo; - decFrameInfo.bytesconsumed = 0; - decFrameInfo.samples = 0; - // decode samples into sampleBuffer - const size_type readFrameCount = m_curFrameIndex - readFrameIndex; - const size_type decodeBufferCapacityInBytes = frames2samples( - frameCount - readFrameCount) * sizeof(*sampleBuffer); - Q_ASSERT(0 < decodeBufferCapacityInBytes); - void* pDecodeBuffer = pSampleBuffer; - NeAACDecDecode2(m_hDecoder, &decFrameInfo, - &m_inputBuffer[m_inputBufferOffset], - m_inputBufferLength / sizeof(m_inputBuffer[0]), - &pDecodeBuffer, decodeBufferCapacityInBytes); - // samples should have been decoded into our own buffer - Q_ASSERT(pSampleBuffer == pDecodeBuffer); - pSampleBuffer += decFrameInfo.samples; - // only the input data that is available should have been read - Q_ASSERT( - decFrameInfo.bytesconsumed - <= (m_inputBufferLength / sizeof(m_inputBuffer[0]))); - // consume input data - m_inputBufferOffset += decFrameInfo.bytesconsumed - * sizeof(m_inputBuffer[0]); - m_inputBufferLength -= decFrameInfo.bytesconsumed - * sizeof(m_inputBuffer[0]); - m_curFrameIndex += samples2frames(decFrameInfo.samples); - if (0 != decFrameInfo.error) { - qWarning() << "AAC decoding error:" - << faacDecGetErrorMessage(decFrameInfo.error); - break; // abort - } - if (getChannelCount() != decFrameInfo.channels) { - qWarning() << "Unexpected number of channels:" - << decFrameInfo.channels; - break; // abort - } - if (getFrameRate() != decFrameInfo.samplerate) { - qWarning() << "Unexpected sample rate:" << decFrameInfo.samplerate; - break; // abort - } - } - return readFrameCount; -} - -Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) { +Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { @@ -335,7 +57,7 @@ Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -QImage SoundSourceM4A::parseCoverArt() { +QImage SoundSourceM4A::parseCoverArt() const { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { @@ -345,11 +67,44 @@ QImage SoundSourceM4A::parseCoverArt() { } } -QList SoundSourceM4A::supportedFileExtensions() { - QList list; - list.push_back("m4a"); - list.push_back("mp4"); - return list; +Mixxx::AudioSourcePointer SoundSourceM4A::open() const { + return Mixxx::AudioSourceM4A::open(getFilename()); +} + +} // namespace Mixxx + +extern "C" MY_EXPORT const char* getMixxxVersion() { + return VERSION; +} + +extern "C" MY_EXPORT int getSoundSourceAPIVersion() { + return MIXXX_SOUNDSOURCE_API_VERSION; } +extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { + return new Mixxx::SoundSourceM4A(fileName); +} + +extern "C" MY_EXPORT char** supportedFileExtensions() { + QList exts = Mixxx::SoundSourceM4A::supportedFileExtensions(); + //Convert to C string array. + char** c_exts = (char**)malloc((exts.count() + 1) * sizeof(char*)); + for (int i = 0; i < exts.count(); i++) + { + QByteArray qba = exts[i].toUtf8(); + c_exts[i] = strdup(qba.constData()); + qDebug() << c_exts[i]; + } + c_exts[exts.count()] = NULL; //NULL terminate the list + + return c_exts; +} + +extern "C" MY_EXPORT void freeFileExtensions(char **exts) { + if (exts) { + for (int i(0); exts[i]; ++i) { + free(exts[i]); + } + free(exts); + } } diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 2565144cc70..713801ea77b 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -20,16 +20,6 @@ #include "soundsource.h" #include "defs_version.h" -#ifdef __MP4V2__ - #include -#else - #include -#endif - -#include - -#include - //As per QLibrary docs: http://doc.trolltech.com/4.6/qlibrary.html#resolve #ifdef Q_OS_WIN #define MY_EXPORT __declspec(dllexport) @@ -43,81 +33,23 @@ namespace Mixxx { class SoundSourceM4A : public SoundSource { typedef SoundSource Super; - public: - static QList supportedFileExtensions(); - - explicit SoundSourceM4A(QString qFileName); - ~SoundSourceM4A(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - - QImage parseCoverArt() /*override*/; - - Result open() /*override*/; - - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - - private: - void close(); +public: + static QList supportedFileExtensions(); - bool isValidSampleId(MP4SampleId sampleId) const; + explicit SoundSourceM4A(QString fileName); - MP4FileHandle m_hFile; - MP4TrackId m_trackId; - MP4SampleId m_maxSampleId; - MP4SampleId m_curSampleId; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; - typedef std::vector InputBuffer; - InputBuffer m_inputBuffer; - InputBuffer::size_type m_inputBufferOffset; - u_int32_t m_inputBufferLength; - - faacDecHandle m_hDecoder; - - typedef std::vector SampleBuffer; - SampleBuffer m_prefetchSampleBuffer; - - diff_type m_curFrameIndex; + Mixxx::AudioSourcePointer open() const /*override*/; }; -extern "C" MY_EXPORT const char* getMixxxVersion() -{ - return VERSION; -} - -extern "C" MY_EXPORT int getSoundSourceAPIVersion() -{ - return MIXXX_SOUNDSOURCE_API_VERSION; -} - -extern "C" MY_EXPORT SoundSource* getSoundSource(QString filename) -{ - return new SoundSourceM4A(filename); -} - -extern "C" MY_EXPORT char** supportedFileExtensions() -{ - QList exts = SoundSourceM4A::supportedFileExtensions(); - //Convert to C string array. - char** c_exts = (char**)malloc((exts.count() + 1) * sizeof(char*)); - for (int i = 0; i < exts.count(); i++) - { - QByteArray qba = exts[i].toUtf8(); - c_exts[i] = strdup(qba.constData()); - qDebug() << c_exts[i]; - } - c_exts[exts.count()] = NULL; //NULL terminate the list - - return c_exts; -} - -extern "C" MY_EXPORT void freeFileExtensions(char **exts) -{ - for (int i(0); exts[i]; ++i) free(exts[i]); - free(exts); -} - } // namespace Mixxx +extern "C" MY_EXPORT const char* getMixxxVersion(); +extern "C" MY_EXPORT int getSoundSourceAPIVersion(); +extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName); +extern "C" MY_EXPORT char** supportedFileExtensions(); +extern "C" MY_EXPORT void freeFileExtensions(char **exts); + #endif diff --git a/plugins/soundsourcemediafoundation/SConscript b/plugins/soundsourcemediafoundation/SConscript index f915cb399ab..d8152dc809e 100644 --- a/plugins/soundsourcemediafoundation/SConscript +++ b/plugins/soundsourcemediafoundation/SConscript @@ -18,7 +18,7 @@ if int(build.flags['mediafoundation']): else: env["LINKFLAGS"].remove("/subsystem:windows,5.01") ssmediafoundation_bin = env.SharedLibrary('soundsourcemediafoundation', - ['soundsourcemediafoundation.cpp', 'soundsource.cpp', 'audiosource.cpp', 'trackmetadatataglib.cpp', 'trackmetadata.cpp', 'sampleutil.cpp'], + ['soundsourcemediafoundation.cpp', 'audiosourcemediafoundation.cpp', 'soundsource.cpp', 'audiosource.cpp', 'trackmetadatataglib.cpp', 'trackmetadata.cpp', 'sampleutil.cpp'], LINKCOM = [env['LINKCOM'], 'mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;1']) Return("ssmediafoundation_bin") diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp new file mode 100644 index 00000000000..1344a924bf4 --- /dev/null +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -0,0 +1,560 @@ +#include "audiosourcemediafoundation.h" + +#include +#include +#include +#include +#include + +#include + +namespace Mixxx { + +namespace +{ + +const bool sDebug = false; + +const int kNumChannels = 2; +const int kSampleRate = 44100; +const int kLeftoverSize = 4096; // in sample_type's, this seems to be the size MF AAC +const int kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give + +/** + * Convert a 100ns Media Foundation value to a number of seconds. + */ +inline qreal secondsFromMF(qint64 mf) { + return static_cast(mf) / 1e7; +} + +/** + * Convert a number of seconds to a 100ns Media Foundation value. + */ +inline qint64 mfFromSeconds(qreal sec) { + return sec * 1e7; +} + +/** + * Convert a 100ns Media Foundation value to a frame offset. + */ +inline qint64 frameFromMF(qint64 mf) { + return static_cast(mf) * kSampleRate / 1e7; +} + +/** + * Convert a frame offset to a 100ns Media Foundation value. + */ +inline qint64 mfFromFrame(qint64 frame) { + return static_cast(frame) / kSampleRate * 1e7; +} + +/** Microsoft examples use this snippet often. */ +template static void safeRelease(T **ppT) { + if (*ppT) { + (*ppT)->Release(); + *ppT = NULL; + } +} + +} + +AudioSourceMediaFoundation::AudioSourceMediaFoundation(QString fileName) + : Super(fileName, "m4a"), m_hrCoInitialize(E_FAIL), m_hrMFStartup(E_FAIL), + m_pReader(NULL), m_pAudioType(NULL), m_wcFilename( + NULL), m_nextFrame(0), m_leftoverBuffer(NULL), m_leftoverBufferSize( + 0), m_leftoverBufferLength(0), m_leftoverBufferPosition(0), m_mfDuration( + 0), m_iCurrentPosition(0), m_dead(false), m_seeking(false) { + + // these are always the same, might as well just stick them here + // -bkgood + // AudioSource properties + setChannelCount(kNumChannels); + setFrameRate(kSampleRate); + + // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for + // presentation descriptors, one of which MFSourceReader is not. + // Therefore, we calculate it ourselves, assuming 16 bits per sample + setBitrate(frames2samples(kBitsPerSampleForBitrate * kSampleRate) / 1000); +} + +AudioSourceMediaFoundation::~AudioSourceMediaFoundation() { + close(); +} + +AudioSourcePointer AudioSourceMediaFoundation::open(QString fileName) { + AudioSourceMediaFoundation* pAudioSourceMediaFoundation(new AudioSourceMediaFoundation); + AudioSourcePointer pAudioSource(pAudioSourceMediaFoundation); // take ownership + if (OK == pAudioSourceMediaFoundation->postConstruct(fileName)) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceMediaFoundation::postConstruct(QString fileName) { + if (sDebug) { + qDebug() << "open()" << fileName; + } + + // Initialize the COM library. + m_hrCoInitialize = CoInitializeEx(NULL, + COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (FAILED(m_hrCoInitialize)) { + qWarning() << "SSMF: failed to initialize COM"; + return ERR; + } + // Initialize the Media Foundation platform. + m_hrMFStartup = MFStartup(MF_VERSION); + if (FAILED(m_hrCoInitialize)) { + qWarning() << "SSMF: failed to initialize Media Foundation"; + return ERR; + } + + QString qurlStr(fileName); + // http://social.msdn.microsoft.com/Forums/en/netfxbcl/thread/35c6a451-3507-40c8-9d1c-8d4edde7c0cc + // gives maximum path + file length as 248 + 260, using that -bkgood + m_wcFilename = new wchar_t[248 + 260]; + int wcFilenameLength(fileName.toWCharArray(m_wcFilename)); + // toWCharArray does not append a null terminator to the string! + m_wcFilename[wcFilenameLength] = '\0'; + + // Create the source reader to read the input file. + hr = MFCreateSourceReaderFromURL(m_wcFilename, NULL, &m_pReader); + if (FAILED(hr)) { + qWarning() << "SSMF: Error opening input file:" << fileName; + return ERR; + } + + if (!configureAudioStream()) { + qWarning() << "SSMF: Error configuring audio stream."; + return ERR; + } + + //Seek to position 0, which forces us to skip over all the header frames. + //This makes sure we're ready to just let the Analyser rip and it'll + //get the number of samples it expects (ie. no header frames). + seekFrame(0); + + return OK; +} + +void AudioSourceMediaFoundation::close() throw() { + delete[] m_wcFilename; + m_wcFilename = NULL; + delete[] m_leftoverBuffer; + m_leftoverBuffer = NULL; + + safeRelease(&m_pReader); + safeRelease(&m_pAudioType); + + if (SUCCEEDED(m_hrMFStartup)) { + MFShutdown(); + m_hrMFStartup = E_FAIL; + } + if (SUCCEEDED(m_hrCoInitialize)) { + CoUninitialize(); + m_hrCoInitialize = E_FAIL; + } + + Super::reset(); +} + +Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekFrame( + diff_type frameIndex) { + if (sDebug) { + qDebug() << "seekFrame()" << frameIndex; + } + qint64 mfSeekTarget(mfFromFrame(frameIndex) - 1); + // minus 1 here seems to make our seeking work properly, otherwise we will + // (more often than not, maybe always) seek a bit too far (although not + // enough for our calculatedFrameFromMF <= nextFrame assertion in ::read). + // Has something to do with 100ns MF units being much smaller than most + // frame offsets (in seconds) -bkgood + long result = m_iCurrentPosition; + if (m_dead) { + return result; + } + + PROPVARIANT prop; + HRESULT hr; + + // this doesn't fail, see MS's implementation + hr = InitPropVariantFromInt64(mfSeekTarget < 0 ? 0 : mfSeekTarget, &prop); + + hr = m_pReader->Flush(MF_SOURCE_READER_FIRST_AUDIO_STREAM); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to flush before seek"; + } + + // http://msdn.microsoft.com/en-us/library/dd374668(v=VS.85).aspx + hr = m_pReader->SetCurrentPosition(GUID_NULL, prop); + if (FAILED(hr)) { + // nothing we can do here as we can't fail (no facility to other than + // crashing mixxx) + qWarning() << "SSMF: failed to seek" + << (hr == MF_E_INVALIDREQUEST ? + "Sample requests still pending" : ""); + } else { + result = frameIndex; + } + PropVariantClear(&prop); + + // record the next frame so that we can make sure we're there the next + // time we get a buffer from MFSourceReader + m_nextFrame = frameIndex; + m_seeking = true; + m_iCurrentPosition = result; + return result; +} + +Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + if (sDebug) { + qDebug() << "read()" << frameCount; + } + size_type framesNeeded(frameCount); + + // first, copy frames from leftover buffer IF the leftover buffer is at + // the correct frame + if (m_leftoverBufferLength > 0 && m_leftoverBufferPosition == m_nextFrame) { + copyFrames(sampleBuffer, &framesNeeded, m_leftoverBuffer, + m_leftoverBufferLength); + if (m_leftoverBufferLength > 0) { + if (framesNeeded != 0) { + qWarning() << __FILE__ << __LINE__ + << "WARNING: Expected frames needed to be 0. Abandoning this file."; + m_dead = true; + } + m_leftoverBufferPosition += frameCount; + } + } else { + // leftoverBuffer already empty or in the wrong position, clear it + m_leftoverBufferLength = 0; + } + + while (!m_dead && framesNeeded > 0) { + HRESULT hr(S_OK); + DWORD dwFlags(0); + qint64 timestamp(0); + IMFSample *pSample(NULL); + bool error(false); // set to true to break after releasing + + hr = m_pReader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, // [in] DWORD dwStreamIndex, + 0, // [in] DWORD dwControlFlags, + NULL, // [out] DWORD *pdwActualStreamIndex, + &dwFlags, // [out] DWORD *pdwStreamFlags, + ×tamp, // [out] LONGLONG *pllTimestamp, + &pSample); // [out] IMFSample **ppSample + if (FAILED(hr)) { + if (sDebug) { + qDebug() << "ReadSample failed."; + } + break; + } + + if (sDebug) { + qDebug() << "ReadSample timestamp:" << timestamp << "frame:" + << frameFromMF(timestamp) << "dwflags:" << dwFlags; + } + + if (dwFlags & MF_SOURCE_READERF_ERROR) { + // our source reader is now dead, according to the docs + qWarning() + << "SSMF: ReadSample set ERROR, SourceReader is now dead"; + m_dead = true; + break; + } else if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { + qDebug() << "SSMF: End of input file."; + break; + } else if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) { + qWarning() << "SSMF: Type change"; + break; + } else if (pSample == NULL) { + // generally this will happen when dwFlags contains ENDOFSTREAM, + // so it'll be caught before now -bkgood + qWarning() << "SSMF: No sample"; + continue; + } // we now own a ref to the instance at pSample + + IMFMediaBuffer *pMBuffer(NULL); + // I know this does at least a memcopy and maybe a malloc, if we have + // xrun issues with this we might want to look into using + // IMFSample::GetBufferByIndex (although MS doesn't recommend this) + if (FAILED(hr = pSample->ConvertToContiguousBuffer(&pMBuffer))) { + error = true; + goto releaseSample; + } + sample_type *buffer(NULL); + DWORD bufferLengthInBytes(0); + hr = pMBuffer->Lock(reinterpret_cast(&buffer), NULL, &bufferLengthInBytes); + if (FAILED(hr)) { + error = true; + goto releaseMBuffer; + } + size_type bufferLength = samples2frames(bufferLengthInBytes / sizeof(buffer[0])); + + if (m_seeking) { + qint64 bufferPosition(frameFromMF(timestamp)); + if (sDebug) { + qDebug() << "While seeking to " << m_nextFrame + << "WMF put us at" << bufferPosition; + + } + if (m_nextFrame < bufferPosition) { + // Uh oh. We are farther forward than our seek target. Emit + // silence? We can't seek backwards here. + sample_type* pBufferCurpos = sampleBuffer + + frames2samples(frameCount - framesNeeded); + qint64 offshootFrames = bufferPosition - m_nextFrame; + + // If we can correct this immediately, write zeros and adjust + // m_nextFrame to pretend it never happened. + + if (offshootFrames <= framesNeeded) { + qWarning() << __FILE__ << __LINE__ + << "Working around inaccurate seeking. Writing silence for" + << offshootFrames << "frames"; + // Set offshootFrames * kNumChannels samples to zero. + memset(pBufferCurpos, 0, + frames2samples(sizeof(*pBufferCurpos) * offshootFrames)); + // Now m_nextFrame == bufferPosition + m_nextFrame += offshootFrames; + framesNeeded -= offshootFrames; + } else { + // It's more complicated. The buffer we have just decoded is + // more than framesNeeded frames away from us. It's too hard + // for us to handle this correctly currently, so let's just + // try to get on with our lives. + m_seeking = false; + m_nextFrame = bufferPosition; + qWarning() << __FILE__ << __LINE__ + << "Seek offshoot is too drastic. Cutting losses and pretending the current decoded audio buffer is the right seek point."; + } + } + + if (m_nextFrame >= bufferPosition + && m_nextFrame < bufferPosition + bufferLength) { + // m_nextFrame is in this buffer. + buffer += frames2samples(m_nextFrame - bufferPosition); + bufferLength -= m_nextFrame - bufferPosition; + m_seeking = false; + } else { + // we need to keep going forward + goto releaseRawBuffer; + } + } + + // If the bufferLength is larger than the leftover buffer, re-allocate + // it with 2x the space. + if (frames2samples(bufferLength) > m_leftoverBufferSize) { + int newSize = m_leftoverBufferSize; + + while (newSize < frames2samples(bufferLength)) { + newSize *= 2; + } + sample_type* newBuffer = new sample_type[newSize]; + memcpy(newBuffer, m_leftoverBuffer, + sizeof(m_leftoverBuffer[0]) * m_leftoverBufferSize); + delete[] m_leftoverBuffer; + m_leftoverBuffer = newBuffer; + m_leftoverBufferSize = newSize; + } + copyFrames( + sampleBuffer + frames2samples(frameCount - framesNeeded), + &framesNeeded, + buffer, bufferLength); + + releaseRawBuffer: hr = pMBuffer->Unlock(); + // I'm ignoring this, MSDN for IMFMediaBuffer::Unlock stipulates + // nothing about the state of the instance if this fails so might as + // well just let it be released. + //if (FAILED(hr)) break; + releaseMBuffer: safeRelease(&pMBuffer); + releaseSample: safeRelease(&pSample); + if (error) + break; + } + + size_type framesRead = frameCount - framesNeeded; + m_iCurrentPosition += framesRead; + m_nextFrame += framesRead; + if (m_leftoverBufferLength > 0) { + if (framesNeeded != 0) { + qWarning() << __FILE__ << __LINE__ + << "WARNING: Expected frames needed to be 0. Abandoning this file."; + m_dead = true; + } + m_leftoverBufferPosition = m_nextFrame; + } + if (sDebug) { + qDebug() << "read()" << frameCount << "returning" << framesRead; + } + return framesRead; +} + +//------------------------------------------------------------------- +// configureAudioStream +// +// Selects an audio stream from the source file, and configures the +// stream to deliver decoded PCM audio. +//------------------------------------------------------------------- + +/** Cobbled together from: + http://msdn.microsoft.com/en-us/library/dd757929(v=vs.85).aspx + and http://msdn.microsoft.com/en-us/library/dd317928(VS.85).aspx + -- Albert + If anything in here fails, just bail. I'm not going to decode HRESULTS. + -- Bill + */ +bool AudioSourceMediaFoundation::configureAudioStream() { + HRESULT hr(S_OK); + + // deselect all streams, we only want the first + hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, false); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to deselect all streams"; + return false; + } + + hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + true); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to select first audio stream"; + return false; + } + + hr = MFCreateMediaType(&m_pAudioType); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to create media type"; + return false; + } + + hr = m_pAudioType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set major type"; + return false; + } + + hr = m_pAudioType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_Float); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set subtype"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, true); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set samples independent"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_FIXED_SIZE_SAMPLES, true); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set fixed size samples"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_SAMPLE_SIZE, kLeftoverSize); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set sample size"; + return false; + } + + // MSDN for this attribute says that if bps is 8, samples are unsigned. + // Otherwise, they're signed (so they're signed for us as 16 bps). Why + // chose to hide this rather useful tidbit here is beyond me -bkgood + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, sizeof(m_leftoverBuffer[0]) * 8); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set bits per sample"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, + frames2samples(sizeof(m_leftoverBuffer[0]))); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set block alignment"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, kNumChannels); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set number of channels"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, kSampleRate); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set sample rate"; + return false; + } + + // Set this type on the source reader. The source reader will + // load the necessary decoder. + hr = m_pReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + NULL, m_pAudioType); + + // the reader has the media type now, free our reference so we can use our + // pointer for other purposes. Do this before checking for failure so we + // don't dangle. + safeRelease(&m_pAudioType); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set media type"; + return false; + } + + // Get the complete uncompressed format. + hr = m_pReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + &m_pAudioType); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to retrieve completed media type"; + return false; + } + + // Ensure the stream is selected. + hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + true); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to select first audio stream (again)"; + return false; + } + + UINT32 leftoverBufferSize = 0; + hr = m_pAudioType->GetUINT32(MF_MT_SAMPLE_SIZE, &leftoverBufferSize); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to get buffer size"; + return false; + } + m_leftoverBufferSize = leftoverBufferSize; + m_leftoverBufferSize /= sizeof(sample_type); // convert size in bytes to sizeof(sample_type) + m_leftoverBuffer = new sample_type[m_leftoverBufferSize]; + + return true; +} + +/** + * Copies min(destFrames, srcFrames) frames to dest from src. Anything leftover + * is moved to the beginning of m_leftoverBuffer, so empty it first (possibly + * with this method). If src and dest overlap, I'll hurt you. + */ +void AudioSourceMediaFoundation::copyFrames(sample_type *dest, size_t *destFrames, + const sample_type *src, size_t srcFrames) { + if (srcFrames > *destFrames) { + int samplesToCopy(*destFrames * kNumChannels); + memcpy(dest, src, samplesToCopy * sizeof(*src)); + srcFrames -= *destFrames; + memmove(m_leftoverBuffer, src + samplesToCopy, + srcFrames * kNumChannels * sizeof(*src)); + *destFrames = 0; + m_leftoverBufferLength = srcFrames; + } else { + int samplesToCopy(srcFrames * kNumChannels); + memcpy(dest, src, samplesToCopy * sizeof(*src)); + *destFrames -= srcFrames; + if (src == m_leftoverBuffer) { + m_leftoverBufferLength = 0; + } + } +} + +} diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h new file mode 100644 index 00000000000..7e870fbc74a --- /dev/null +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -0,0 +1,63 @@ +#ifndef AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H +#define AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H + +#include "audiosource.h" +#include "util/defs.h" + +#include + +#ifdef Q_OS_WIN +#define MY_EXPORT __declspec(dllexport) +#else +#define MY_EXPORT +#endif + +class IMFSourceReader; +class IMFMediaType; +class IMFMediaSource; + +namespace Mixxx { + +class AudioSourceMediaFoundation : public AudioSource { + typedef AudioSource Super; + +public: + static AudioSourcePointer open(QString fileName); + + ~AudioSourceMediaFoundation(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /* override*/; + +private: + AudioSourceMediaFoundation(); + + Result postConstruct(QString fileName); + + bool configureAudioStream(); + + void copyFrames(sample_type *dest, size_t *destFrames, const sample_type *src, + size_t srcFrames); + + HRESULT m_hrCoInitialize; + HRESULT m_hrMFStartup; + IMFSourceReader *m_pReader; + IMFMediaType *m_pAudioType; + wchar_t *m_wcFilename; + int m_nextFrame; + sample_type *m_leftoverBuffer; + size_t m_leftoverBufferSize; + size_t m_leftoverBufferLength; + int m_leftoverBufferPosition; + qint64 m_mfDuration; + long m_iCurrentPosition; + bool m_dead; + bool m_seeking; +}; + +} + +#endif // ifndef AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 39280877b94..5c975fe9bed 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -22,376 +22,26 @@ #include "soundsourcemediafoundation.h" +#include "audiosourcemediafoundation.h" #include "trackmetadatataglib.h" #include -#include -#include -#include -#include -#include -#include - #include -namespace -{ - -const bool sDebug = false; - -const int kNumChannels = 2; -const int kSampleRate = 44100; -const int kLeftoverSize = 4096; // in sample_type's, this seems to be the size MF AAC -const int kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give - -/** - * Convert a 100ns Media Foundation value to a number of seconds. - */ -inline qreal secondsFromMF(qint64 mf) { - return static_cast(mf) / 1e7; -} - -/** - * Convert a number of seconds to a 100ns Media Foundation value. - */ -inline qint64 mfFromSeconds(qreal sec) { - return sec * 1e7; -} - -/** - * Convert a 100ns Media Foundation value to a frame offset. - */ -inline qint64 frameFromMF(qint64 mf) { - return static_cast(mf) * kSampleRate / 1e7; -} - -/** - * Convert a frame offset to a 100ns Media Foundation value. - */ -inline qint64 mfFromFrame(qint64 frame) { - return static_cast(frame) / kSampleRate * 1e7; -} -} - -/** Microsoft examples use this snippet often. */ -template static void safeRelease(T **ppT) { - if (*ppT) { - (*ppT)->Release(); - *ppT = NULL; - } -} - -SoundSourceMediaFoundation::SoundSourceMediaFoundation(QString filename) - : Super(filename, "m4a"), m_pReader(NULL), m_pAudioType(NULL), m_wcFilename( - NULL), m_nextFrame(0), m_leftoverBuffer(NULL), m_leftoverBufferSize( - 0), m_leftoverBufferLength(0), m_leftoverBufferPosition(0), m_mfDuration( - 0), m_iCurrentPosition(0), m_dead(false), m_seeking(false) { - // these are always the same, might as well just stick them here - // -bkgood - // AudioSource properties - setChannelCount(kNumChannels); - setFrameRate(kSampleRate); - - // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for - // presentation descriptors, one of which MFSourceReader is not. - // Therefore, we calculate it ourselves, assuming 16 bits per sample - setBitrate(frames2samples(kBitsPerSampleForBitrate * kSampleRate) / 1000); - - // http://social.msdn.microsoft.com/Forums/en/netfxbcl/thread/35c6a451-3507-40c8-9d1c-8d4edde7c0cc - // gives maximum path + file length as 248 + 260, using that -bkgood - m_wcFilename = new wchar_t[248 + 260]; -} - -SoundSourceMediaFoundation::~SoundSourceMediaFoundation() { - delete[] m_wcFilename; - delete[] m_leftoverBuffer; - - safeRelease(&m_pReader); - safeRelease(&m_pAudioType); - MFShutdown(); - CoUninitialize(); -} - -Result SoundSourceMediaFoundation::open() { - if (sDebug) { - qDebug() << "open()" << getFilename(); - } - - QString qurlStr(getFilename()); - int wcFilenameLength(getFilename().toWCharArray(m_wcFilename)); - // toWCharArray does not append a null terminator to the string! - m_wcFilename[wcFilenameLength] = '\0'; - - HRESULT hr(S_OK); - // Initialize the COM library. - hr = CoInitializeEx(NULL, - COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to initialize COM"; - return ERR; - } - - // Initialize the Media Foundation platform. - hr = MFStartup(MF_VERSION); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to initialize Media Foundation"; - return ERR; - } - - // Create the source reader to read the input file. - hr = MFCreateSourceReaderFromURL(m_wcFilename, NULL, &m_pReader); - if (FAILED(hr)) { - qWarning() << "SSMF: Error opening input file:" << getFilename(); - return ERR; - } - - if (!configureAudioStream()) { - qWarning() << "SSMF: Error configuring audio stream."; - return ERR; - } - - //Seek to position 0, which forces us to skip over all the header frames. - //This makes sure we're ready to just let the Analyser rip and it'll - //get the number of samples it expects (ie. no header frames). - seekFrame(0); - - return OK; +// static +QList SoundSourceMediaFoundation::supportedFileExtensions() { + QList list; + list.push_back("m4a"); + list.push_back("mp4"); + return list; } -Mixxx::AudioSource::diff_type SoundSourceMediaFoundation::seekFrame( - diff_type frameIndex) { - if (sDebug) { - qDebug() << "seekFrame()" << frameIndex; - } - PROPVARIANT prop; - HRESULT hr(S_OK); - qint64 mfSeekTarget(mfFromFrame(frameIndex) - 1); - // minus 1 here seems to make our seeking work properly, otherwise we will - // (more often than not, maybe always) seek a bit too far (although not - // enough for our calculatedFrameFromMF <= nextFrame assertion in ::read). - // Has something to do with 100ns MF units being much smaller than most - // frame offsets (in seconds) -bkgood - long result = m_iCurrentPosition; - if (m_dead) { - return result; - } - - // this doesn't fail, see MS's implementation - hr = InitPropVariantFromInt64(mfSeekTarget < 0 ? 0 : mfSeekTarget, &prop); - - hr = m_pReader->Flush(MF_SOURCE_READER_FIRST_AUDIO_STREAM); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to flush before seek"; - } - - // http://msdn.microsoft.com/en-us/library/dd374668(v=VS.85).aspx - hr = m_pReader->SetCurrentPosition(GUID_NULL, prop); - if (FAILED(hr)) { - // nothing we can do here as we can't fail (no facility to other than - // crashing mixxx) - qWarning() << "SSMF: failed to seek" - << (hr == MF_E_INVALIDREQUEST ? - "Sample requests still pending" : ""); - } else { - result = frameIndex; - } - PropVariantClear(&prop); - - // record the next frame so that we can make sure we're there the next - // time we get a buffer from MFSourceReader - m_nextFrame = frameIndex; - m_seeking = true; - m_iCurrentPosition = result; - return result; +SoundSourceMediaFoundation::SoundSourceMediaFoundation(QString fileName) + : Super(fileName, "m4a") { } -Mixxx::AudioSource::size_type SoundSourceMediaFoundation::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - if (sDebug) { - qDebug() << "read()" << frameCount; - } - size_type framesNeeded(frameCount); - - // first, copy frames from leftover buffer IF the leftover buffer is at - // the correct frame - if (m_leftoverBufferLength > 0 && m_leftoverBufferPosition == m_nextFrame) { - copyFrames(sampleBuffer, &framesNeeded, m_leftoverBuffer, - m_leftoverBufferLength); - if (m_leftoverBufferLength > 0) { - if (framesNeeded != 0) { - qWarning() << __FILE__ << __LINE__ - << "WARNING: Expected frames needed to be 0. Abandoning this file."; - m_dead = true; - } - m_leftoverBufferPosition += frameCount; - } - } else { - // leftoverBuffer already empty or in the wrong position, clear it - m_leftoverBufferLength = 0; - } - - while (!m_dead && framesNeeded > 0) { - HRESULT hr(S_OK); - DWORD dwFlags(0); - qint64 timestamp(0); - IMFSample *pSample(NULL); - bool error(false); // set to true to break after releasing - - hr = m_pReader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, // [in] DWORD dwStreamIndex, - 0, // [in] DWORD dwControlFlags, - NULL, // [out] DWORD *pdwActualStreamIndex, - &dwFlags, // [out] DWORD *pdwStreamFlags, - ×tamp, // [out] LONGLONG *pllTimestamp, - &pSample); // [out] IMFSample **ppSample - if (FAILED(hr)) { - if (sDebug) { - qDebug() << "ReadSample failed."; - } - break; - } - - if (sDebug) { - qDebug() << "ReadSample timestamp:" << timestamp << "frame:" - << frameFromMF(timestamp) << "dwflags:" << dwFlags; - } - - if (dwFlags & MF_SOURCE_READERF_ERROR) { - // our source reader is now dead, according to the docs - qWarning() - << "SSMF: ReadSample set ERROR, SourceReader is now dead"; - m_dead = true; - break; - } else if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { - qDebug() << "SSMF: End of input file."; - break; - } else if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) { - qWarning() << "SSMF: Type change"; - break; - } else if (pSample == NULL) { - // generally this will happen when dwFlags contains ENDOFSTREAM, - // so it'll be caught before now -bkgood - qWarning() << "SSMF: No sample"; - continue; - } // we now own a ref to the instance at pSample - - IMFMediaBuffer *pMBuffer(NULL); - // I know this does at least a memcopy and maybe a malloc, if we have - // xrun issues with this we might want to look into using - // IMFSample::GetBufferByIndex (although MS doesn't recommend this) - if (FAILED(hr = pSample->ConvertToContiguousBuffer(&pMBuffer))) { - error = true; - goto releaseSample; - } - sample_type *buffer(NULL); - DWORD bufferLengthInBytes(0); - hr = pMBuffer->Lock(reinterpret_cast(&buffer), NULL, &bufferLengthInBytes); - if (FAILED(hr)) { - error = true; - goto releaseMBuffer; - } - size_type bufferLength = samples2frames(bufferLengthInBytes / sizeof(buffer[0])); - - if (m_seeking) { - qint64 bufferPosition(frameFromMF(timestamp)); - if (sDebug) { - qDebug() << "While seeking to " << m_nextFrame - << "WMF put us at" << bufferPosition; - - } - if (m_nextFrame < bufferPosition) { - // Uh oh. We are farther forward than our seek target. Emit - // silence? We can't seek backwards here. - sample_type* pBufferCurpos = sampleBuffer - + frames2samples(frameCount - framesNeeded); - qint64 offshootFrames = bufferPosition - m_nextFrame; - - // If we can correct this immediately, write zeros and adjust - // m_nextFrame to pretend it never happened. - - if (offshootFrames <= framesNeeded) { - qWarning() << __FILE__ << __LINE__ - << "Working around inaccurate seeking. Writing silence for" - << offshootFrames << "frames"; - // Set offshootFrames * kNumChannels samples to zero. - memset(pBufferCurpos, 0, - frames2samples(sizeof(*pBufferCurpos) * offshootFrames)); - // Now m_nextFrame == bufferPosition - m_nextFrame += offshootFrames; - framesNeeded -= offshootFrames; - } else { - // It's more complicated. The buffer we have just decoded is - // more than framesNeeded frames away from us. It's too hard - // for us to handle this correctly currently, so let's just - // try to get on with our lives. - m_seeking = false; - m_nextFrame = bufferPosition; - qWarning() << __FILE__ << __LINE__ - << "Seek offshoot is too drastic. Cutting losses and pretending the current decoded audio buffer is the right seek point."; - } - } - - if (m_nextFrame >= bufferPosition - && m_nextFrame < bufferPosition + bufferLength) { - // m_nextFrame is in this buffer. - buffer += frames2samples(m_nextFrame - bufferPosition); - bufferLength -= m_nextFrame - bufferPosition; - m_seeking = false; - } else { - // we need to keep going forward - goto releaseRawBuffer; - } - } - - // If the bufferLength is larger than the leftover buffer, re-allocate - // it with 2x the space. - if (frames2samples(bufferLength) > m_leftoverBufferSize) { - int newSize = m_leftoverBufferSize; - - while (newSize < frames2samples(bufferLength)) { - newSize *= 2; - } - sample_type* newBuffer = new sample_type[newSize]; - memcpy(newBuffer, m_leftoverBuffer, - sizeof(m_leftoverBuffer[0]) * m_leftoverBufferSize); - delete[] m_leftoverBuffer; - m_leftoverBuffer = newBuffer; - m_leftoverBufferSize = newSize; - } - copyFrames( - sampleBuffer + frames2samples(frameCount - framesNeeded), - &framesNeeded, - buffer, bufferLength); - - releaseRawBuffer: hr = pMBuffer->Unlock(); - // I'm ignoring this, MSDN for IMFMediaBuffer::Unlock stipulates - // nothing about the state of the instance if this fails so might as - // well just let it be released. - //if (FAILED(hr)) break; - releaseMBuffer: safeRelease(&pMBuffer); - releaseSample: safeRelease(&pSample); - if (error) - break; - } - - size_type framesRead = frameCount - framesNeeded; - m_iCurrentPosition += framesRead; - m_nextFrame += framesRead; - if (m_leftoverBufferLength > 0) { - if (framesNeeded != 0) { - qWarning() << __FILE__ << __LINE__ - << "WARNING: Expected frames needed to be 0. Abandoning this file."; - m_dead = true; - } - m_leftoverBufferPosition = m_nextFrame; - } - if (sDebug) { - qDebug() << "read()" << frameCount << "returning" << framesRead; - } - return framesRead; -} - -Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata) { +Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { // Must be toLocal8Bit since Windows fopen does not do UTF-8 TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); @@ -415,7 +65,7 @@ Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata return OK; } -QImage SoundSourceMediaFoundation::parseCoverArt() { +QImage SoundSourceMediaFoundation::parseCoverArt() const { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { @@ -425,173 +75,6 @@ QImage SoundSourceMediaFoundation::parseCoverArt() { } } -// static -QList SoundSourceMediaFoundation::supportedFileExtensions() { - QList list; - list.push_back("m4a"); - list.push_back("mp4"); - return list; -} - -//------------------------------------------------------------------- -// configureAudioStream -// -// Selects an audio stream from the source file, and configures the -// stream to deliver decoded PCM audio. -//------------------------------------------------------------------- - -/** Cobbled together from: - http://msdn.microsoft.com/en-us/library/dd757929(v=vs.85).aspx - and http://msdn.microsoft.com/en-us/library/dd317928(VS.85).aspx - -- Albert - If anything in here fails, just bail. I'm not going to decode HRESULTS. - -- Bill - */ -bool SoundSourceMediaFoundation::configureAudioStream() { - HRESULT hr(S_OK); - - // deselect all streams, we only want the first - hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, false); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to deselect all streams"; - return false; - } - - hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, - true); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to select first audio stream"; - return false; - } - - hr = MFCreateMediaType(&m_pAudioType); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to create media type"; - return false; - } - - hr = m_pAudioType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set major type"; - return false; - } - - hr = m_pAudioType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_Float); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set subtype"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, true); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set samples independent"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_FIXED_SIZE_SAMPLES, true); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set fixed size samples"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_SAMPLE_SIZE, kLeftoverSize); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set sample size"; - return false; - } - - // MSDN for this attribute says that if bps is 8, samples are unsigned. - // Otherwise, they're signed (so they're signed for us as 16 bps). Why - // chose to hide this rather useful tidbit here is beyond me -bkgood - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, sizeof(m_leftoverBuffer[0]) * 8); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set bits per sample"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, - frames2samples(sizeof(m_leftoverBuffer[0]))); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set block alignment"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, kNumChannels); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set number of channels"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, kSampleRate); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set sample rate"; - return false; - } - - // Set this type on the source reader. The source reader will - // load the necessary decoder. - hr = m_pReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, - NULL, m_pAudioType); - - // the reader has the media type now, free our reference so we can use our - // pointer for other purposes. Do this before checking for failure so we - // don't dangle. - safeRelease(&m_pAudioType); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set media type"; - return false; - } - - // Get the complete uncompressed format. - hr = m_pReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, - &m_pAudioType); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to retrieve completed media type"; - return false; - } - - // Ensure the stream is selected. - hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, - true); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to select first audio stream (again)"; - return false; - } - - UINT32 leftoverBufferSize = 0; - hr = m_pAudioType->GetUINT32(MF_MT_SAMPLE_SIZE, &leftoverBufferSize); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to get buffer size"; - return false; - } - m_leftoverBufferSize = leftoverBufferSize; - m_leftoverBufferSize /= sizeof(sample_type); // convert size in bytes to sizeof(sample_type) - m_leftoverBuffer = new sample_type[m_leftoverBufferSize]; - - return true; -} - -/** - * Copies min(destFrames, srcFrames) frames to dest from src. Anything leftover - * is moved to the beginning of m_leftoverBuffer, so empty it first (possibly - * with this method). If src and dest overlap, I'll hurt you. - */ -void SoundSourceMediaFoundation::copyFrames(sample_type *dest, size_t *destFrames, - const sample_type *src, size_t srcFrames) { - if (srcFrames > *destFrames) { - int samplesToCopy(*destFrames * kNumChannels); - memcpy(dest, src, samplesToCopy * sizeof(*src)); - srcFrames -= *destFrames; - memmove(m_leftoverBuffer, src + samplesToCopy, - srcFrames * kNumChannels * sizeof(*src)); - *destFrames = 0; - m_leftoverBufferLength = srcFrames; - } else { - int samplesToCopy(srcFrames * kNumChannels); - memcpy(dest, src, samplesToCopy * sizeof(*src)); - *destFrames -= srcFrames; - if (src == m_leftoverBuffer) { - m_leftoverBufferLength = 0; - } - } +Mixxx::AudioSourcePointer SoundSourceMediaFoundation::open() const { + return Mixxx::AudioSourceMediaFoundation::open(getFilename()); } diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 8d3cdcf787b..c23cf3c546c 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -20,12 +20,8 @@ #ifndef SOUNDSOURCEMEDIAFOUNDATION_H #define SOUNDSOURCEMEDIAFOUNDATION_H -#include -#include - -#include "util/defs.h" -#include "defs_version.h" #include "soundsource.h" +#include "defs_version.h" #ifdef Q_OS_WIN #define MY_EXPORT __declspec(dllexport) @@ -33,46 +29,18 @@ #define MY_EXPORT #endif -class IMFSourceReader; -class IMFMediaType; -class IMFMediaSource; - class SoundSourceMediaFoundation : public Mixxx::SoundSource { typedef SoundSource Super; public: static QList supportedFileExtensions(); - explicit SoundSourceMediaFoundation(QString filename); - ~SoundSourceMediaFoundation(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - QImage parseCoverArt() /*override*/; - - Result open(); - - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; - -private: - bool configureAudioStream(); + explicit SoundSourceMediaFoundation(QString fileName); - void copyFrames(sample_type *dest, size_t *destFrames, const sample_type *src, - size_t srcFrames); + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; - IMFSourceReader *m_pReader; - IMFMediaType *m_pAudioType; - wchar_t *m_wcFilename; - int m_nextFrame; - sample_type *m_leftoverBuffer; - size_t m_leftoverBufferSize; - size_t m_leftoverBufferLength; - int m_leftoverBufferPosition; - qint64 m_mfDuration; - long m_iCurrentPosition; - bool m_dead; - bool m_seeking; + Mixxx::AudioSourcePointer open() const /*override*/; }; extern "C" MY_EXPORT const char* getMixxxVersion() { @@ -83,8 +51,8 @@ extern "C" MY_EXPORT int getSoundSourceAPIVersion() { return MIXXX_SOUNDSOURCE_API_VERSION; } -extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString filename) { - return new SoundSourceMediaFoundation(filename); +extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { + return new SoundSourceMediaFoundation(fileName); } extern "C" MY_EXPORT char** supportedFileExtensions() { diff --git a/plugins/soundsourcewv/SConscript b/plugins/soundsourcewv/SConscript index 82e904c6f9c..bc9e129f945 100644 --- a/plugins/soundsourcewv/SConscript +++ b/plugins/soundsourcewv/SConscript @@ -11,7 +11,8 @@ Import('build') # On Posix default SCons.LIBPREFIX = 'lib', on Windows default SCons.LIBPREFIX = '' wv_sources = [ - "soundsourcewv.cpp", # Wavpack support + "soundsourcewv.cpp", # Wavpack support (tags) + "audiosourcewv.cpp", # Wavpack support (audio) "soundsource.cpp", # required to subclass SoundSource "audiosource.cpp", # required to subclass AudioSource "trackmetadatataglib.cpp", # TagLib dependencies diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp new file mode 100644 index 00000000000..562a8c5a147 --- /dev/null +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -0,0 +1,86 @@ +#include "audiosourcewv.h" + +#include "sampleutil.h" + +namespace Mixxx { + +AudioSourceWV::AudioSourceWV() + : m_wpc(NULL), m_sampleScale(0.0f) { +} + +AudioSourceWV::~AudioSourceWV() { + close(); +} + +AudioSourcePointer AudioSourceWV::open(QString fileName) { + AudioSourceWV* pAudioSourceWV(new AudioSourceWV); + AudioSourcePointer pAudioSource(pAudioSourceWV); // take ownership + if (OK == pAudioSourceWV->postConstruct(fileName)) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceWV::postConstruct(QString fileName) { + char msg[80]; // hold possible error message + m_wpc = WavpackOpenFileInput(fileName.toLocal8Bit().constData(), msg, + OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); + if (!m_wpc) { + qDebug() << "SSWV::open: failed to open file : " << msg; + return ERR; + } + + setChannelCount(WavpackGetReducedChannels(m_wpc)); + setFrameRate(WavpackGetSampleRate(m_wpc)); + setFrameCount(WavpackGetNumSamples(m_wpc)); + + if (WavpackGetMode(m_wpc) & MODE_FLOAT) { + m_sampleScale = 1.0f; + } else { + const int bitsPerSample = WavpackGetBitsPerSample(m_wpc); + const uint32_t maxSampleValue = uint32_t(1) << (bitsPerSample - 1); + m_sampleScale = 1.0f / (0.5f + sample_type(maxSampleValue)); + } + + return OK; +} + +void AudioSourceWV::close() throw() { + if (m_wpc) { + WavpackCloseFile(m_wpc); + m_wpc = NULL; + } + Super::reset(); +} + +AudioSource::diff_type AudioSourceWV::seekFrame(diff_type frameIndex) { + if (WavpackSeekSample(m_wpc, frameIndex) == TRUE) { + return frameIndex; + } else { + qDebug() << "SSWV::seek : could not seek to frame #" << frameIndex; + return WavpackGetSampleIndex(m_wpc); + } +} + +AudioSource::size_type AudioSourceWV::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + // static assert: sizeof(sample_type) == sizeof(int32_t) + size_type unpackCount = WavpackUnpackSamples(m_wpc, + reinterpret_cast(sampleBuffer), frameCount); + if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { + // signed integer -> float + const size_type sampleCount = frames2samples(unpackCount); + for (size_type i = 0; i < sampleCount; ++i) { + const int32_t sampleValue = + reinterpret_cast(sampleBuffer)[i]; + sampleBuffer[i] = SampleUtil::clampSample( + sampleValue * m_sampleScale); + } + } + return unpackCount; +} + +} // namespace Mixxx diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h new file mode 100644 index 00000000000..17bdc630b43 --- /dev/null +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -0,0 +1,43 @@ +#ifndef AUDIOSOURCEWV_H +#define AUDIOSOURCEWV_H + +#include "audiosource.h" +#include "util/defs.h" + +#include "wavpack/wavpack.h" + +#ifdef Q_OS_WIN +#define MY_EXPORT __declspec(dllexport) +#else +#define MY_EXPORT +#endif + +namespace Mixxx { + +class AudioSourceWV: public AudioSource { + typedef AudioSource Super; + +public: + static AudioSourcePointer open(QString fileName); + + ~AudioSourceWV(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /* override*/; + +private: + AudioSourceWV(); + + Result postConstruct(QString fileName); + + WavpackContext* m_wpc; + + sample_type m_sampleScale; +}; + +} // namespace Mixxx + +#endif diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index f837ad97fad..16a434d83f3 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -1,7 +1,7 @@ #include "soundsourcewv.h" +#include "audiosourcewv.h" #include "trackmetadatataglib.h" -#include "sampleutil.h" #include @@ -13,68 +13,11 @@ QList SoundSourceWV::supportedFileExtensions() { return list; } -SoundSourceWV::SoundSourceWV(QString qFilename) - : Super(qFilename, "wv"), m_wpc(NULL), m_sampleScale(0.0f) { +SoundSourceWV::SoundSourceWV(QString fileName) + : Super(fileName, "wv") { } -SoundSourceWV::~SoundSourceWV() { - if (m_wpc) { - WavpackCloseFile(m_wpc); - } -} - -Result SoundSourceWV::open() { - char msg[80]; // hold possible error message - m_wpc = WavpackOpenFileInput(getFilename().toLocal8Bit().constData(), msg, - OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); - if (!m_wpc) { - qDebug() << "SSWV::open: failed to open file : " << msg; - return ERR; - } - - setChannelCount(WavpackGetReducedChannels(m_wpc)); - setFrameRate(WavpackGetSampleRate(m_wpc)); - setFrameCount(WavpackGetNumSamples(m_wpc)); - - if (WavpackGetMode(m_wpc) & MODE_FLOAT) { - m_sampleScale = 1.0f; - } else { - const int bitsPerSample = WavpackGetBitsPerSample(m_wpc); - const uint32_t maxSampleValue = uint32_t(1) << (bitsPerSample - 1); - m_sampleScale = 1.0f / (0.5f + sample_type(maxSampleValue)); - } - - return OK; -} - -AudioSource::diff_type SoundSourceWV::seekFrame(diff_type frameIndex) { - if (WavpackSeekSample(m_wpc, frameIndex) == TRUE) { - return frameIndex; - } else { - qDebug() << "SSWV::seek : could not seek to frame #" << frameIndex; - return WavpackGetSampleIndex(m_wpc); - } -} - -AudioSource::size_type SoundSourceWV::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - // static assert: sizeof(sample_type) == sizeof(int32_t) - size_type unpackCount = WavpackUnpackSamples(m_wpc, - reinterpret_cast(sampleBuffer), frameCount); - if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { - // signed integer -> float - const size_type sampleCount = frames2samples(unpackCount); - for (size_type i = 0; i < sampleCount; ++i) { - const int32_t sampleValue = - reinterpret_cast(sampleBuffer)[i]; - sampleBuffer[i] = SampleUtil::clampSample( - sampleValue * m_sampleScale); - } - } - return unpackCount; -} - -Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) { +Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { TagLib::WavPack::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { @@ -97,7 +40,7 @@ Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -QImage SoundSourceWV::parseCoverArt() { +QImage SoundSourceWV::parseCoverArt() const { TagLib::WavPack::File f(getFilename().toLocal8Bit().constData()); TagLib::APE::Tag *ape = f.APETag(); if (ape) { @@ -107,4 +50,44 @@ QImage SoundSourceWV::parseCoverArt() { } } +Mixxx::AudioSourcePointer SoundSourceWV::open() const { + return Mixxx::AudioSourceWV::open(getFilename()); +} + } // namespace Mixxx + +extern "C" MY_EXPORT const char* getMixxxVersion() { + return VERSION; +} + +extern "C" MY_EXPORT int getSoundSourceAPIVersion() { + return MIXXX_SOUNDSOURCE_API_VERSION; +} + +extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { + return new Mixxx::SoundSourceWV(fileName); +} + +extern "C" MY_EXPORT char** supportedFileExtensions() { + QList exts = Mixxx::SoundSourceWV::supportedFileExtensions(); + //Convert to C string array. + char** c_exts = (char**)malloc((exts.count() + 1) * sizeof(char*)); + for (int i = 0; i < exts.count(); i++) + { + QByteArray qba = exts[i].toUtf8(); + c_exts[i] = strdup(qba.constData()); + qDebug() << c_exts[i]; + } + c_exts[exts.count()] = NULL; //NULL terminate the list + + return c_exts; +} + +extern "C" MY_EXPORT void freeFileExtensions(char **exts) { + if (exts) { + for (int i(0); exts[i]; ++i) { + free(exts[i]); + } + free(exts); + } +} diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 95eaa5b4b91..39c30e47345 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -4,16 +4,12 @@ #include "soundsource.h" #include "defs_version.h" -#include "wavpack/wavpack.h" - #ifdef Q_OS_WIN #define MY_EXPORT __declspec(dllexport) #else #define MY_EXPORT #endif -#define WV_BUF_LENGTH 65536 - namespace Mixxx { class SoundSourceWV: public SoundSource { @@ -22,57 +18,20 @@ class SoundSourceWV: public SoundSource { public: static QList supportedFileExtensions(); - explicit SoundSourceWV(QString qFilename); - ~SoundSourceWV(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - QImage parseCoverArt() /*override*/; - - Result open(); + explicit SoundSourceWV(QString fileName); - diff_type seekFrame(diff_type frameIndex) /*override*/; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; - -private: - WavpackContext* m_wpc; - - sample_type m_sampleScale; + Mixxx::AudioSourcePointer open() const /*override*/; }; -extern "C" MY_EXPORT const char* getMixxxVersion() { - return VERSION; -} - -extern "C" MY_EXPORT int getSoundSourceAPIVersion() { - return MIXXX_SOUNDSOURCE_API_VERSION; -} - -extern "C" MY_EXPORT SoundSource* getSoundSource(QString filename) { - return new SoundSourceWV(filename); -} - -extern "C" MY_EXPORT char** supportedFileExtensions() { - QList exts = SoundSourceWV::supportedFileExtensions(); - //Convert to C string array. - char** c_exts = (char**) malloc((exts.count() + 1) * sizeof(char*)); - for (int i = 0; i < exts.count(); i++) { - QByteArray qba = exts[i].toUtf8(); - c_exts[i] = strdup(qba.constData()); - qDebug() << c_exts[i]; - } - c_exts[exts.count()] = NULL; //NULL terminate the list - - return c_exts; -} - -extern "C" MY_EXPORT void freeFileExtensions(char **exts) { - for (int i(0); exts[i]; ++i) - free(exts[i]); - free(exts); -} - } // namespace Mixxx +extern "C" MY_EXPORT const char* getMixxxVersion(); +extern "C" MY_EXPORT int getSoundSourceAPIVersion(); +extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName); +extern "C" MY_EXPORT char** supportedFileExtensions(); +extern "C" MY_EXPORT void freeFileExtensions(char **exts); + #endif diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 4c1a4d9dc19..d67a83c12de 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -283,25 +283,17 @@ void AnalyserQueue::run() { // Get the audio SoundSourceProxy soundSourceProxy(nextTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); - if (pAudioSource.isNull()) { + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); + if (!pAudioSource) { qWarning() << "Failed to open file for analyzing:" << nextTrack->getLocation(); continue; } - int iNumSamples = pAudioSource->getFrameCount() * kAnalysisChannels; - int iSampleRate = pAudioSource->getFrameRate(); - - if (iNumSamples == 0 || iSampleRate == 0) { - qWarning() << "Skipping invalid file:" << nextTrack->getLocation(); - continue; - } - QListIterator it(m_aq); bool processTrack = false; while (it.hasNext()) { // Make sure not to short-circuit initialise(...) - if (it.next()->initialise(nextTrack, iSampleRate, iNumSamples)) { + if (it.next()->initialise(nextTrack, pAudioSource->getFrameRate(), pAudioSource->frames2samples(pAudioSource->getFrameCount()))) { processTrack = true; } } diff --git a/src/audiosource.h b/src/audiosource.h index ba94a4b9ec8..8ca19a273e9 100644 --- a/src/audiosource.h +++ b/src/audiosource.h @@ -159,6 +159,12 @@ class AudioSource { virtual size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer); + // Explicitly closes the audio sources. It is safe to + // close an audio source repeatedly. Should be invoked + // in the destructor of derived classes and therefore + // must not throw any exceptions + virtual void close() throw() = 0; + protected: AudioSource(); diff --git a/src/audiosourcecoreaudio.cpp b/src/audiosourcecoreaudio.cpp new file mode 100644 index 00000000000..3ef3047caea --- /dev/null +++ b/src/audiosourcecoreaudio.cpp @@ -0,0 +1,159 @@ +#include "audiosourcecoreaudio.h" + +#include "util/math.h" + +#include + +namespace Mixxx { + +namespace +{ +AudioSource::size_type kChannelCount = 2; +} + +AudioSourceCoreAudio::AudioSourceCoreAudio() + : m_headerFrames(0) { +} + +AudioSourceCoreAudio::~AudioSourceCoreAudio() { + close(); +} + +AudioSourcePointer AudioSourceCoreAudio::open(QString fileName) { + AudioSourceCoreAudio* pAudioSourceCoreAudio(new AudioSourceCoreAudio); + AudioSourcePointer pAudioSource(pAudioSourceCoreAudio); // take ownership + if (OK == pAudioSourceCoreAudio->postConstruct(fileName)) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +// soundsource overrides +Result AudioSourceCoreAudio::postConstruct(QString fileName) { + //Open the audio file. + OSStatus err; + + /** This code blocks works with OS X 10.5+ only. DO NOT DELETE IT for now. */ + CFStringRef urlStr = CFStringCreateWithCharacters( + 0, reinterpret_cast(fileName.unicode()), fileName.size()); + CFURLRef urlRef = CFURLCreateWithFileSystemPath(NULL, urlStr, kCFURLPOSIXPathStyle, false); + err = ExtAudioFileOpenURL(urlRef, &m_audioFile); + CFRelease(urlStr); + CFRelease(urlRef); + + /** TODO: Use FSRef for compatibility with 10.4 Tiger. + Note that ExtAudioFileOpen() is deprecated above Tiger, so we must maintain + both code paths if someone finishes this part of the code. + FSRef fsRef; + CFURLGetFSRef(reinterpret_cast(url.get()), &fsRef); + err = ExtAudioFileOpen(&fsRef, &m_audioFile); + */ + + if (err != noErr) { + qDebug() << "SSCA: Error opening file " << fileName; + return ERR; + } + + // get the input file format + UInt32 inputFormatSize = sizeof(m_inputFormat); + err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileDataFormat, &inputFormatSize, &m_inputFormat); + if (err != noErr) { + qDebug() << "SSCA: Error getting file format (" << fileName << ")"; + return ERR; + } + + // create the output format + m_outputFormat = CAStreamBasicDescription( + m_inputFormat.mSampleRate, kChannelCount, + CAStreamBasicDescription::kPCMFormatFloat32, true); + + // set the client format + err = ExtAudioFileSetProperty(m_audioFile, kExtAudioFileProperty_ClientDataFormat, + sizeof(m_outputFormat), &m_outputFormat); + if (err != noErr) { + qDebug() << "SSCA: Error setting file property"; + return ERR; + } + + //get the total length in frames of the audio file - copypasta: http://discussions.apple.com/thread.jspa?threadID=2364583&tstart=47 + SInt64 totalFrameCount; + UInt32 totalFrameCountSize = sizeof(totalFrameCount); + err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileLengthFrames, &totalFrameCountSize, &totalFrameCount); + if (err != noErr) { + qDebug() << "SSCA: Error getting number of frames"; + return ERR; + } + + // + // WORKAROUND for bug in ExtFileAudio + // + + AudioConverterRef acRef; + UInt32 acrsize = sizeof(AudioConverterRef); + err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_AudioConverter, &acrsize, &acRef); + //_ThrowExceptionIfErr(@"kExtAudioFileProperty_AudioConverter", err); + + AudioConverterPrimeInfo primeInfo; + UInt32 piSize = sizeof(AudioConverterPrimeInfo); + memset(&primeInfo, 0, piSize); + err = AudioConverterGetProperty(acRef, kAudioConverterPrimeInfo, &piSize, &primeInfo); + if (err != kAudioConverterErr_PropertyNotSupported) { // Only if decompressing + //_ThrowExceptionIfErr(@"kAudioConverterPrimeInfo", err); + m_headerFrames = primeInfo.leadingFrames; + } + + setChannelCount(m_outputFormat.NumberChannels()); + setFrameRate(m_inputFormat.mSampleRate); + setFrameCount(totalFrameCount/* - m_headerFrames*/); + + //Seek to position 0, which forces us to skip over all the header frames. + //This makes sure we're ready to just let the Analyser rip and it'll + //get the number of samples it expects (ie. no header frames). + seekFrame(0); + + return OK; +} + +void AudioSourceCoreAudio::close() throw() { + ExtAudioFileDispose(m_audioFile); +} + +AudioSource::diff_type AudioSourceCoreAudio::seekFrame(diff_type frameIndex) { + OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); + //_ThrowExceptionIfErr(@"ExtAudioFileSeek", err); + //qDebug() << "SSCA: Seeking to" << frameIndex; + if (err != noErr) { + qDebug() << "SSCA: Error seeking to" << frameIndex;// << GetMacOSStatusErrorString(err) << GetMacOSStatusCommentString(err); + } + return frameIndex; +} + +AudioSource::size_type AudioSourceCoreAudio::readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) { + //if (!m_decoder) return 0; + size_type numFramesRead = 0; + + while (numFramesRead < frameCount) { + size_type numFramesToRead = frameCount - numFramesRead; + + AudioBufferList fillBufList; + fillBufList.mNumberBuffers = 1; + fillBufList.mBuffers[0].mNumberChannels = getChannelCount(); + fillBufList.mBuffers[0].mDataByteSize = frames2samples(numFramesToRead) * sizeof(sampleBuffer[0]); + fillBufList.mBuffers[0].mData = sampleBuffer + frames2samples(numFramesRead); + + UInt32 numFramesToReadInOut = numFramesToRead; // input/output parameter + OSStatus err = ExtAudioFileRead(m_audioFile, &numFramesToReadInOut, &fillBufList); + if (0 == numFramesToReadInOut) { + // EOF + break; // done + } + numFramesRead += numFramesToReadInOut; + } + return numFramesRead; +} + +} // namespace Mixxx diff --git a/src/audiosourcecoreaudio.h b/src/audiosourcecoreaudio.h new file mode 100644 index 00000000000..f46b348c7fd --- /dev/null +++ b/src/audiosourcecoreaudio.h @@ -0,0 +1,51 @@ +#ifndef AUDIOSOURCECOREAUDIO_H +#define AUDIOSOURCECOREAUDIO_H + +#include "audiosource.h" +#include "util/defs.h" + +#include +//In our tree at lib/apple/ +#include "CAStreamBasicDescription.h" + +#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) +#include +#include +#include +#include +#else +#include "CoreAudioTypes.h" +#include "AudioFile.h" +#include "AudioFormat.h" +#endif + +namespace Mixxx { + +class AudioSourceCoreAudio : public AudioSource { + typedef AudioSource Super; + +public: + static AudioSourcePointer open(QString fileName); + + ~AudioSourceCoreAudio(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) /*override*/; + + void close() throw() /*override*/; + +private: + AudioSourceCoreAudio(); + + Result postConstruct(QString fileName); + + ExtAudioFileRef m_audioFile; + CAStreamBasicDescription m_inputFormat; + CAStreamBasicDescription m_outputFormat; + SInt64 m_headerFrames; +}; + +} + +#endif // ifndef AUDIOSOURCECOREAUDIO_H diff --git a/src/audiosourceffmpeg.cpp b/src/audiosourceffmpeg.cpp new file mode 100644 index 00000000000..f9df8c84880 --- /dev/null +++ b/src/audiosourceffmpeg.cpp @@ -0,0 +1,504 @@ +#include "audiosourceffmpeg.h" + +#include + +#include + +#define AUDIOSOURCEFFMPEG_CACHESIZE 1000 +#define AUDIOSOURCEFFMPEG_POSDISTANCE ((1024 * 1000) / 8) + +namespace Mixxx { + +AudioSourceFFmpeg::AudioSourceFFmpeg() + : m_pFormatCtx(NULL) + , m_iAudioStream(-1) + , m_pCodecCtx(NULL) + , m_pCodec(NULL) + , m_pResample(NULL) + , m_iCurrentMixxTs(0) + , m_bIsSeeked(false) + , m_lCacheBytePos(0) + , m_lCacheStartByte(0) + , m_lCacheEndByte(0) + , m_lCacheLastPos(0) + , m_lLastStoredPos(0) + , m_lStoredSeekPoint(-1) { +} + +AudioSourceFFmpeg::~AudioSourceFFmpeg() { + close(); +} + +AudioSourcePointer AudioSourceFFmpeg::open(QString fileName) { + AudioSourceFFmpeg* pAudioSourceFFmpeg(new AudioSourceFFmpeg); + AudioSourcePointer pAudioSource(pAudioSourceFFmpeg); // take ownership + if (OK == pAudioSourceFFmpeg->postConstruct(fileName)) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceFFmpeg::postConstruct(QString fileName) { + unsigned int i; + AVDictionary *l_iFormatOpts = NULL; + + QByteArray qBAFilename = fileName.toLocal8Bit(); + + qDebug() << "New AudioSourceFFmpeg :" << qBAFilename; + + m_pFormatCtx = avformat_alloc_context(); + +#if LIBAVCODEC_VERSION_INT < 3622144 + m_pFormatCtx->max_analyze_duration = 999999999; +#endif + + // Open file and make m_pFormatCtx + if (avformat_open_input(&m_pFormatCtx, qBAFilename.constData(), NULL, + &l_iFormatOpts)!=0) { + qDebug() << "av_open_input_file: cannot open" << qBAFilename; + return ERR; + } + +#if LIBAVCODEC_VERSION_INT > 3544932 + av_dict_free(&l_iFormatOpts); +#endif + + // Retrieve stream information + if (avformat_find_stream_info(m_pFormatCtx, NULL)<0) { + qDebug() << "av_find_stream_info: cannot open" << qBAFilename; + return ERR; + } + + //debug only (Enable if needed) + //av_dump_format(m_pFormatCtx, 0, qBAFilename.constData(), false); + + // Find the first video stream + m_iAudioStream=-1; + + for (i=0; inb_streams; i++) + if (m_pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) { + m_iAudioStream=i; + break; + } + if (m_iAudioStream==-1) { + qDebug() << "ffmpeg: cannot find an audio stream: cannot open" + << qBAFilename; + return ERR; + } + + // Get a pointer to the codec context for the video stream + m_pCodecCtx=m_pFormatCtx->streams[m_iAudioStream]->codec; + + // Find the decoder for the audio stream + if (!(m_pCodec=avcodec_find_decoder(m_pCodecCtx->codec_id))) { + qDebug() << "ffmpeg: cannot find a decoder for" << qBAFilename; + return ERR; + } + + if (avcodec_open2(m_pCodecCtx, m_pCodec, NULL)<0) { + qDebug() << "ffmpeg: cannot open" << qBAFilename; + return ERR; + } + + m_pResample = new EncoderFfmpegResample(m_pCodecCtx); + m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_S16); + + setChannelCount(m_pCodecCtx->channels); + setFrameRate(m_pCodecCtx->sample_rate); + setFrameCount((m_pFormatCtx->duration * m_pCodecCtx->sample_rate) / AV_TIME_BASE); + + qDebug() << "ffmpeg: Samplerate: " << getFrameRate() << ", Channels: " << + getChannelCount() << "\n"; + if (getChannelCount() > 2) { + qDebug() << "ffmpeg: No support for more than 2 channels!"; + return ERR; + } + + return OK; +} + +void AudioSourceFFmpeg::close() throw() { + clearCache(); + + if (m_pCodecCtx != NULL) { + qDebug() << "~AudioSourceFFmpeg(): Clear FFMPEG stuff"; + avcodec_close(m_pCodecCtx); + avformat_close_input(&m_pFormatCtx); + av_free(m_pFormatCtx); + } + + if (m_pResample != NULL) { + qDebug() << "~AudioSourceFFmpeg(): Delete FFMPEG Resampler"; + delete m_pResample; + } + + while (m_SJumpPoints.size() > 0) { + ffmpegLocationObject* l_SRmJmp = m_SJumpPoints[0]; + m_SJumpPoints.remove(0); + free(l_SRmJmp); + } +} + +void AudioSourceFFmpeg::clearCache() throw() { + while (m_SCache.size() > 0) { + struct ffmpegCacheObject* l_SRmObj = m_SCache[0]; + m_SCache.remove(0); + free(l_SRmObj->bytes); + free(l_SRmObj); + } +} + +bool AudioSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { + unsigned int l_iCount = 0; + qint32 l_iRet = 0; + AVPacket l_SPacket; + AVFrame *l_pFrame = NULL; + bool l_iStop = false; + int l_iFrameFinished = 0; + struct ffmpegCacheObject *l_SObj = NULL; + struct ffmpegCacheObject *l_SRmObj = NULL; + bool m_bUnique = false; + qint64 l_lLastPacketPos = -1; + int l_iError = 0; + int l_iFrameCount = 0; + + l_iCount = count; + + l_SPacket.data = NULL; + l_SPacket.size = 0; + + + while (l_iCount > 0) { + if (l_pFrame != NULL) { + l_iFrameCount --; +// FFMPEG 2.2 3561060 anb beyond +#if LIBAVCODEC_VERSION_INT >= 3561060 + av_frame_free(&l_pFrame); +// FFMPEG 0.11 and below +#elif LIBAVCODEC_VERSION_INT <= 3544932 + av_free(l_pFrame); +// FFMPEG 1.0 - 2.1 +#else + avcodec_free_frame(&l_pFrame); +#endif + l_pFrame = NULL; + + } + + if (l_iStop == true) { + break; + } + l_iFrameCount ++; + av_init_packet(&l_SPacket); +#if LIBAVCODEC_VERSION_INT < 3617792 + l_pFrame = avcodec_alloc_frame(); +#else + l_pFrame = av_frame_alloc(); +#endif + + if (av_read_frame(m_pFormatCtx, &l_SPacket) >= 0) { + if (l_SPacket.stream_index == m_iAudioStream) { + if (m_lStoredSeekPoint > 0) { + // Seek for correct jump point + if (m_lStoredSeekPoint > l_SPacket.pos && + m_lStoredSeekPoint >= AUDIOSOURCEFFMPEG_POSDISTANCE) { + av_free_packet(&l_SPacket); + l_SPacket.data = NULL; + l_SPacket.size = 0; + continue; + } + m_lStoredSeekPoint = -1; + } + + l_iRet = avcodec_decode_audio4(m_pCodecCtx,l_pFrame,&l_iFrameFinished, + &l_SPacket); + + if (l_iRet <= 0) { + // An error or EOF occured,index break out and return what + // we have so far. + qDebug() << "EOF!"; + l_iStop = true; + continue; + } else { + l_iRet = 0; + l_SObj = (struct ffmpegCacheObject *)malloc(sizeof(struct ffmpegCacheObject)); + if (l_SObj == NULL) { + qDebug() << "AudioSourceFFmpeg::readFramesToCache: Not enough memory!"; + l_iStop = true; + continue; + } + memset(l_SObj, 0x00, sizeof(struct ffmpegCacheObject)); + l_iRet = m_pResample->reSample(l_pFrame, &l_SObj->bytes); + + if (l_iRet > 0) { + // Remove from cache + if (m_SCache.size() >= (AUDIOSOURCEFFMPEG_CACHESIZE - 10)) { + l_SRmObj = m_SCache[0]; + m_SCache.remove(0); + free(l_SRmObj->bytes); + free(l_SRmObj); + } + + // Add to cache and store byte place to memory + m_SCache.append(l_SObj); + l_SObj->startByte = m_lCacheBytePos / 2; + l_SObj->length = l_iRet / 2; + m_lCacheBytePos += l_iRet; + + // Ogg/Opus have packages pos that have many + // audio frames so seek next unique pos.. + if (l_SPacket.pos != l_lLastPacketPos) { + m_bUnique = true; + l_lLastPacketPos = l_SPacket.pos; + } + + // If we are over last storepos and we have read more than jump point need is and pos is unique we store it to memory + if (m_lCacheBytePos > m_lLastStoredPos && + m_lCacheBytePos > (AUDIOSOURCEFFMPEG_POSDISTANCE + m_lLastStoredPos) && + m_bUnique == true) { + struct ffmpegLocationObject *l_SJmp = (struct ffmpegLocationObject *)malloc( + sizeof(struct ffmpegLocationObject)); + m_lLastStoredPos = m_lCacheBytePos; + l_SJmp->startByte = m_lCacheBytePos / 2; + l_SJmp->pos = l_SPacket.pos; + l_SJmp->pts = l_SPacket.pts; + m_SJumpPoints.append(l_SJmp); + m_bUnique = false; + } + + if (offset < 0 || (quint64) offset <= (m_lCacheBytePos / 2)) { + l_iCount --; + } + } else { + free(l_SObj); + l_SObj = NULL; + qDebug() << + "AudioSourceFFmpeg::readFramesToCache: General error in audio decode:" << + l_iRet; + } + } + + av_free_packet(&l_SPacket); + l_SPacket.data = NULL; + l_SPacket.size = 0; + + } else { + l_iError ++; + if (l_iError == 5) { + // Stream end and we couldn't read enough frames + l_iStop = true; + } + } + + + } else { + qDebug() << "AudioSourceFFmpeg::readFramesToCache: Packet too big or File end"; + l_iStop = true; + } + + } + + if (l_pFrame != NULL) { + l_iFrameCount --; +// FFMPEG 2.2 3561060 anb beyond +#if LIBAVCODEC_VERSION_INT >= 3561060 + av_frame_unref(l_pFrame); + av_frame_free(&l_pFrame); +// FFMPEG 0.11 and below +#elif LIBAVCODEC_VERSION_INT <= 3544932 + av_free(l_pFrame); +// FFMPEG 1.0 - 2.1 +#else + avcodec_free_frame(&l_pFrame); +#endif + l_pFrame = NULL; + + } + + if (l_iFrameCount > 0) { + qDebug() << "AudioSourceFFmpeg::readFramesToCache(): Frame balance is not 0 it is: " << l_iFrameCount; + } + + l_SObj = m_SCache.first(); + m_lCacheStartByte = l_SObj->startByte; + l_SObj = m_SCache.last(); + m_lCacheEndByte = (l_SObj->startByte + l_SObj->length); + + if (!l_iCount) { + return true; + } else { + return false; + } +} + +bool AudioSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, + quint64 size) { + struct ffmpegCacheObject *l_SObj = NULL; + quint32 l_lPos = 0; + quint32 l_lLeft = 0; + quint32 l_lOffset = 0; + quint32 l_lBytesToCopy = 0; + + if (offset >= m_lCacheStartByte) { + if (m_lCacheLastPos == 0) { + m_lCacheLastPos = m_SCache.size() - 1; + } + for (l_lPos = m_lCacheLastPos; l_lPos > 0; l_lPos --) { + l_SObj = m_SCache[l_lPos]; + if ((l_SObj->startByte + l_SObj->length) < offset) { + break; + } + } + + l_SObj = m_SCache[l_lPos]; + + l_lLeft = (size * 2); + memset(buffer, 0x00, l_lLeft); + while (l_lLeft > 0) { + + if (l_SObj == NULL || (l_lPos + 5) > (unsigned int)m_SCache.size()) { + offset = l_SObj->startByte; + if (readFramesToCache(50, -1) == false) { + return false; + } + for (l_lPos = (m_SCache.size() - 50); l_lPos > 0; l_lPos --) { + l_SObj = m_SCache[l_lPos]; + if ((l_SObj->startByte + l_SObj->length) < offset) { + break; + } + } + l_SObj = m_SCache[l_lPos]; + continue; + } + + if (l_SObj->startByte <= offset) { + l_lOffset = (offset - l_SObj->startByte) * 2; + } + + if (l_lOffset >= (l_SObj->length * 2)) { + l_SObj = m_SCache[++ l_lPos]; + continue; + } + + if (l_lLeft > (l_SObj->length * 2)) { + l_lBytesToCopy = ((l_SObj->length * 2) - l_lOffset); + memcpy(buffer, (l_SObj->bytes + l_lOffset), l_lBytesToCopy); + l_lOffset = 0; + buffer += l_lBytesToCopy; + l_lLeft -= l_lBytesToCopy; + } else { + memcpy(buffer, l_SObj->bytes, l_lLeft); + l_lLeft = 0; + } + + l_SObj = m_SCache[++ l_lPos]; + } + + m_lCacheLastPos = --l_lPos; + return true; + } + + return false; +} + +AudioSource::diff_type AudioSourceFFmpeg::seekFrame(diff_type frameIndex) { + const diff_type filepos = frames2samples(frameIndex); + + int ret = 0; + qint64 i = 0; + + if (filepos < 0 || (unsigned long) filepos < m_lCacheStartByte) { + ret = avformat_seek_file(m_pFormatCtx, + m_iAudioStream, + 0, + 32767 * 2, + 32767 * 2, + AVSEEK_FLAG_BACKWARD); + + + if (ret < 0) { + qDebug() << "AudioSourceFFmpeg::seek: Can't seek to 0 byte!"; + return -1; + } + + clearCache(); + m_lCacheStartByte = 0; + m_lCacheEndByte = 0; + m_lCacheLastPos = 0; + m_lCacheBytePos = 0; + m_lStoredSeekPoint = -1; + + + // Try to find some jump point near to + // where we are located so we don't needed + // to try guess it + if (filepos >= AUDIOSOURCEFFMPEG_POSDISTANCE) { + for (i = 0; i < m_SJumpPoints.size(); i ++) { + if (m_SJumpPoints[i]->startByte >= (unsigned long) filepos && i > 2) { + m_lCacheBytePos = m_SJumpPoints[i - 2]->startByte * 2; + m_lStoredSeekPoint = m_SJumpPoints[i - 2]->pos; + break; + } + } + } + + if (filepos == 0) { + readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE - 50), -1); + } else { + readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE / 2), filepos); + } + } + + + if (m_lCacheEndByte <= (unsigned long) filepos) { + readFramesToCache(100, filepos); + } + + m_iCurrentMixxTs = filepos; + + m_bIsSeeked = TRUE; + + return frameIndex; +} + +unsigned int AudioSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { + + if (m_SCache.size() == 0) { + // Make sure we allways start at begining and cache have some + // material that we can consume. + seekFrame(0); + m_bIsSeeked = FALSE; + } + + getBytesFromCache((char *)destination, m_iCurrentMixxTs, size); + + // As this is also Hack + // If we don't seek like we don't on analyzer.. keep + // place in mind.. + if (m_bIsSeeked == FALSE) { + m_iCurrentMixxTs += size; + } + + m_bIsSeeked = FALSE; + return size; +} + +AudioSource::size_type AudioSourceFFmpeg::readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer) { + // This is just a hack that simply reuses existing + // functionality. Sample data should be resampled + // directly into AV_SAMPLE_FMT_FLT instead of + // AV_SAMPLE_FMT_S16! + typedef std::vector TempBuffer; + TempBuffer tempBuffer(frames2samples(frameCount)); + const size_type readSamples = read(tempBuffer.size(), &tempBuffer[0]); + for (size_type i = 0; i < readSamples; ++i) { + sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / sample_type(SAMPLE_MAX); + } + return samples2frames(readSamples); +} + +} // namespace Mixxx diff --git a/src/audiosourceffmpeg.h b/src/audiosourceffmpeg.h new file mode 100644 index 00000000000..8bed1627d9b --- /dev/null +++ b/src/audiosourceffmpeg.h @@ -0,0 +1,93 @@ +#ifndef AUDIOSOURCEFFMPEG_H +#define AUDIOSOURCEFFMPEG_H + +#include "audiosource.h" +#include "util/defs.h" + +#include + +// Needed to ensure that macros in get defined. +#ifndef __STDC_CONSTANT_MACROS +#if __cplusplus < 201103L +#define __STDC_CONSTANT_MACROS +#endif +#endif + +#include +#include + +#ifndef __FFMPEGOLDAPI__ +#include +#include +#endif + +// Compability +#include +#include + +#include + +namespace Mixxx { + +struct ffmpegLocationObject { + quint64 pos; + qint64 pts; + quint64 startByte; +}; + +struct ffmpegCacheObject { + quint64 startByte; + quint32 length; + quint8 *bytes; +}; + +class AudioSourceFFmpeg : public AudioSource { + typedef AudioSource Super; + +public: + static AudioSourcePointer open(QString fileName); + + ~AudioSourceFFmpeg(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /*override*/; + +private: + AudioSourceFFmpeg(); + + Result postConstruct(QString fileName); + + bool readFramesToCache(unsigned int count, qint64 offset); + bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); + quint64 getSizeofCache(); + void clearCache() throw(); + + unsigned int read(unsigned long size, SAMPLE*); + + AVFormatContext *m_pFormatCtx; + int m_iAudioStream; + AVCodecContext *m_pCodecCtx; + AVCodec *m_pCodec; + + EncoderFfmpegResample *m_pResample; + + qint64 m_iCurrentMixxTs; + + bool m_bIsSeeked; + + quint64 m_lCacheBytePos; + quint64 m_lCacheStartByte; + quint64 m_lCacheEndByte; + quint32 m_lCacheLastPos; + QVector m_SCache; + QVector m_SJumpPoints; + quint64 m_lLastStoredPos; + qint64 m_lStoredSeekPoint; +}; + +} + +#endif diff --git a/src/audiosourceflac.cpp b/src/audiosourceflac.cpp new file mode 100644 index 00000000000..c8d7234c8d3 --- /dev/null +++ b/src/audiosourceflac.cpp @@ -0,0 +1,352 @@ +#include "audiosourceflac.h" + +#include "sampleutil.h" +#include "util/math.h" + +#include + +namespace Mixxx +{ + +namespace +{ +// begin callbacks (have to be regular functions because normal libFLAC isn't C++-aware) + +FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder*, + FLAC__byte buffer[], size_t *bytes, void *client_data) { + return static_cast(client_data)->flacRead(buffer, bytes); +} + +FLAC__StreamDecoderSeekStatus FLAC_seek_cb(const FLAC__StreamDecoder*, + FLAC__uint64 absolute_byte_offset, void *client_data) { + return static_cast(client_data)->flacSeek(absolute_byte_offset); +} + +FLAC__StreamDecoderTellStatus FLAC_tell_cb(const FLAC__StreamDecoder*, + FLAC__uint64 *absolute_byte_offset, void *client_data) { + return static_cast(client_data)->flacTell(absolute_byte_offset); +} + +FLAC__StreamDecoderLengthStatus FLAC_length_cb(const FLAC__StreamDecoder*, + FLAC__uint64 *stream_length, void *client_data) { + return static_cast(client_data)->flacLength(stream_length); +} + +FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder*, void *client_data) { + return static_cast(client_data)->flacEOF(); +} + +FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder*, + const FLAC__Frame *frame, const FLAC__int32 * const buffer[], + void *client_data) { + return static_cast(client_data)->flacWrite(frame, buffer); +} + +void FLAC_metadata_cb(const FLAC__StreamDecoder*, + const FLAC__StreamMetadata *metadata, void *client_data) { + static_cast(client_data)->flacMetadata(metadata); +} + +void FLAC_error_cb(const FLAC__StreamDecoder*, + FLAC__StreamDecoderErrorStatus status, void *client_data) { + static_cast(client_data)->flacError(status); +} + +// end callbacks +} + +AudioSourceFLAC::AudioSourceFLAC(QString fileName) + : m_file(fileName), m_decoder(NULL), m_minBlocksize( + 0), m_maxBlocksize(0), m_minFramesize(0), m_maxFramesize(0), m_sampleScale( + kSampleValueZero), m_decodeSampleBufferReadOffset(0), m_decodeSampleBufferWriteOffset( + 0) { +} + +AudioSourceFLAC::~AudioSourceFLAC() { + close(); +} + +AudioSourcePointer AudioSourceFLAC::open(QString fileName) { + AudioSourceFLAC* pAudioSourceFLAC(new AudioSourceFLAC(fileName)); + AudioSourcePointer pAudioSource(pAudioSourceFLAC); // take ownership + if (OK == pAudioSourceFLAC->postConstruct()) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceFLAC::postConstruct() { + if (!m_file.open(QIODevice::ReadOnly)) { + qWarning() << "SSFLAC: Could not read file!"; + return ERR; + } + + m_decoder = FLAC__stream_decoder_new(); + if (m_decoder == NULL) { + qWarning() << "SSFLAC: decoder allocation failed!"; + return ERR; + } + FLAC__stream_decoder_set_md5_checking(m_decoder, FALSE); + const FLAC__StreamDecoderInitStatus initStatus( + FLAC__stream_decoder_init_stream(m_decoder, FLAC_read_cb, + FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, + FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); + if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { + qWarning() << "SSFLAC: decoder init failed:" << initStatus; + return ERR; + } + if (!FLAC__stream_decoder_process_until_end_of_metadata(m_decoder)) { + qWarning() << "SSFLAC: process to end of meta failed:" + << FLAC__stream_decoder_get_state(m_decoder); + return ERR; + } + + return OK; +} + +void AudioSourceFLAC::close() throw() { + if (m_decoder) { + FLAC__stream_decoder_finish(m_decoder); + FLAC__stream_decoder_delete(m_decoder); // frees memory + m_decoder = NULL; + } + m_decodeSampleBuffer.clear(); + m_file.close(); + Super::reset(); +} + +Mixxx::AudioSource::diff_type AudioSourceFLAC::seekFrame(diff_type frameIndex) { + // clear decode buffer before seeking + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + bool result = FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex); + if (!result) { + qWarning() << "SSFLAC: Seeking error at file" << m_file.fileName(); + } + return frameIndex; +} + +Mixxx::AudioSource::size_type AudioSourceFLAC::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); +} + +Mixxx::AudioSource::size_type AudioSourceFLAC::readStereoFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); +} + +Mixxx::AudioSource::size_type AudioSourceFLAC::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer, + bool readStereoSamples) { + sample_type* outBuffer = sampleBuffer; + size_type framesRemaining = frameCount; + while (0 < framesRemaining) { + Q_ASSERT( + m_decodeSampleBufferReadOffset + <= m_decodeSampleBufferWriteOffset); + // if our buffer from libflac is empty (either because we explicitly cleared + // it or because we've simply used all the samples), ask for a new buffer + if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + if (FLAC__stream_decoder_process_single(m_decoder)) { + if (m_decodeSampleBufferReadOffset + >= m_decodeSampleBufferWriteOffset) { + // EOF + break; + } + } else { + qWarning() << "SSFLAC: decoder_process_single returned false (" + << m_file.fileName() << ")"; + break; + } + } + Q_ASSERT( + m_decodeSampleBufferReadOffset + <= m_decodeSampleBufferWriteOffset); + const size_type decodeBufferSamples = m_decodeSampleBufferWriteOffset + - m_decodeSampleBufferReadOffset; + const size_type decodeBufferFrames = samples2frames( + decodeBufferSamples); + const size_type framesToCopy = math_min(framesRemaining, decodeBufferFrames); + const size_type samplesToCopy = frames2samples(framesToCopy); + if (readStereoSamples && !isChannelCountStereo()) { + if (isChannelCountMono()) { + SampleUtil::copyMonoToDualMono(outBuffer, + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy); + } else { + SampleUtil::copyMultiToStereo(outBuffer, + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy, getChannelCount()); + } + outBuffer += framesToCopy * 2; // copied 2 samples per frame + } else { + SampleUtil::copy(outBuffer, + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + samplesToCopy); + outBuffer += samplesToCopy; + } + m_decodeSampleBufferReadOffset += samplesToCopy; + framesRemaining -= framesToCopy; + Q_ASSERT( + m_decodeSampleBufferReadOffset + <= m_decodeSampleBufferWriteOffset); + } + return frameCount - framesRemaining; +} + +// flac callback methods +FLAC__StreamDecoderReadStatus AudioSourceFLAC::flacRead(FLAC__byte buffer[], + size_t *bytes) { + *bytes = m_file.read((char*) buffer, *bytes); + if (*bytes > 0) { + return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; + } else if (*bytes == 0) { + return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; + } else { + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } +} + +FLAC__StreamDecoderSeekStatus AudioSourceFLAC::flacSeek(FLAC__uint64 offset) { + if (m_file.seek(offset)) { + return FLAC__STREAM_DECODER_SEEK_STATUS_OK; + } else { + qWarning() << "SSFLAC: An unrecoverable error occurred (" + << m_file.fileName() << ")"; + return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR; + } +} + +FLAC__StreamDecoderTellStatus AudioSourceFLAC::flacTell(FLAC__uint64 *offset) { + if (m_file.isSequential()) { + return FLAC__STREAM_DECODER_TELL_STATUS_UNSUPPORTED; + } + *offset = m_file.pos(); + return FLAC__STREAM_DECODER_TELL_STATUS_OK; +} + +FLAC__StreamDecoderLengthStatus AudioSourceFLAC::flacLength( + FLAC__uint64 *length) { + if (m_file.isSequential()) { + return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; + } + *length = m_file.size(); + return FLAC__STREAM_DECODER_LENGTH_STATUS_OK; +} + +FLAC__bool AudioSourceFLAC::flacEOF() { + if (m_file.isSequential()) { + return false; + } + return m_file.atEnd(); +} + +FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( + const FLAC__Frame *frame, const FLAC__int32 * const buffer[]) { + if (getChannelCount() != frame->header.channels) { + qWarning() << "Invalid number of channels in FLAC frame header:" + << "expected" << getChannelCount() << "actual" + << frame->header.channels; + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; + } + Q_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + Q_ASSERT( + (m_decodeSampleBuffer.size() - m_decodeSampleBufferWriteOffset) + >= frames2samples(frame->header.blocksize)); + switch (getChannelCount()) { + case 1: { + // optimized code for 1 channel (mono) + Q_ASSERT(1 <= frame->header.channels); + for (unsigned i = 0; i < frame->header.blocksize; ++i) { + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[0][i] * m_sampleScale; + } + break; + } + case 2: { + // optimized code for 2 channels (stereo) + Q_ASSERT(2 <= frame->header.channels); + for (unsigned i = 0; i < frame->header.blocksize; ++i) { + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[0][i] * m_sampleScale; + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[1][i] * m_sampleScale; + } + break; + } + default: { + // generic code for multiple channels + for (unsigned i = 0; i < frame->header.blocksize; ++i) { + for (unsigned j = 0; j < frame->header.channels; ++j) { + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[j][i] * m_sampleScale; + } + } + } + } + Q_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; +} + +void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata *metadata) { + switch (metadata->type) { + case FLAC__METADATA_TYPE_STREAMINFO: + setChannelCount(metadata->data.stream_info.channels); + setFrameRate(metadata->data.stream_info.sample_rate); + setFrameCount(metadata->data.stream_info.total_samples); + m_sampleScale = kSampleValuePeak + / sample_type( + FLAC__int32(1) + << metadata->data.stream_info.bits_per_sample); + qDebug() << "FLAC file " << m_file.fileName(); + qDebug() << getChannelCount() << " @ " << getFrameRate() << " Hz, " + << getFrameCount() << " total, " << "bit depth" + << " metadata->data.stream_info.bits_per_sample"; + m_minBlocksize = metadata->data.stream_info.min_blocksize; + m_maxBlocksize = metadata->data.stream_info.max_blocksize; + m_minFramesize = metadata->data.stream_info.min_framesize; + m_maxFramesize = metadata->data.stream_info.max_framesize; + qDebug() << "Blocksize in [" << m_minBlocksize << ", " << m_maxBlocksize + << "], Framesize in [" << m_minFramesize << ", " + << m_maxFramesize << "]"; + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + m_decodeSampleBuffer.resize(m_maxBlocksize * getChannelCount()); + break; + default: + break; + } +} + +void AudioSourceFLAC::flacError(FLAC__StreamDecoderErrorStatus status) { + QString error; + // not much can be done at this point -- luckly the decoder seems to be + // pretty forgiving -- bkgood + switch (status) { + case FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC: + error = "STREAM_DECODER_ERROR_STATUS_LOST_SYNC"; + break; + case FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER: + error = "STREAM_DECODER_ERROR_STATUS_BAD_HEADER"; + break; + case FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH: + error = "STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH"; + break; + case FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM: + error = "STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM"; + break; + } + qWarning() << "SSFLAC got error" << error << "from libFLAC for file" + << m_file.fileName(); + // not much else to do here... whatever function that initiated whatever + // decoder method resulted in this error will return an error, and the caller + // will bail. libFLAC docs say to not close the decoder here -- bkgood +} + +} // namespace Mixxx diff --git a/src/audiosourceflac.h b/src/audiosourceflac.h new file mode 100644 index 00000000000..fb1c7a74c65 --- /dev/null +++ b/src/audiosourceflac.h @@ -0,0 +1,70 @@ +#ifndef AUDIOSOURCEFLAC_H +#define AUDIOSOURCEFLAC_H + +#include "audiosource.h" +#include "util/defs.h" + +#include + +#include + +#include + +namespace Mixxx +{ + +class AudioSourceFLAC : public AudioSource { + typedef AudioSource Super; + +public: + static AudioSourcePointer open(QString fileName); + + ~AudioSourceFLAC(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /* override*/; + + // callback methods + FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t *bytes); + FLAC__StreamDecoderSeekStatus flacSeek(FLAC__uint64 offset); + FLAC__StreamDecoderTellStatus flacTell(FLAC__uint64 *offset); + FLAC__StreamDecoderLengthStatus flacLength(FLAC__uint64 *length); + FLAC__bool flacEOF(); + FLAC__StreamDecoderWriteStatus flacWrite(const FLAC__Frame *frame, const FLAC__int32 *const buffer[]); + void flacMetadata(const FLAC__StreamMetadata *metadata); + void flacError(FLAC__StreamDecoderErrorStatus status); + +private: + explicit AudioSourceFLAC(QString fileName); + + Result postConstruct(); + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); + + QFile m_file; + + FLAC__StreamDecoder *m_decoder; + // misc bits about the flac format: + // flac encodes from and decodes to LPCM in blocks, each block is made up of + // subblocks (one for each chan) + // flac stores in 'frames', each of which has a header and a certain number + // of subframes (one for each channel) + size_type m_minBlocksize; // in time samples (audio samples = time samples * chanCount) + size_type m_maxBlocksize; + size_type m_minFramesize; + size_type m_maxFramesize; + sample_type m_sampleScale; + + typedef std::vector SampleBuffer; + std::vector m_decodeSampleBuffer; + SampleBuffer::size_type m_decodeSampleBufferReadOffset; + SampleBuffer::size_type m_decodeSampleBufferWriteOffset; +}; + +} + +#endif // ifndef AUDIOSOURCEFLAC_H diff --git a/src/audiosourcemodplug.cpp b/src/audiosourcemodplug.cpp new file mode 100644 index 00000000000..de4b497dc59 --- /dev/null +++ b/src/audiosourcemodplug.cpp @@ -0,0 +1,157 @@ +#include "audiosourcemodplug.h" + +#include "util/timer.h" + +#include +#include + +#include +#include + +/* read files in 512k chunks */ +#define CHUNKSIZE (1 << 18) + +namespace Mixxx +{ + +// reserve some static space for settings... +namespace +{ +// identification of modplug module type +enum ModuleTypes { + NONE = 0x00, + MOD = 0x01, + S3M = 0x02, + XM = 0x04, + MED = 0x08, + IT = 0x20, + STM = 0x100, + OKT = 0x8000 +}; +} + +unsigned int AudioSourceModPlug::s_bufferSizeLimit = 0; + +void AudioSourceModPlug::configure( + unsigned int bufferSizeLimit, + const ModPlug::ModPlug_Settings &settings) { + s_bufferSizeLimit = bufferSizeLimit; + ModPlug::ModPlug_SetSettings(&settings); +} + +AudioSourceModPlug::AudioSourceModPlug() + : m_pModFile(NULL), m_fileLength(0), m_seekPos(0) { +} + +AudioSourceModPlug::~AudioSourceModPlug() { + close(); +} + +AudioSourcePointer AudioSourceModPlug::open(QString fileName) { + AudioSourceModPlug* pAudioSourceModPlug(new AudioSourceModPlug); + AudioSourcePointer pAudioSource(pAudioSourceModPlug); // take ownership + if (OK == pAudioSourceModPlug->postConstruct(fileName)) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceModPlug::postConstruct(QString fileName) { + ScopedTimer t("AudioSourceModPlug::open()"); + + qDebug() << "[ModPlug] Loading ModPlug module " << fileName; + + // read module file to byte array + QFile modFile(fileName); + modFile.open(QIODevice::ReadOnly); + m_fileBuf = modFile.readAll(); + modFile.close(); + // get ModPlugFile descriptor for later access + m_pModFile = ModPlug::ModPlug_Load(m_fileBuf.constData(), + m_fileBuf.length()); + + if (m_pModFile == NULL) { + // an error occured + t.cancel(); + qDebug() << "[ModPlug] Could not load module file: " << fileName; + return ERR; + } + + // estimate size of sample buffer (for better performance) + // beware: module length estimation is unreliable due to loops + // song milliseconds * 2 (bytes per sample) + // * 2 (channels) + // * 44.1 (samples per millisecond) + // + some more to accomodate short loops etc. + // approximate and align with CHUNKSIZE yields: + // (((milliseconds << 1) >> 10 /* to seconds */) + // div 11 /* samples to chunksize ratio */) + // << 19 /* align to chunksize */ + unsigned int estimate = ((ModPlug::ModPlug_GetLength(m_pModFile) >> 8) / 11) << 18; + estimate = math_min(estimate, s_bufferSizeLimit); + m_sampleBuf.reserve(estimate); + qDebug() << "[ModPlug] Reserved " << m_sampleBuf.capacity() + << " #samples"; + + // decode samples to sample buffer + int samplesRead = -1; + int currentSize = 0; + while ((samplesRead != 0) && (m_sampleBuf.size() < s_bufferSizeLimit)) { + // reserve enough space in sample buffer + m_sampleBuf.resize(currentSize + CHUNKSIZE); + samplesRead = ModPlug::ModPlug_Read(m_pModFile, + m_sampleBuf.data() + currentSize, + CHUNKSIZE * 2) / 2; + // adapt to actual size + currentSize += samplesRead; + if (samplesRead != CHUNKSIZE) { + m_sampleBuf.resize(currentSize); + samplesRead = 0; // we reached the end of the file + } + } + qDebug() << "[ModPlug] Filled Sample buffer with " << m_sampleBuf.size() + << " samples."; + qDebug() << "[ModPlug] Sample buffer has " + << m_sampleBuf.capacity() - m_sampleBuf.size() + << " samples unused capacity."; + + setChannelCount(kChannelCount); + setFrameRate(kFrameRate); + setFrameCount(samples2frames(m_sampleBuf.size())); + m_seekPos = 0; + + return OK; +} + +void AudioSourceModPlug::close() throw() { + if (m_pModFile) { + ModPlug::ModPlug_Unload(m_pModFile); + m_pModFile = NULL; + } + Super::reset(); +} + +AudioSource::diff_type AudioSourceModPlug::seekFrame( + diff_type frameIndex) { + return m_seekPos = frameIndex; +} + +AudioSource::size_type AudioSourceModPlug::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + const size_type maxFrames = samples2frames(m_sampleBuf.size()); + const size_type readFrames = math_min(maxFrames - m_seekPos, frameCount); + + const size_type readSamples = frames2samples(readFrames); + const size_type readOffset = frames2samples(m_seekPos); + for (size_type i = 0; i < readSamples; ++i) { + sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) / sample_type(SAMPLE_MAX); + } + + m_seekPos += readFrames; + return readFrames; +} + +} // namespace Mixxx diff --git a/src/audiosourcemodplug.h b/src/audiosourcemodplug.h new file mode 100644 index 00000000000..fe256719c55 --- /dev/null +++ b/src/audiosourcemodplug.h @@ -0,0 +1,56 @@ +#ifndef AUDIOSOURCEMODPLUG_H +#define AUDIOSOURCEMODPLUG_H + +#include "audiosource.h" +#include "util/defs.h" + +namespace ModPlug { +#include +} + +#include + +namespace Mixxx +{ + +// Class for reading tracker files using libmodplug. +// The whole file is decoded at once and saved +// in RAM to allow seeking and smooth operation in Mixxx. +class AudioSourceModPlug: public AudioSource { + typedef AudioSource Super; + +public: + static const size_type kChannelCount = 2; // always stereo + static const size_type kFrameRate = 44100; // always 44.1kHz + + // apply settings for decoding + static void configure(unsigned int bufferSizeLimit, + const ModPlug::ModPlug_Settings &settings); + + static AudioSourcePointer open(QString fileName); + + ~AudioSourceModPlug(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /* override*/; + +private: + static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) + + AudioSourceModPlug(); + + Result postConstruct(QString fileName); + + ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor + unsigned long m_fileLength; // length of file in samples + unsigned long m_seekPos; // current read position + QByteArray m_fileBuf; // original module file data + std::vector m_sampleBuf; // 16bit stereo samples, 44.1kHz +}; + +} + +#endif diff --git a/src/audiosourcemp3.cpp b/src/audiosourcemp3.cpp new file mode 100644 index 00000000000..9b28ed40bae --- /dev/null +++ b/src/audiosourcemp3.cpp @@ -0,0 +1,370 @@ +#include "audiosourcemp3.h" + +#include "util/math.h" + +#include + +namespace Mixxx +{ + +namespace +{ + const AudioSource::size_type kSeekFramePrefetchCount = 2; // required for synchronization + + const AudioSource::size_type kMaxSamplesPerMp3Frame = 1152; + + const AudioSource::diff_type kMaxSkipFrameSamplesWhenSeeking = 2 * kSeekFramePrefetchCount * kMaxSamplesPerMp3Frame; + + const AudioSource::sample_type kMadScale = + AudioSource::kSampleValuePeak + / AudioSource::sample_type( + mad_fixed_t(1) << MAD_F_FRACBITS); + + inline AudioSource::sample_type madScale(mad_fixed_t sample) { + return sample * kMadScale; + } + + // Optimization: Reserve initial capacity for seek frame list + const AudioSource::size_type kMinutesPerFile = 10; // enough for the majority of files (tunable) + const AudioSource::size_type kSecondsPerMinute = 60; // fixed + const AudioSource::size_type kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 + const AudioSource::size_type kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; + + bool mad_skip_id3_tag(mad_stream* pStream) { + long tagsize = id3_tag_query(pStream->this_frame, + pStream->bufend - pStream->this_frame); + if (0 < tagsize) { + mad_stream_skip(pStream, tagsize); + return true; + } else { + return false; + } + } +} + +AudioSourceMp3::AudioSourceMp3(QString fileName) + : m_file(fileName), + m_fileSize(0), + m_pFileData(NULL), + m_avgSeekFrameCount(0), + m_curFrameIndex(0), + m_madSynthCount(0) { + mad_stream_init(&m_madStream); + mad_frame_init(&m_madFrame); + mad_synth_init(&m_madSynth); +} + +AudioSourceMp3::~AudioSourceMp3() { + close(); +} + +AudioSourcePointer AudioSourceMp3::open(QString fileName) { + AudioSourceMp3* pAudioSourceMp3(new AudioSourceMp3(fileName)); + AudioSourcePointer pAudioSource(pAudioSourceMp3); // take ownership + if (OK == pAudioSourceMp3->postConstruct()) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceMp3::postConstruct() { + if (!m_file.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file:" << m_file.fileName(); + return ERR; + } + + // Get a pointer to the file using memory mapped IO + m_fileSize = m_file.size(); + m_pFileData = m_file.map(0, m_fileSize); + + // Transfer it to the mad stream-buffer: + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); + + m_seekFrameList.reserve(kSeekFrameListCapacity); + + // Decode all the headers and calculate audio properties + unsigned long sumBitrate = 0; + unsigned int madFrameCount = 0; + setChannelCount(kChannelCountDefault); + setFrameRate(kFrameRateDefault); + mad_timer_t madFileDuration = mad_timer_zero; + mad_units madUnits; + while ((m_madStream.bufend - m_madStream.this_frame) > 0) { + mad_header madHeader; + mad_header_init(&madHeader); + if (0 != mad_header_decode(&madHeader, &m_madStream)) { + if (MAD_RECOVERABLE(m_madStream.error)) { + if (MAD_ERROR_LOSTSYNC == m_madStream.error) { + // Ignore LOSTSYNC due to ID3 tags + mad_skip_id3_tag(&m_madStream); + } else { + qDebug() << "Recoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); + } + mad_header_finish(&madHeader); + continue; + } else { + if (MAD_ERROR_BUFLEN == m_madStream.error) { + // EOF + break; // done + } else { + qWarning() << "Unrecoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); + mad_header_finish(&madHeader); + return ERR; // abort + } + } + } + + // Grab data from madHeader + const size_type madChannelCount = MAD_NCHANNELS(&madHeader); + if (kChannelCountDefault == getChannelCount()) { + // initially set the number of channels + setChannelCount(madChannelCount); + } else { + // check for consistent number of channels + if ((0 < madChannelCount) && getChannelCount() != madChannelCount) { + qWarning() << "Differing number of channels in some headers:" + << m_file.fileName() << getChannelCount() << "<>" + << madChannelCount; + } + } + const size_type madSampleRate = madHeader.samplerate; + if (kFrameRateDefault == getFrameRate()) { + // initially set the frame/sample rate + switch (madSampleRate) { + case 8000: + madUnits = MAD_UNITS_8000_HZ; + break; + case 11025: + madUnits = MAD_UNITS_11025_HZ; + break; + case 12000: + madUnits = MAD_UNITS_12000_HZ; + break; + case 16000: + madUnits = MAD_UNITS_16000_HZ; + break; + case 22050: + madUnits = MAD_UNITS_22050_HZ; + break; + case 24000: + madUnits = MAD_UNITS_24000_HZ; + break; + case 32000: + madUnits = MAD_UNITS_32000_HZ; + break; + case 44100: + madUnits = MAD_UNITS_44100_HZ; + break; + case 48000: + madUnits = MAD_UNITS_48000_HZ; + break; + default: + qWarning() << "Invalid sample rate:" + << m_file.fileName() << madSampleRate; + return ERR; // abort + } + setFrameRate(madSampleRate); + } else { + // check for consistent frame/sample rate + if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { + qWarning() << "Differing sample rate in some headers:" + << m_file.fileName() << getFrameRate() << "<>" + << madSampleRate; + return ERR; // abort + } + } + + // Add frame to list of frames + MadSeekFrameType seekFrame; + seekFrame.pFileData = m_madStream.this_frame; + seekFrame.frameIndex = mad_timer_count(madFileDuration, madUnits); + m_seekFrameList.push_back(seekFrame); + + mad_timer_add(&madFileDuration, madHeader.duration); + sumBitrate += madHeader.bitrate; + + mad_header_finish(&madHeader); + + ++madFrameCount; + } + + if (0 < madFrameCount) { + setFrameCount(mad_timer_count(madFileDuration, madUnits)); + m_avgSeekFrameCount = getFrameCount() / madFrameCount; + int avgBitrate = sumBitrate / madFrameCount; + setBitrate(avgBitrate); + } else { + // This is not a working MP3 file. + qWarning() << "SSMP3: This is not a working MP3 file:" << m_file.fileName(); + return ERR; // abort + } + + // restart decoding + m_curFrameIndex = getFrameCount(); + seekFrame(0); + + return OK; +} + +void AudioSourceMp3::close() throw() { + m_seekFrameList.clear(); + m_avgSeekFrameCount = 0; + m_curFrameIndex = 0; + + mad_synth_finish(&m_madSynth); + mad_frame_finish(&m_madFrame); + mad_stream_finish(&m_madStream); + m_madSynthCount = 0; + + m_file.unmap(m_pFileData); + m_fileSize = 0; + m_pFileData = NULL; + m_file.close(); + + Super::reset(); +} + +AudioSourceMp3::MadSeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { + if ((0 >= frameIndex) || m_seekFrameList.empty()) { + return 0; + } + // Guess position of frame in m_seekFrameList based on average frame size + AudioSourceMp3::MadSeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; + if (seekFrameIndex >= m_seekFrameList.size()) { + seekFrameIndex = m_seekFrameList.size() - 1; + } + // binary search starting at seekFrameIndex + AudioSourceMp3::MadSeekFrameList::size_type lowerBound = 0; + AudioSourceMp3::MadSeekFrameList::size_type upperBound = m_seekFrameList.size(); + while ((upperBound - lowerBound) > 1) { + Q_ASSERT(seekFrameIndex >= lowerBound); + Q_ASSERT(seekFrameIndex < upperBound); + if (m_seekFrameList[seekFrameIndex].frameIndex > frameIndex) { + upperBound = seekFrameIndex; + } else { + lowerBound = seekFrameIndex; + } + seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; + } + Q_ASSERT(m_seekFrameList.size() > seekFrameIndex); + Q_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); + Q_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); + return seekFrameIndex; +} + +AudioSource::diff_type AudioSourceMp3::seekFrame(diff_type frameIndex) { + if (m_curFrameIndex == frameIndex) { + return m_curFrameIndex; + } + if (0 > frameIndex) { + return seekFrame(0); + } + // simply skip frames when jumping no more than kMaxSkipFrameSamplesWhenSeeking frames forward + if ((frameIndex < m_curFrameIndex) || ((frameIndex - m_curFrameIndex) > kMaxSkipFrameSamplesWhenSeeking)) { + MadSeekFrameList::size_type seekFrameIndex = findSeekFrameIndex(frameIndex); + if (seekFrameIndex <= kSeekFramePrefetchCount) { + seekFrameIndex = 0; + } else { + seekFrameIndex -= kSeekFramePrefetchCount; + } + Q_ASSERT(seekFrameIndex < m_seekFrameList.size()); + const MadSeekFrameType& seekFrame(m_seekFrameList[seekFrameIndex]); + // restart decoder + mad_synth_finish(&m_madSynth); + mad_frame_finish(&m_madFrame); + mad_stream_finish(&m_madStream); + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + mad_stream_buffer(&m_madStream, seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); + mad_frame_init(&m_madFrame); + mad_synth_init(&m_madSynth); + m_curFrameIndex = seekFrame.frameIndex; + m_madSynthCount = 0; + } + // decode and discard prefetch data + Q_ASSERT(m_curFrameIndex <= frameIndex); + skipFrameSamples(frameIndex - m_curFrameIndex); + Q_ASSERT(m_curFrameIndex == frameIndex); + return m_curFrameIndex; +} + +AudioSource::size_type AudioSourceMp3::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); +} + +AudioSource::size_type AudioSourceMp3::readStereoFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); +} + +AudioSource::size_type AudioSourceMp3::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer, + bool readStereoSamples) { + size_type framesRemaining = frameCount; + sample_type* pSampleBuffer = sampleBuffer; + while (0 < framesRemaining) { + if (0 >= m_madSynthCount) { + if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { + if (MAD_RECOVERABLE(m_madStream.error)) { + if (MAD_ERROR_LOSTSYNC == m_madStream.error) { + // Ignore LOSTSYNC due to ID3 tags + mad_skip_id3_tag(&m_madStream); + } else { + qDebug() << "Recoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); + } + continue; + } else { + if (MAD_ERROR_BUFLEN != m_madStream.error) { + qWarning() << "Unrecoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); + } + break; + } + } + /* Once decoded the frame is synthesized to PCM samples. No ERRs + * are reported by mad_synth_frame(); + */ + mad_synth_frame(&m_madSynth, &m_madFrame); + m_madSynthCount = m_madSynth.pcm.length; + } + const size_type madSynthOffset = m_madSynth.pcm.length - m_madSynthCount; + const size_type framesRead = math_min(m_madSynthCount, framesRemaining); + m_madSynthCount -= framesRead; + m_curFrameIndex += framesRead; + framesRemaining -= framesRead; + if (NULL != pSampleBuffer) { + if (isChannelCountMono()) { + for (size_type i = 0; i < framesRead; ++i) { + const sample_type sampleValue = madScale( + m_madSynth.pcm.samples[0][madSynthOffset + i]); + *(pSampleBuffer++) = sampleValue; + if (readStereoSamples) { + *(pSampleBuffer++) = sampleValue; + } + } + } else if (isChannelCountStereo() || readStereoSamples) { + for (size_type i = 0; i < framesRead; ++i) { + *(pSampleBuffer++) = madScale( + m_madSynth.pcm.samples[0][madSynthOffset + i]); + *(pSampleBuffer++) = madScale( + m_madSynth.pcm.samples[1][madSynthOffset + i]); + } + } else { + for (size_type i = 0; i < framesRead; ++i) { + for (size_type j = 0; j < getChannelCount(); ++j) { + *(pSampleBuffer++) = madScale( + m_madSynth.pcm.samples[j][madSynthOffset + i]); + } + } + } + } + } + return frameCount - framesRemaining; +} + +} // namespace Mixxx diff --git a/src/audiosourcemp3.h b/src/audiosourcemp3.h new file mode 100644 index 00000000000..45db6972907 --- /dev/null +++ b/src/audiosourcemp3.h @@ -0,0 +1,82 @@ +#ifndef AUDIOSOURCEMP3_H +#define AUDIOSOURCEMP3_H + +#include "audiosource.h" +#include "util/defs.h" + +#ifdef _MSC_VER + // So mad.h doesn't try to use inline assembly which MSVC doesn't support. + // Notably, FPM_64BIT does not require a 64-bit machine. It merely requires a + // compiler that supports 64-bit types. + #define FPM_64BIT +#endif +#include + +#include + +#include + +namespace Mixxx +{ + +class AudioSourceMp3 : public AudioSource { + typedef AudioSource Super; + +public: + static AudioSourcePointer open(QString fileName); + + ~AudioSourceMp3(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /*override*/; + +private: + explicit AudioSourceMp3(QString fileName); + + Result postConstruct(); + + inline size_type skipFrameSamples(size_type frameCount) { + return readFrameSamplesInterleaved(frameCount, NULL); + } + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); + + QFile m_file; + quint64 m_fileSize; + unsigned char* m_pFileData; + + mad_stream m_madStream; + + /** Struct used to store mad frames for seeking */ + struct MadSeekFrameType { + diff_type frameIndex; + const unsigned char* pFileData; + }; + + /** It is not possible to make a precise seek in an mp3 file without decoding the whole stream. + * To have precise seek within a limited range from the current decode position, we keep track + * of past decodeded frame, and their exact position. If a seek occours and it is within the + * range of frames we keep track of a precise seek occours, otherwise an unprecise seek is performed + */ + typedef std::vector MadSeekFrameList; + MadSeekFrameList m_seekFrameList; // ordered-by frameIndex + size_type m_avgSeekFrameCount; // avg. samples frames per MP3 frame + + /** Returns the position of the frame which was found. The found frame is set to + * the current element in m_qSeekList */ + MadSeekFrameList::size_type findSeekFrameIndex(diff_type frameIndex) const; + + diff_type m_curFrameIndex; + + // current play position + mad_frame m_madFrame; + mad_synth m_madSynth; + size_type m_madSynthCount; // left overs from the previous read +}; + +} + +#endif diff --git a/src/audiosourceoggvorbis.cpp b/src/audiosourceoggvorbis.cpp new file mode 100644 index 00000000000..eb141235cc7 --- /dev/null +++ b/src/audiosourceoggvorbis.cpp @@ -0,0 +1,134 @@ +#include "audiosourceoggvorbis.h" + +#include + +namespace Mixxx +{ + +AudioSourceOggVorbis::AudioSourceOggVorbis() { + memset(&m_vf, 0, sizeof(m_vf)); +} + +AudioSourceOggVorbis::~AudioSourceOggVorbis() { + close(); +} + +AudioSourcePointer AudioSourceOggVorbis::open(QString fileName) { + AudioSourceOggVorbis* pAudioSourceOggVorbis(new AudioSourceOggVorbis); + AudioSourcePointer pAudioSource(pAudioSourceOggVorbis); // take ownership + if (OK == pAudioSourceOggVorbis->postConstruct(fileName)) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceOggVorbis::postConstruct(QString fileName) { + const QByteArray qbaFilename(fileName.toLocal8Bit()); + if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { + qWarning() << "Failed to open OggVorbis file:" << fileName; + return ERR; + } + + if (!ov_seekable(&m_vf)) { + qWarning() << "OggVorbis file is not seekable:" << fileName; + return ERR; + } + + // lookup the ogg's channels and samplerate + const vorbis_info* vi = ov_info(&m_vf, -1); + if (!vi) { + qWarning() << "Failed to read OggVorbis file:" << fileName; + return ERR; + } + setChannelCount(vi->channels); + setFrameRate(vi->rate); + + ogg_int64_t frameCount = ov_pcm_total(&m_vf, -1); + if (0 <= frameCount) { + setFrameCount(frameCount); + } else { + qWarning() << "Failed to read OggVorbis file:" << fileName; + return ERR; + } + + return OK; +} + +void AudioSourceOggVorbis::close() throw() { + const int clearResult = ov_clear(&m_vf); + if (0 != clearResult) { + qWarning() << "Failed to close OggVorbis file" << clearResult; + } + Super::reset(); +} + +AudioSource::diff_type AudioSourceOggVorbis::seekFrame( + diff_type frameIndex) { + const int seekResult = ov_pcm_seek(&m_vf, frameIndex); + if (0 != seekResult) { + qWarning() << "Failed to seek OggVorbis file:" << seekResult; + } + return ov_pcm_tell(&m_vf); +} + +AudioSource::size_type AudioSourceOggVorbis::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); +} + +AudioSource::size_type AudioSourceOggVorbis::readStereoFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); +} + +AudioSource::size_type AudioSourceOggVorbis::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer, + bool readStereoSamples) { + size_type readCount = 0; + sample_type* nextSample = sampleBuffer; + while (readCount < frameCount) { + float** pcmChannels; + int currentSection; + const long readResult = ov_read_float(&m_vf, &pcmChannels, + frameCount - readCount, ¤tSection); + if (0 == readResult) { + // EOF + break; // done + } + if (0 < readResult) { + if (isChannelCountMono()) { + if (readStereoSamples) { + for (long i = 0; i < readResult; ++i) { + *nextSample++ = pcmChannels[0][i]; + *nextSample++ = pcmChannels[0][i]; + } + } else { + for (long i = 0; i < readResult; ++i) { + *nextSample++ = pcmChannels[0][i]; + } + } + } else if (isChannelCountStereo() || readStereoSamples) { + for (long i = 0; i < readResult; ++i) { + *nextSample++ = pcmChannels[0][i]; + *nextSample++ = pcmChannels[1][i]; + } + } else { + for (long i = 0; i < readResult; ++i) { + for (size_type j = 0; j < getChannelCount(); ++j) { + *nextSample++ = pcmChannels[j][i]; + } + } + } + readCount += readResult; + } else { + qWarning() << "Failed to read from OggVorbis file:" << readResult; + break; // abort + } + } + return readCount; +} + +} // namespace Mixxx diff --git a/src/audiosourceoggvorbis.h b/src/audiosourceoggvorbis.h new file mode 100644 index 00000000000..e72dce33247 --- /dev/null +++ b/src/audiosourceoggvorbis.h @@ -0,0 +1,41 @@ +#ifndef AUDIOSOURCEOGGVORBIS_H +#define AUDIOSOURCEOGGVORBIS_H + +#include "audiosource.h" +#include "util/defs.h" + +#define OV_EXCLUDE_STATIC_CALLBACKS +#include + +namespace Mixxx +{ + +class AudioSourceOggVorbis: public AudioSource { + typedef AudioSource Super; + +public: + static AudioSourcePointer open(QString fileName); + + ~AudioSourceOggVorbis(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /*override*/; + +private: + AudioSourceOggVorbis(); + + Result postConstruct(QString fileName); + + size_type readFrameSamplesInterleaved(size_type frameCount, + sample_type* sampleBuffer, bool readStereoSamples); + + OggVorbis_File m_vf; +}; + +} + +#endif diff --git a/src/audiosourceopus.cpp b/src/audiosourceopus.cpp new file mode 100644 index 00000000000..1d972473338 --- /dev/null +++ b/src/audiosourceopus.cpp @@ -0,0 +1,115 @@ +#include "audiosourceopus.h" + +namespace Mixxx +{ + +AudioSourceOpus::AudioSourceOpus() + : m_pOggOpusFile(NULL) { +} + +AudioSourceOpus::~AudioSourceOpus() { + close(); +} + +AudioSourcePointer AudioSourceOpus::open(QString fileName) { + AudioSourceOpus* pAudioSourceOpus(new AudioSourceOpus); + AudioSourcePointer pAudioSource(pAudioSourceOpus); // take ownership + if (OK == pAudioSourceOpus->postConstruct(fileName)) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceOpus::postConstruct(QString fileName) { + + int errorCode = 0; + const QByteArray qbaFilename(fileName.toLocal8Bit()); + m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); + if (!m_pOggOpusFile) { + qDebug() << "Failed to open OggOpus file:" << fileName + << "errorCode" << errorCode; + return ERR; + } + + if (!op_seekable(m_pOggOpusFile)) { + qWarning() << "OggOpus file is not seekable:" << fileName; + return ERR; + } + + setChannelCount(op_channel_count(m_pOggOpusFile, -1)); + setFrameRate(kFrameRate); + + ogg_int64_t frameCount = op_pcm_total(m_pOggOpusFile, -1); + if (0 <= frameCount) { + setFrameCount(frameCount); + } else { + qWarning() << "Failed to read OggOpus file:" << fileName; + return ERR; + } + + return OK; +} + +void AudioSourceOpus::close() throw() { + if (m_pOggOpusFile) { + op_free(m_pOggOpusFile); + m_pOggOpusFile = NULL; + } + Super::reset(); +} + +AudioSource::diff_type AudioSourceOpus::seekFrame(diff_type frameIndex) { + int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); + if (0 != seekResult) { + qWarning() << "Failed to seek OggOpus file:" << seekResult; + } + return op_pcm_tell(m_pOggOpusFile); +} + +AudioSource::size_type AudioSourceOpus::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + size_type readCount = 0; + while (readCount < frameCount) { + int readResult = op_read_float(m_pOggOpusFile, + sampleBuffer + frames2samples(readCount), + frames2samples(frameCount - readCount), NULL); + if (0 == readResult) { + break; // done + } + if (0 < readResult) { + readCount += readResult; + } else { + qWarning() << "Failed to read sample data from OggOpus file:" + << readResult; + break; // abort + } + } + return readCount; +} + +AudioSource::size_type AudioSourceOpus::readStereoFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + size_type readCount = 0; + while (readCount < frameCount) { + int readResult = op_read_float_stereo(m_pOggOpusFile, + sampleBuffer + (readCount * 2), + (frameCount - readCount) * 2); + if (0 == readResult) { + // EOF + break; // done + } + if (0 < readResult) { + readCount += readResult; + } else { + qWarning() << "Failed to read sample data from OggOpus file:" + << readResult; + break; // abort + } + } + return readCount; +} + +} // namespace Mixxx diff --git a/src/audiosourceopus.h b/src/audiosourceopus.h new file mode 100644 index 00000000000..ce331211aea --- /dev/null +++ b/src/audiosourceopus.h @@ -0,0 +1,41 @@ +#ifndef AUDIOSOURCEOPUS_H +#define AUDIOSOURCEOPUS_H + +#include "audiosource.h" +#include "util/defs.h" + +#define OV_EXCLUDE_STATIC_CALLBACKS +#include + +namespace Mixxx +{ + +class AudioSourceOpus: public AudioSource { + typedef AudioSource Super; + +public: + // All Opus audio is encoded at 48 kHz + static const size_type kFrameRate = 48000; + + static AudioSourcePointer open(QString fileName); + + ~AudioSourceOpus(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /*override*/; + +private: + AudioSourceOpus(); + + Result postConstruct(QString fileName); + + OggOpusFile *m_pOggOpusFile; +}; + +}; + +#endif diff --git a/src/audiosourcesndfile.cpp b/src/audiosourcesndfile.cpp new file mode 100644 index 00000000000..265278a427b --- /dev/null +++ b/src/audiosourcesndfile.cpp @@ -0,0 +1,93 @@ +#include "audiosourcesndfile.h" + +namespace Mixxx +{ + + +AudioSourceSndFile::AudioSourceSndFile() + : m_pSndFile(NULL) { + memset(&m_sfInfo, 0, sizeof(m_sfInfo)); +} + +AudioSourceSndFile::~AudioSourceSndFile() { + close(); +} + +AudioSourcePointer AudioSourceSndFile::open(QString fileName) { + AudioSourceSndFile* pAudioSourceSndFile(new AudioSourceSndFile); + AudioSourcePointer pAudioSource(pAudioSourceSndFile); // take ownership + if (OK == pAudioSourceSndFile->postConstruct(fileName)) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + +Result AudioSourceSndFile::postConstruct(QString fileName) { +#ifdef __WINDOWS__ + // Pointer valid until string changed + LPCWSTR lpcwFilename = (LPCWSTR)fileName.utf16(); + m_pSndFile = sf_wchar_open(lpcwFilename, SFM_READ, &m_sfInfo); +#else + m_pSndFile = sf_open(fileName.toLocal8Bit().constData(), SFM_READ, &m_sfInfo); +#endif + + if (m_pSndFile == NULL) { // sf_format_check is only for writes + qWarning() << "Error opening libsndfile file:" << fileName + << sf_strerror(m_pSndFile); + return ERR; + } + + if (sf_error(m_pSndFile) > 0) { + qWarning() << "Error opening libsndfile file:" << fileName + << sf_strerror(m_pSndFile); + return ERR; + } + + setChannelCount(m_sfInfo.channels); + setFrameRate(m_sfInfo.samplerate); + setFrameCount(m_sfInfo.frames); + + return OK; +} + +void AudioSourceSndFile::close() throw() { + if (m_pSndFile) { + const int closeResult = sf_close(m_pSndFile); + if (0 != closeResult) { + qWarning() << "Failed to close libsnd file:" << closeResult + << sf_strerror(m_pSndFile); + } + m_pSndFile = NULL; + memset(&m_sfInfo, 0, sizeof(m_sfInfo)); + } + Super::reset(); +} + +AudioSource::diff_type AudioSourceSndFile::seekFrame( + diff_type frameIndex) { + const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); + if (0 <= seekResult) { + return seekResult; + } else { + qWarning() << "Failed to seek libsnd file:" << seekResult + << sf_strerror(m_pSndFile); + return sf_seek(m_pSndFile, 0, SEEK_CUR); + } +} + +AudioSource::size_type AudioSourceSndFile::readFrameSamplesInterleaved( + size_type frameCount, sample_type* sampleBuffer) { + const sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, frameCount); + if (0 <= readCount) { + return readCount; + } else { + qWarning() << "Failed to read from libsnd file:" + << readCount << sf_strerror(m_pSndFile); + return 0; + } +} + +} // namespace Mixxx diff --git a/src/audiosourcesndfile.h b/src/audiosourcesndfile.h new file mode 100644 index 00000000000..b5d188399f6 --- /dev/null +++ b/src/audiosourcesndfile.h @@ -0,0 +1,43 @@ +#ifndef AUDIOSOURCESNDFILE_H +#define AUDIOSOURCESNDFILE_H + +#include "audiosource.h" +#include "util/defs.h" + +#ifdef Q_OS_WIN +//Enable unicode in libsndfile on Windows +//(sf_open uses UTF-8 otherwise) +#include +#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 +#endif +#include + +namespace Mixxx +{ + +class AudioSourceSndFile: public AudioSource { + typedef AudioSource Super; + +public: + static AudioSourcePointer open(QString fileName); + + ~AudioSourceSndFile(); + + diff_type seekFrame(diff_type frameIndex) /*override*/; + + size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + + void close() throw() /*override*/; + +private: + AudioSourceSndFile(); + + Result postConstruct(QString fileName); + + SNDFILE* m_pSndFile; + SF_INFO m_sfInfo; +}; + +} + +#endif diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 60f5d789326..ded9922d59a 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -117,7 +117,7 @@ namespace { Mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); if (pAudioSource.isNull()) { qWarning() << "Failed to open file:" << pTrack->getLocation(); return Mixxx::AudioSourcePointer(); diff --git a/src/dlgprefmodplug.cpp b/src/dlgprefmodplug.cpp index d4c5ddc114e..85b71d45a05 100644 --- a/src/dlgprefmodplug.cpp +++ b/src/dlgprefmodplug.cpp @@ -5,7 +5,7 @@ #include "ui_dlgprefmodplugdlg.h" #include "configobject.h" -#include "soundsourcemodplug.h" +#include "audiosourcemodplug.h" #define kConfigKey "[Modplug]" @@ -180,5 +180,5 @@ void DlgPrefModplug::applySettings() { settings.mLoopCount = 0; // apply modplug settings - SoundSourceModPlug::configure(bufferSizeLimit, settings); + Mixxx::AudioSourceModPlug::configure(bufferSizeLimit, settings); } diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 078b4f52e23..aae5e70c751 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -95,7 +95,7 @@ ChromaPrinter::ChromaPrinter(QObject* parent) QString ChromaPrinter::getFingerPrint(TrackPointer pTrack) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); if (pAudioSource.isNull()) { qDebug() << "Skipping invalid file:" << pTrack->getLocation(); return QString(); diff --git a/src/soundsource.h b/src/soundsource.h index 7d0c821d411..d90b6f31641 100644 --- a/src/soundsource.h +++ b/src/soundsource.h @@ -30,7 +30,6 @@ */ #include "audiosource.h" - #include "util/defs.h" #include @@ -41,18 +40,18 @@ class SoundSource; class TrackMetadata; } -typedef Mixxx::SoundSource* (*getSoundSourceFunc)(QString filename); +typedef Mixxx::SoundSource* (*getSoundSourceFunc)(QString fileName); typedef char** (*getSupportedFileExtensionsFunc)(); typedef int (*getSoundSourceAPIVersionFunc)(); /* New in version 3 */ -typedef void (*freeFileExtensionsFunc)(char** exts); +typedef void (*freeFileExtensionsFunc)(char** fileExts); namespace Mixxx { /* Base class for sound sources. */ -class SoundSource: public AudioSource { +class SoundSource { public: virtual ~SoundSource(); @@ -69,18 +68,13 @@ class SoundSource: public AudioSource { // The implementation is free to set inaccurate estimated // values here that are overwritten when the AudioSource is // actually opened for reading. - virtual Result parseMetadata(Mixxx::TrackMetadata* pMetadata) = 0; + virtual Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const = 0; // Returns the first cover art image embedded within the file (if any). - virtual QImage parseCoverArt() = 0; - - /** - * Opens the SoundSource for reading audio data. - * - * When opening a SoundSource the corresponding - * AudioSource must be initialized. - */ - virtual Result open() = 0; + virtual QImage parseCoverArt() const = 0; + + // Opens the SoundSource for reading audio data. + virtual AudioSourcePointer open() const = 0; protected: explicit SoundSource(QString sFilename); diff --git a/src/soundsourcecoreaudio.cpp b/src/soundsourcecoreaudio.cpp index 0b6d697d884..698f7b2185d 100644 --- a/src/soundsourcecoreaudio.cpp +++ b/src/soundsourcecoreaudio.cpp @@ -15,6 +15,7 @@ #include "soundsourcecoreaudio.h" +#include "audiosourcecoreaudio.h" #include "trackmetadatataglib.h" #include @@ -22,11 +23,6 @@ #include -namespace -{ - Mixxx::AudioSource::size_type kChannelCount = 2; -} - QList SoundSourceCoreAudio::supportedFileExtensions() { QList list; list.push_back("m4a"); @@ -40,137 +36,11 @@ QList SoundSourceCoreAudio::supportedFileExtensions() { return list; } -SoundSourceCoreAudio::SoundSourceCoreAudio(QString filename) - : Super(filename) - , m_headerFrames(0) { -} - -SoundSourceCoreAudio::~SoundSourceCoreAudio() { - ExtAudioFileDispose(m_audioFile); +SoundSourceCoreAudio::SoundSourceCoreAudio(QString fileName) + : Super(filename) { } -// soundsource overrides -Result SoundSourceCoreAudio::open() { - //Open the audio file. - OSStatus err; - - /** This code blocks works with OS X 10.5+ only. DO NOT DELETE IT for now. */ - CFStringRef urlStr = CFStringCreateWithCharacters( - 0, reinterpret_cast(getFilename().unicode()), getFilename().size()); - CFURLRef urlRef = CFURLCreateWithFileSystemPath(NULL, urlStr, kCFURLPOSIXPathStyle, false); - err = ExtAudioFileOpenURL(urlRef, &m_audioFile); - CFRelease(urlStr); - CFRelease(urlRef); - - /** TODO: Use FSRef for compatibility with 10.4 Tiger. - Note that ExtAudioFileOpen() is deprecated above Tiger, so we must maintain - both code paths if someone finishes this part of the code. - FSRef fsRef; - CFURLGetFSRef(reinterpret_cast(url.get()), &fsRef); - err = ExtAudioFileOpen(&fsRef, &m_audioFile); - */ - - if (err != noErr) { - qDebug() << "SSCA: Error opening file " << getFilename(); - return ERR; - } - - // get the input file format - UInt32 inputFormatSize = sizeof(m_inputFormat); - err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileDataFormat, &inputFormatSize, &m_inputFormat); - if (err != noErr) { - qDebug() << "SSCA: Error getting file format (" << getFilename() << ")"; - return ERR; - } - - // create the output format - m_outputFormat = CAStreamBasicDescription( - m_inputFormat.mSampleRate, kChannelCount, - CAStreamBasicDescription::kPCMFormatFloat32, true); - - // set the client format - err = ExtAudioFileSetProperty(m_audioFile, kExtAudioFileProperty_ClientDataFormat, - sizeof(m_outputFormat), &m_outputFormat); - if (err != noErr) { - qDebug() << "SSCA: Error setting file property"; - return ERR; - } - - //get the total length in frames of the audio file - copypasta: http://discussions.apple.com/thread.jspa?threadID=2364583&tstart=47 - SInt64 totalFrameCount; - UInt32 totalFrameCountSize = sizeof(totalFrameCount); - err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileLengthFrames, &totalFrameCountSize, &totalFrameCount); - if (err != noErr) { - qDebug() << "SSCA: Error getting number of frames"; - return ERR; - } - - // - // WORKAROUND for bug in ExtFileAudio - // - - AudioConverterRef acRef; - UInt32 acrsize = sizeof(AudioConverterRef); - err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_AudioConverter, &acrsize, &acRef); - //_ThrowExceptionIfErr(@"kExtAudioFileProperty_AudioConverter", err); - - AudioConverterPrimeInfo primeInfo; - UInt32 piSize = sizeof(AudioConverterPrimeInfo); - memset(&primeInfo, 0, piSize); - err = AudioConverterGetProperty(acRef, kAudioConverterPrimeInfo, &piSize, &primeInfo); - if (err != kAudioConverterErr_PropertyNotSupported) { // Only if decompressing - //_ThrowExceptionIfErr(@"kAudioConverterPrimeInfo", err); - m_headerFrames = primeInfo.leadingFrames; - } - - setChannelCount(m_outputFormat.NumberChannels()); - setFrameRate(m_inputFormat.mSampleRate); - setFrameCount(totalFrameCount/* - m_headerFrames*/); - - //Seek to position 0, which forces us to skip over all the header frames. - //This makes sure we're ready to just let the Analyser rip and it'll - //get the number of samples it expects (ie. no header frames). - seekFrame(0); - - return OK; -} - -Mixxx::AudioSource::diff_type SoundSourceCoreAudio::seekFrame(diff_type frameIndex) { - OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); - //_ThrowExceptionIfErr(@"ExtAudioFileSeek", err); - //qDebug() << "SSCA: Seeking to" << frameIndex; - if (err != noErr) { - qDebug() << "SSCA: Error seeking to" << frameIndex << " (file " << getFilename() << ")";// << GetMacOSStatusErrorString(err) << GetMacOSStatusCommentString(err); - } - return frameIndex; -} - -Mixxx::AudioSource::size_type SoundSourceCoreAudio::readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) { - //if (!m_decoder) return 0; - size_type numFramesRead = 0; - - while (numFramesRead < frameCount) { - size_type numFramesToRead = frameCount - numFramesRead; - - AudioBufferList fillBufList; - fillBufList.mNumberBuffers = 1; - fillBufList.mBuffers[0].mNumberChannels = getChannelCount(); - fillBufList.mBuffers[0].mDataByteSize = frames2samples(numFramesToRead) * sizeof(sampleBuffer[0]); - fillBufList.mBuffers[0].mData = sampleBuffer + frames2samples(numFramesRead); - - UInt32 numFramesToReadInOut = numFramesToRead; // input/output parameter - OSStatus err = ExtAudioFileRead(m_audioFile, &numFramesToReadInOut, &fillBufList); - if (0 == numFramesToReadInOut) { - // EOF reached - break; - } - numFramesRead += numFramesToReadInOut; - } - return numFramesRead; -} - -Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) { +Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { if (getType() == "m4a") { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { @@ -219,7 +89,7 @@ Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -QImage SoundSourceCoreAudio::parseCoverArt() { +QImage SoundSourceCoreAudio::parseCoverArt() const { QImage coverArt; if (getType() == "m4a") { TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); @@ -245,3 +115,7 @@ QImage SoundSourceCoreAudio::parseCoverArt() { } return coverArt; } + +Mixxx::AudioSourcePointer SoundSourceCoreAudio::open() const { + return Mixxx::AudioSourceCoreAudio::open(getFilename()); +} diff --git a/src/soundsourcecoreaudio.h b/src/soundsourcecoreaudio.h index e5a33c6c830..4fd4ba4a430 100644 --- a/src/soundsourcecoreaudio.h +++ b/src/soundsourcecoreaudio.h @@ -21,45 +21,18 @@ #include "soundsource.h" -#include -//In our tree at lib/apple/ -#include "CAStreamBasicDescription.h" - -#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) -#include -#include -#include -#include -#else -#include "CoreAudioTypes.h" -#include "AudioFile.h" -#include "AudioFormat.h" -#endif - class SoundSourceCoreAudio : public Mixxx::SoundSource { typedef SoundSource Super; public: static QList supportedFileExtensions(); - explicit SoundSourceCoreAudio(QString filename); - ~SoundSourceCoreAudio(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata); - QImage parseCoverArt(); + explicit SoundSourceCoreAudio(QString fileName); - Result open(); + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; - -private: - ExtAudioFileRef m_audioFile; - CAStreamBasicDescription m_inputFormat; - CAStreamBasicDescription m_outputFormat; - SInt64 m_headerFrames; + Mixxx::AudioSourcePointer open() const /*override*/; }; - #endif // ifndef SOUNDSOURCECOREAUDIO_H diff --git a/src/soundsourceffmpeg.cpp b/src/soundsourceffmpeg.cpp index 0d3a1339aae..6bb965a8643 100644 --- a/src/soundsourceffmpeg.cpp +++ b/src/soundsourceffmpeg.cpp @@ -23,506 +23,52 @@ #include "soundsourceffmpeg.h" +#include "audiosourceffmpeg.h" #include "trackmetadata.h" -#include #include -#include - -#define SOUNDSOURCEFFMPEG_CACHESIZE 1000 -#define SOUNDSOURCEFFMPEG_POSDISTANCE ((1024 * 1000) / 8) - -SoundSourceFFmpeg::SoundSourceFFmpeg(QString filename) - : Mixxx::SoundSource(filename) - , m_pFormatCtx(NULL) - , m_iAudioStream(-1) - , m_pCodecCtx(NULL) - , m_pCodec(NULL) - , m_pResample(NULL) - , m_iCurrentMixxTs(0) - , m_bIsSeeked(false) - , m_lCacheBytePos(0) - , m_lCacheStartByte(0) - , m_lCacheEndByte(0) - , m_lCacheLastPos(0) - , m_lLastStoredPos(0) - , m_lStoredSeekPoint(-1) { -} - -SoundSourceFFmpeg::~SoundSourceFFmpeg() { - clearCache(); - - if (m_pCodecCtx != NULL) { - qDebug() << "~SoundSourceFFmpeg(): Clear FFMPEG stuff"; - avcodec_close(m_pCodecCtx); - avformat_close_input(&m_pFormatCtx); - av_free(m_pFormatCtx); - } - - if (m_pResample != NULL) { - qDebug() << "~SoundSourceFFmpeg(): Delete FFMPEG Resampler"; - delete m_pResample; - } - - while (m_SJumpPoints.size() > 0) { - ffmpegLocationObject* l_SRmJmp = m_SJumpPoints[0]; - m_SJumpPoints.remove(0); - free(l_SRmJmp); - } -} - -bool SoundSourceFFmpeg::clearCache() { - struct ffmpegCacheObject *l_SRmObj = NULL; - - while (m_SCache.size() > 0) { - l_SRmObj = m_SCache[0]; - m_SCache.remove(0); - free(l_SRmObj->bytes); - free(l_SRmObj); - } - - return true; -} - -bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { - unsigned int l_iCount = 0; - qint32 l_iRet = 0; - AVPacket l_SPacket; - AVFrame *l_pFrame = NULL; - bool l_iStop = false; - int l_iFrameFinished = 0; - struct ffmpegCacheObject *l_SObj = NULL; - struct ffmpegCacheObject *l_SRmObj = NULL; - bool m_bUnique = false; - qint64 l_lLastPacketPos = -1; - int l_iError = 0; - int l_iFrameCount = 0; - - l_iCount = count; - - l_SPacket.data = NULL; - l_SPacket.size = 0; - - - while (l_iCount > 0) { - if (l_pFrame != NULL) { - l_iFrameCount --; -// FFMPEG 2.2 3561060 anb beyond -#if LIBAVCODEC_VERSION_INT >= 3561060 - av_frame_free(&l_pFrame); -// FFMPEG 0.11 and below -#elif LIBAVCODEC_VERSION_INT <= 3544932 - av_free(l_pFrame); -// FFMPEG 1.0 - 2.1 -#else - avcodec_free_frame(&l_pFrame); -#endif - l_pFrame = NULL; - - } - - if (l_iStop == true) { - break; - } - l_iFrameCount ++; - av_init_packet(&l_SPacket); -#if LIBAVCODEC_VERSION_INT < 3617792 - l_pFrame = avcodec_alloc_frame(); -#else - l_pFrame = av_frame_alloc(); -#endif - - if (av_read_frame(m_pFormatCtx, &l_SPacket) >= 0) { - if (l_SPacket.stream_index == m_iAudioStream) { - if (m_lStoredSeekPoint > 0) { - // Seek for correct jump point - if (m_lStoredSeekPoint > l_SPacket.pos && - m_lStoredSeekPoint >= SOUNDSOURCEFFMPEG_POSDISTANCE) { - av_free_packet(&l_SPacket); - l_SPacket.data = NULL; - l_SPacket.size = 0; - continue; - } - m_lStoredSeekPoint = -1; - } - - l_iRet = avcodec_decode_audio4(m_pCodecCtx,l_pFrame,&l_iFrameFinished, - &l_SPacket); - - if (l_iRet <= 0) { - // An error or EOF occured,index break out and return what - // we have so far. - qDebug() << "EOF!"; - l_iStop = true; - continue; - } else { - l_iRet = 0; - l_SObj = (struct ffmpegCacheObject *)malloc(sizeof(struct ffmpegCacheObject)); - if (l_SObj == NULL) { - qDebug() << "SoundSourceFFmpeg::readFramesToCache: Not enough memory!"; - l_iStop = true; - continue; - } - memset(l_SObj, 0x00, sizeof(struct ffmpegCacheObject)); - l_iRet = m_pResample->reSample(l_pFrame, &l_SObj->bytes); - - if (l_iRet > 0) { - // Remove from cache - if (m_SCache.size() >= (SOUNDSOURCEFFMPEG_CACHESIZE - 10)) { - l_SRmObj = m_SCache[0]; - m_SCache.remove(0); - free(l_SRmObj->bytes); - free(l_SRmObj); - } - - // Add to cache and store byte place to memory - m_SCache.append(l_SObj); - l_SObj->startByte = m_lCacheBytePos / 2; - l_SObj->length = l_iRet / 2; - m_lCacheBytePos += l_iRet; - - // Ogg/Opus have packages pos that have many - // audio frames so seek next unique pos.. - if (l_SPacket.pos != l_lLastPacketPos) { - m_bUnique = true; - l_lLastPacketPos = l_SPacket.pos; - } - - // If we are over last storepos and we have read more than jump point need is and pos is unique we store it to memory - if (m_lCacheBytePos > m_lLastStoredPos && - m_lCacheBytePos > (SOUNDSOURCEFFMPEG_POSDISTANCE + m_lLastStoredPos) && - m_bUnique == true) { - struct ffmpegLocationObject *l_SJmp = (struct ffmpegLocationObject *)malloc( - sizeof(struct ffmpegLocationObject)); - m_lLastStoredPos = m_lCacheBytePos; - l_SJmp->startByte = m_lCacheBytePos / 2; - l_SJmp->pos = l_SPacket.pos; - l_SJmp->pts = l_SPacket.pts; - m_SJumpPoints.append(l_SJmp); - m_bUnique = false; - } - - if (offset < 0 || (quint64) offset <= (m_lCacheBytePos / 2)) { - l_iCount --; - } - } else { - free(l_SObj); - l_SObj = NULL; - qDebug() << - "SoundSourceFFmpeg::readFramesToCache: General error in audio decode:" << - l_iRet; - } - } - - av_free_packet(&l_SPacket); - l_SPacket.data = NULL; - l_SPacket.size = 0; - - } else { - l_iError ++; - if (l_iError == 5) { - // Stream end and we couldn't read enough frames - l_iStop = true; - } - } - - - } else { - qDebug() << "SoundSourceFFmpeg::readFramesToCache: Packet too big or File end"; - l_iStop = true; - } - - } - - if (l_pFrame != NULL) { - l_iFrameCount --; -// FFMPEG 2.2 3561060 anb beyond -#if LIBAVCODEC_VERSION_INT >= 3561060 - av_frame_unref(l_pFrame); - av_frame_free(&l_pFrame); -// FFMPEG 0.11 and below -#elif LIBAVCODEC_VERSION_INT <= 3544932 - av_free(l_pFrame); -// FFMPEG 1.0 - 2.1 -#else - avcodec_free_frame(&l_pFrame); -#endif - l_pFrame = NULL; - - } - - if (l_iFrameCount > 0) { - qDebug() << "SoundSourceFFmpeg::readFramesToCache(): Frame balance is not 0 it is: " << l_iFrameCount; - } - - l_SObj = m_SCache.first(); - m_lCacheStartByte = l_SObj->startByte; - l_SObj = m_SCache.last(); - m_lCacheEndByte = (l_SObj->startByte + l_SObj->length); - - if (!l_iCount) { - return true; - } else { - return false; - } -} - -bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, - quint64 size) { - struct ffmpegCacheObject *l_SObj = NULL; - quint32 l_lPos = 0; - quint32 l_lLeft = 0; - quint32 l_lOffset = 0; - quint32 l_lBytesToCopy = 0; - - if (offset >= m_lCacheStartByte) { - if (m_lCacheLastPos == 0) { - m_lCacheLastPos = m_SCache.size() - 1; - } - for (l_lPos = m_lCacheLastPos; l_lPos > 0; l_lPos --) { - l_SObj = m_SCache[l_lPos]; - if ((l_SObj->startByte + l_SObj->length) < offset) { - break; - } - } - - l_SObj = m_SCache[l_lPos]; - - l_lLeft = (size * 2); - memset(buffer, 0x00, l_lLeft); - while (l_lLeft > 0) { - - if (l_SObj == NULL || (l_lPos + 5) > (unsigned int)m_SCache.size()) { - offset = l_SObj->startByte; - if (readFramesToCache(50, -1) == false) { - return false; - } - for (l_lPos = (m_SCache.size() - 50); l_lPos > 0; l_lPos --) { - l_SObj = m_SCache[l_lPos]; - if ((l_SObj->startByte + l_SObj->length) < offset) { - break; - } - } - l_SObj = m_SCache[l_lPos]; - continue; - } - - if (l_SObj->startByte <= offset) { - l_lOffset = (offset - l_SObj->startByte) * 2; - } - - if (l_lOffset >= (l_SObj->length * 2)) { - l_SObj = m_SCache[++ l_lPos]; - continue; - } - - if (l_lLeft > (l_SObj->length * 2)) { - l_lBytesToCopy = ((l_SObj->length * 2) - l_lOffset); - memcpy(buffer, (l_SObj->bytes + l_lOffset), l_lBytesToCopy); - l_lOffset = 0; - buffer += l_lBytesToCopy; - l_lLeft -= l_lBytesToCopy; - } else { - memcpy(buffer, l_SObj->bytes, l_lLeft); - l_lLeft = 0; - } - - l_SObj = m_SCache[++ l_lPos]; - } - - m_lCacheLastPos = --l_lPos; - return true; - } - - return false; -} - -Result SoundSourceFFmpeg::open() { - unsigned int i; - AVDictionary *l_iFormatOpts = NULL; - - qDebug() << "New SoundSourceFFmpeg :" << getFilename(); - - m_pFormatCtx = avformat_alloc_context(); - -#if LIBAVCODEC_VERSION_INT < 3622144 - m_pFormatCtx->max_analyze_duration = 999999999; -#endif - - // Open file and make m_pFormatCtx - if (avformat_open_input(&m_pFormatCtx, getFilename().toLocal8Bit().constData(), NULL, - &l_iFormatOpts)!=0) { - qDebug() << "av_open_input_file: cannot open" << getFilename(); - return ERR; - } - - -#if LIBAVCODEC_VERSION_INT > 3544932 - av_dict_free(&l_iFormatOpts); -#endif - - - // Retrieve stream information - if (avformat_find_stream_info(m_pFormatCtx, NULL)<0) { - qDebug() << "av_find_stream_info: cannot open" << getFilename(); - return ERR; - } - - - //debug only (Enable if needed) - //av_dump_format(m_pFormatCtx, 0, getFilename().toLocal8Bit().constData(), false); - - // Find the first video stream - m_iAudioStream=-1; +QList SoundSourceFFmpeg::supportedFileExtensions() { + QList list; + AVInputFormat *l_SInputFmt = NULL; - for (i=0; inb_streams; i++) - if (m_pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) { - m_iAudioStream=i; + while ((l_SInputFmt = av_iformat_next(l_SInputFmt))) { + if (l_SInputFmt->name == NULL) { break; } - if (m_iAudioStream==-1) { - qDebug() << "ffmpeg: cannot find an audio stream: cannot open" - << getFilename(); - return ERR; - } - - // Get a pointer to the codec context for the video stream - m_pCodecCtx=m_pFormatCtx->streams[m_iAudioStream]->codec; - - // Find the decoder for the audio stream - if (!(m_pCodec=avcodec_find_decoder(m_pCodecCtx->codec_id))) { - qDebug() << "ffmpeg: cannot find a decoder for" << getFilename(); - return ERR; - } - - if (avcodec_open2(m_pCodecCtx, m_pCodec, NULL)<0) { - qDebug() << "ffmpeg: cannot open" << getFilename(); - return ERR; - } - - m_pResample = new EncoderFfmpegResample(m_pCodecCtx); - m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_S16); - - this->setChannelCount(m_pCodecCtx->channels); - this->setFrameRate(m_pCodecCtx->sample_rate); - this->setFrameCount((m_pFormatCtx->duration * m_pCodecCtx->sample_rate) / AV_TIME_BASE); - - qDebug() << "ffmpeg: Samplerate: " << this->getFrameRate() << ", Channels: " << - this->getChannelCount() << "\n"; - if (this->getChannelCount() > 2) { - qDebug() << "ffmpeg: No support for more than 2 channels!"; - return ERR; - } - - return OK; -} - -Mixxx::AudioSource::diff_type SoundSourceFFmpeg::seekFrame(diff_type frameIndex) { - const diff_type filepos = frames2samples(frameIndex); - - int ret = 0; - qint64 i = 0; - - if (filepos < 0 || (unsigned long) filepos < m_lCacheStartByte) { - ret = avformat_seek_file(m_pFormatCtx, - m_iAudioStream, - 0, - 32767 * 2, - 32767 * 2, - AVSEEK_FLAG_BACKWARD); - - - if (ret < 0) { - qDebug() << "SoundSourceFFmpeg::seek: Can't seek to 0 byte!"; - return -1; - } - - clearCache(); - m_SCache.clear(); - m_lCacheStartByte = 0; - m_lCacheEndByte = 0; - m_lCacheLastPos = 0; - m_lCacheBytePos = 0; - m_lStoredSeekPoint = -1; - - - // Try to find some jump point near to - // where we are located so we don't needed - // to try guess it - if (filepos >= SOUNDSOURCEFFMPEG_POSDISTANCE) { - for (i = 0; i < m_SJumpPoints.size(); i ++) { - if (m_SJumpPoints[i]->startByte >= (unsigned long) filepos && i > 2) { - m_lCacheBytePos = m_SJumpPoints[i - 2]->startByte * 2; - m_lStoredSeekPoint = m_SJumpPoints[i - 2]->pos; - break; - } - } - } - if (filepos == 0) { - readFramesToCache((SOUNDSOURCEFFMPEG_CACHESIZE - 50), -1); - } else { - readFramesToCache((SOUNDSOURCEFFMPEG_CACHESIZE / 2), filepos); + if (!strcmp(l_SInputFmt->name, "flac")) { + list.append("flac"); + } else if (!strcmp(l_SInputFmt->name, "ogg")) { + list.append("ogg"); + } else if (!strcmp(l_SInputFmt->name, "mov,mp4,m4a,3gp,3g2,mj2")) { + list.append("m4a"); + } else if (!strcmp(l_SInputFmt->name, "mp4")) { + list.append("mp4"); + } else if (!strcmp(l_SInputFmt->name, "mp3")) { + list.append("mp3"); + } else if (!strcmp(l_SInputFmt->name, "aac")) { + list.append("aac"); + } else if (!strcmp(l_SInputFmt->name, "opus") || + !strcmp(l_SInputFmt->name, "libopus")) { + list.append("opus"); + } else if (!strcmp(l_SInputFmt->name, "wma")) { + list.append("wma"); } } - - if (m_lCacheEndByte <= (unsigned long) filepos) { - readFramesToCache(100, filepos); - } - - m_iCurrentMixxTs = filepos; - - m_bIsSeeked = TRUE; - - return frameIndex; -} - -unsigned int SoundSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { - - if (m_SCache.size() == 0) { - // Make sure we allways start at begining and cache have some - // material that we can consume. - seekFrame(0); - m_bIsSeeked = FALSE; - } - - getBytesFromCache((char *)destination, m_iCurrentMixxTs, size); - - - // As this is also Hack - // If we don't seek like we don't on analyzer.. keep - // place in mind.. - if (m_bIsSeeked == FALSE) { - m_iCurrentMixxTs += size; - } - - m_bIsSeeked = FALSE; - return size; - + return list; } -Mixxx::AudioSource::size_type SoundSourceFFmpeg::readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) { - // This is just a hack that simply reuses existing - // functionality. Sample data should be resampled - // directly into AV_SAMPLE_FMT_FLT instead of - // AV_SAMPLE_FMT_S16! - typedef std::vector TempBuffer; - TempBuffer tempBuffer(frames2samples(frameCount)); - const size_type readSamples = read(tempBuffer.size(), &tempBuffer[0]); - for (size_type i = 0; i < readSamples; ++i) { - sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / sample_type(SAMPLE_MAX); - } - return samples2frames(readSamples); +SoundSourceFFmpeg::SoundSourceFFmpeg(QString fileName) + : Mixxx::SoundSource(fileName) { } -Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) { +Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { qDebug() << "ffmpeg: SoundSourceFFmpeg::parseMetadata" << getFilename(); AVFormatContext *FmtCtx = avformat_alloc_context(); - AVCodecContext *CodecCtx; + AVCodecContext *CodecCtx = NULL; AVDictionaryEntry *FmtTag = NULL; unsigned int i; AVDictionary *l_iFormatOpts = NULL; @@ -547,22 +93,30 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) { if (avformat_find_stream_info(FmtCtx, NULL)<0) { qDebug() << "av_find_stream_info: Can't find metadata" << getFilename(); + avcodec_close(CodecCtx); + avformat_close_input(&FmtCtx); + av_free(FmtCtx); return ERR; } - for (i=0; inb_streams; i++) + int iAudioStream = -1; + for (i = 0; i < FmtCtx->nb_streams; ++i) { if (FmtCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) { - m_iAudioStream=i; + iAudioStream = i; break; } - if (m_iAudioStream==-1) { + } + if (iAudioStream == -1) { qDebug() << "cannot find an audio stream: Can't find stream" << getFilename(); + avcodec_close(CodecCtx); + avformat_close_input(&FmtCtx); + av_free(FmtCtx); return ERR; } // Get a pointer to the codec context for the video stream - CodecCtx=FmtCtx->streams[m_iAudioStream]->codec; + CodecCtx=FmtCtx->streams[iAudioStream]->codec; while ((FmtTag = av_dict_get(FmtCtx->metadata, "", FmtTag, AV_DICT_IGNORE_SUFFIX))) { @@ -592,7 +146,7 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) { } - while ((FmtTag = av_dict_get(FmtCtx->streams[m_iAudioStream]->metadata, "", + while ((FmtTag = av_dict_get(FmtCtx->streams[iAudioStream]->metadata, "", FmtTag, AV_DICT_IGNORE_SUFFIX))) { // Convert the value from UTF-8. QString strValue (QString::fromUtf8 (FmtTag->value)); @@ -629,39 +183,11 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -QImage SoundSourceFFmpeg::parseCoverArt() { +QImage SoundSourceFFmpeg::parseCoverArt() const { // currently not implemented return QImage(); } -QList SoundSourceFFmpeg::supportedFileExtensions() { - QList list; - AVInputFormat *l_SInputFmt = NULL; - - while ((l_SInputFmt = av_iformat_next(l_SInputFmt))) { - if (l_SInputFmt->name == NULL) { - break; - } - - if (!strcmp(l_SInputFmt->name, "flac")) { - list.append("flac"); - } else if (!strcmp(l_SInputFmt->name, "ogg")) { - list.append("ogg"); - } else if (!strcmp(l_SInputFmt->name, "mov,mp4,m4a,3gp,3g2,mj2")) { - list.append("m4a"); - } else if (!strcmp(l_SInputFmt->name, "mp4")) { - list.append("mp4"); - } else if (!strcmp(l_SInputFmt->name, "mp3")) { - list.append("mp3"); - } else if (!strcmp(l_SInputFmt->name, "aac")) { - list.append("aac"); - } else if (!strcmp(l_SInputFmt->name, "opus") || - !strcmp(l_SInputFmt->name, "libopus")) { - list.append("opus"); - } else if (!strcmp(l_SInputFmt->name, "wma")) { - list.append("wma"); - } - } - - return list; +Mixxx::AudioSourcePointer SoundSourceFFmpeg::open() const { + return Mixxx::AudioSourceFFmpeg::open(getFilename()); } diff --git a/src/soundsourceffmpeg.h b/src/soundsourceffmpeg.h index 73731314ddf..dd84f9e8d4b 100644 --- a/src/soundsourceffmpeg.h +++ b/src/soundsourceffmpeg.h @@ -20,45 +20,6 @@ #include "soundsource.h" -#include - -extern "C" { -// Needed to ensure that macros in get defined. -#ifndef __STDC_CONSTANT_MACROS -#if __cplusplus < 201103L -#define __STDC_CONSTANT_MACROS -#endif -#endif - -#include -#include - -#ifndef __FFMPEGOLDAPI__ -#include -#include -#endif - -// Compability -#include -#include - -} -#define SOUNDSOURCEFFMPEGDEBUG - -class TrackInfoObject; - -struct ffmpegLocationObject { - quint64 pos; - qint64 pts; - quint64 startByte; -}; - -struct ffmpegCacheObject { - quint64 startByte; - quint32 length; - quint8 *bytes; -}; - class SoundSourceFFmpeg : public Mixxx::SoundSource { typedef SoundSource Super; @@ -66,44 +27,11 @@ class SoundSourceFFmpeg : public Mixxx::SoundSource { static QList supportedFileExtensions(); explicit SoundSourceFFmpeg(QString qFilename); - ~SoundSourceFFmpeg(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - QImage parseCoverArt() /*override*/; - - Result open() /*override*/; - - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; - -private: - bool readFramesToCache(unsigned int count, qint64 offset); - bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); - quint64 getSizeofCache(); - bool clearCache(); - - unsigned int read(unsigned long size, SAMPLE*); - - AVFormatContext *m_pFormatCtx; - int m_iAudioStream; - AVCodecContext *m_pCodecCtx; - AVCodec *m_pCodec; - - EncoderFfmpegResample *m_pResample; - - qint64 m_iCurrentMixxTs; - bool m_bIsSeeked; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; - quint64 m_lCacheBytePos; - quint64 m_lCacheStartByte; - quint64 m_lCacheEndByte; - quint32 m_lCacheLastPos; - QVector m_SCache; - QVector m_SJumpPoints; - quint64 m_lLastStoredPos; - qint64 m_lStoredSeekPoint; + Mixxx::AudioSourcePointer open() const /*override*/; }; #endif diff --git a/src/soundsourceflac.cpp b/src/soundsourceflac.cpp index 12542e8dd0f..cb30a4d7808 100644 --- a/src/soundsourceflac.cpp +++ b/src/soundsourceflac.cpp @@ -16,8 +16,7 @@ #include "soundsourceflac.h" #include "trackmetadatataglib.h" -#include "sampleutil.h" -#include "util/math.h" +#include "audiosourceflac.h" #include @@ -29,150 +28,11 @@ QList SoundSourceFLAC::supportedFileExtensions() { return list; } -SoundSourceFLAC::SoundSourceFLAC(QString filename) - : Super(filename, "flac"), m_file(filename), m_decoder(NULL), m_minBlocksize( - 0), m_maxBlocksize(0), m_minFramesize(0), m_maxFramesize(0), m_sampleScale( - kSampleValueZero), m_decodeSampleBufferReadOffset(0), m_decodeSampleBufferWriteOffset( - 0) { +SoundSourceFLAC::SoundSourceFLAC(QString fileName) + : Super(fileName, "flac") { } -SoundSourceFLAC::~SoundSourceFLAC() { - close(); -} - -// soundsource overrides -Result SoundSourceFLAC::open() { - if (NULL != m_decoder) { - qWarning() << "SSFLAC: Already open!"; - return ERR; - } - - if (!m_file.open(QIODevice::ReadOnly)) { - qWarning() << "SSFLAC: Could not read file!"; - return ERR; - } - - m_decoder = FLAC__stream_decoder_new(); - if (m_decoder == NULL) { - qWarning() << "SSFLAC: decoder allocation failed!"; - close(); - return ERR; - } - FLAC__stream_decoder_set_md5_checking(m_decoder, FALSE); - const FLAC__StreamDecoderInitStatus initStatus( - FLAC__stream_decoder_init_stream(m_decoder, FLAC_read_cb, - FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, - FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); - if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { - qWarning() << "SSFLAC: decoder init failed!"; - close(); - return ERR; - } - if (!FLAC__stream_decoder_process_until_end_of_metadata(m_decoder)) { - qWarning() << "SSFLAC: process to end of meta failed!"; - qWarning() << "SSFLAC: decoder state: " - << FLAC__stream_decoder_get_state(m_decoder); - close(); - return ERR; - } - - return OK; -} - -void SoundSourceFLAC::close() { - if (m_decoder) { - FLAC__stream_decoder_finish(m_decoder); - FLAC__stream_decoder_delete(m_decoder); // frees memory - m_decoder = NULL; - } - m_decodeSampleBuffer.clear(); - m_file.close(); - Super::reset(); -} - -Mixxx::AudioSource::diff_type SoundSourceFLAC::seekFrame(diff_type frameIndex) { - // clear decode buffer before seeking - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; - bool result = FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex); - if (!result) { - qWarning() << "SSFLAC: Seeking error at file" << getFilename(); - } - return frameIndex; -} - -Mixxx::AudioSource::size_type SoundSourceFLAC::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); -} - -Mixxx::AudioSource::size_type SoundSourceFLAC::readStereoFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); -} - -Mixxx::AudioSource::size_type SoundSourceFLAC::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer, - bool readStereoSamples) { - sample_type* outBuffer = sampleBuffer; - size_type framesRemaining = frameCount; - while (0 < framesRemaining) { - Q_ASSERT( - m_decodeSampleBufferReadOffset - <= m_decodeSampleBufferWriteOffset); - // if our buffer from libflac is empty (either because we explicitly cleared - // it or because we've simply used all the samples), ask for a new buffer - if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; - if (FLAC__stream_decoder_process_single(m_decoder)) { - if (m_decodeSampleBufferReadOffset - >= m_decodeSampleBufferWriteOffset) { - // EOF - break; - } - } else { - qWarning() << "SSFLAC: decoder_process_single returned false (" - << getFilename() << ")"; - break; - } - } - Q_ASSERT( - m_decodeSampleBufferReadOffset - <= m_decodeSampleBufferWriteOffset); - const size_type decodeBufferSamples = m_decodeSampleBufferWriteOffset - - m_decodeSampleBufferReadOffset; - const size_type decodeBufferFrames = samples2frames( - decodeBufferSamples); - const size_type framesToCopy = math_min(framesRemaining, decodeBufferFrames); - const size_type samplesToCopy = frames2samples(framesToCopy); - if (readStereoSamples && !isChannelCountStereo()) { - if (isChannelCountMono()) { - SampleUtil::copyMonoToDualMono(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy); - } else { - SampleUtil::copyMultiToStereo(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy, getChannelCount()); - } - outBuffer += framesToCopy * 2; // copied 2 samples per frame - } else { - SampleUtil::copy(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - samplesToCopy); - outBuffer += samplesToCopy; - } - m_decodeSampleBufferReadOffset += samplesToCopy; - framesRemaining -= framesToCopy; - Q_ASSERT( - m_decodeSampleBufferReadOffset - <= m_decodeSampleBufferWriteOffset); - } - return frameCount - framesRemaining; -} - -Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) { +Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { @@ -200,7 +60,7 @@ Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -QImage SoundSourceFLAC::parseCoverArt() { +QImage SoundSourceFLAC::parseCoverArt() const { TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); QImage coverArt; TagLib::Ogg::XiphComment *xiph(f.xiphComment()); @@ -225,194 +85,6 @@ QImage SoundSourceFLAC::parseCoverArt() { return coverArt; } -// flac callback methods -FLAC__StreamDecoderReadStatus SoundSourceFLAC::flacRead(FLAC__byte buffer[], - size_t *bytes) { - *bytes = m_file.read((char*) buffer, *bytes); - if (*bytes > 0) { - return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; - } else if (*bytes == 0) { - return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; - } else { - return FLAC__STREAM_DECODER_READ_STATUS_ABORT; - } -} - -FLAC__StreamDecoderSeekStatus SoundSourceFLAC::flacSeek(FLAC__uint64 offset) { - if (m_file.seek(offset)) { - return FLAC__STREAM_DECODER_SEEK_STATUS_OK; - } else { - qWarning() << "SSFLAC: An unrecoverable error occurred (" - << getFilename() << ")"; - return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR; - } -} - -FLAC__StreamDecoderTellStatus SoundSourceFLAC::flacTell(FLAC__uint64 *offset) { - if (m_file.isSequential()) { - return FLAC__STREAM_DECODER_TELL_STATUS_UNSUPPORTED; - } - *offset = m_file.pos(); - return FLAC__STREAM_DECODER_TELL_STATUS_OK; -} - -FLAC__StreamDecoderLengthStatus SoundSourceFLAC::flacLength( - FLAC__uint64 *length) { - if (m_file.isSequential()) { - return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; - } - *length = m_file.size(); - return FLAC__STREAM_DECODER_LENGTH_STATUS_OK; -} - -FLAC__bool SoundSourceFLAC::flacEOF() { - if (m_file.isSequential()) { - return false; - } - return m_file.atEnd(); -} - -FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( - const FLAC__Frame *frame, const FLAC__int32 * const buffer[]) { - if (getChannelCount() != frame->header.channels) { - qWarning() << "Invalid number of channels in FLAC frame header:" - << "expected" << getChannelCount() << "actual" - << frame->header.channels; - return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; - } - Q_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - Q_ASSERT( - (m_decodeSampleBuffer.size() - m_decodeSampleBufferWriteOffset) - >= frames2samples(frame->header.blocksize)); - switch (getChannelCount()) { - case 1: { - // optimized code for 1 channel (mono) - Q_ASSERT(1 <= frame->header.channels); - for (unsigned i = 0; i < frame->header.blocksize; ++i) { - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[0][i] * m_sampleScale; - } - break; - } - case 2: { - // optimized code for 2 channels (stereo) - Q_ASSERT(2 <= frame->header.channels); - for (unsigned i = 0; i < frame->header.blocksize; ++i) { - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[0][i] * m_sampleScale; - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[1][i] * m_sampleScale; - } - break; - } - default: { - // generic code for multiple channels - for (unsigned i = 0; i < frame->header.blocksize; ++i) { - for (unsigned j = 0; j < frame->header.channels; ++j) { - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[j][i] * m_sampleScale; - } - } - } - } - Q_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; -} - -void SoundSourceFLAC::flacMetadata(const FLAC__StreamMetadata *metadata) { - switch (metadata->type) { - case FLAC__METADATA_TYPE_STREAMINFO: - setChannelCount(metadata->data.stream_info.channels); - setFrameRate(metadata->data.stream_info.sample_rate); - setFrameCount(metadata->data.stream_info.total_samples); - m_sampleScale = kSampleValuePeak - / sample_type( - FLAC__int32(1) - << metadata->data.stream_info.bits_per_sample); - qDebug() << "FLAC file " << getFilename(); - qDebug() << getChannelCount() << " @ " << getFrameRate() << " Hz, " - << getFrameCount() << " total, " << "bit depth" - << " metadata->data.stream_info.bits_per_sample"; - m_minBlocksize = metadata->data.stream_info.min_blocksize; - m_maxBlocksize = metadata->data.stream_info.max_blocksize; - m_minFramesize = metadata->data.stream_info.min_framesize; - m_maxFramesize = metadata->data.stream_info.max_framesize; - qDebug() << "Blocksize in [" << m_minBlocksize << ", " << m_maxBlocksize - << "], Framesize in [" << m_minFramesize << ", " - << m_maxFramesize << "]"; - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; - m_decodeSampleBuffer.resize(m_maxBlocksize * getChannelCount()); - break; - default: - break; - } -} - -void SoundSourceFLAC::flacError(FLAC__StreamDecoderErrorStatus status) { - QString error; - // not much can be done at this point -- luckly the decoder seems to be - // pretty forgiving -- bkgood - switch (status) { - case FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC: - error = "STREAM_DECODER_ERROR_STATUS_LOST_SYNC"; - break; - case FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER: - error = "STREAM_DECODER_ERROR_STATUS_BAD_HEADER"; - break; - case FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH: - error = "STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH"; - break; - case FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM: - error = "STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM"; - break; - } - qWarning() << "SSFLAC got error" << error << "from libFLAC for file" - << getFilename(); - // not much else to do here... whatever function that initiated whatever - // decoder method resulted in this error will return an error, and the caller - // will bail. libFLAC docs say to not close the decoder here -- bkgood -} - -// begin callbacks (have to be regular functions because normal libFLAC isn't C++-aware) - -FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder*, - FLAC__byte buffer[], size_t *bytes, void *client_data) { - return ((SoundSourceFLAC*) client_data)->flacRead(buffer, bytes); -} - -FLAC__StreamDecoderSeekStatus FLAC_seek_cb(const FLAC__StreamDecoder*, - FLAC__uint64 absolute_byte_offset, void *client_data) { - return ((SoundSourceFLAC*) client_data)->flacSeek(absolute_byte_offset); -} - -FLAC__StreamDecoderTellStatus FLAC_tell_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *absolute_byte_offset, void *client_data) { - return ((SoundSourceFLAC*) client_data)->flacTell(absolute_byte_offset); -} - -FLAC__StreamDecoderLengthStatus FLAC_length_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *stream_length, void *client_data) { - return ((SoundSourceFLAC*) client_data)->flacLength(stream_length); -} - -FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder*, void *client_data) { - return ((SoundSourceFLAC*) client_data)->flacEOF(); -} - -FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder*, - const FLAC__Frame *frame, const FLAC__int32 * const buffer[], - void *client_data) { - return ((SoundSourceFLAC*) client_data)->flacWrite(frame, buffer); -} - -void FLAC_metadata_cb(const FLAC__StreamDecoder*, - const FLAC__StreamMetadata *metadata, void *client_data) { - ((SoundSourceFLAC*) client_data)->flacMetadata(metadata); -} - -void FLAC_error_cb(const FLAC__StreamDecoder*, - FLAC__StreamDecoderErrorStatus status, void *client_data) { - ((SoundSourceFLAC*) client_data)->flacError(status); +Mixxx::AudioSourcePointer SoundSourceFLAC::open() const { + return Mixxx::AudioSourceFLAC::open(getFilename()); } -// end callbacks diff --git a/src/soundsourceflac.h b/src/soundsourceflac.h index 0224a14887a..6dcf0d44275 100644 --- a/src/soundsourceflac.h +++ b/src/soundsourceflac.h @@ -20,73 +20,18 @@ #include "soundsource.h" -#include - -#include - -#include - class SoundSourceFLAC : public Mixxx::SoundSource { typedef SoundSource Super; public: static QList supportedFileExtensions(); - explicit SoundSourceFLAC(QString filename); - ~SoundSourceFLAC(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - QImage parseCoverArt() /*override*/; - - Result open() /*override*/; - - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + explicit SoundSourceFLAC(QString fileName); - // callback methods - FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t *bytes); - FLAC__StreamDecoderSeekStatus flacSeek(FLAC__uint64 offset); - FLAC__StreamDecoderTellStatus flacTell(FLAC__uint64 *offset); - FLAC__StreamDecoderLengthStatus flacLength(FLAC__uint64 *length); - FLAC__bool flacEOF(); - FLAC__StreamDecoderWriteStatus flacWrite(const FLAC__Frame *frame, const FLAC__int32 *const buffer[]); - void flacMetadata(const FLAC__StreamMetadata *metadata); - void flacError(FLAC__StreamDecoderErrorStatus status); + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; -private: - void close(); - - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); - - QFile m_file; - - FLAC__StreamDecoder *m_decoder; - // misc bits about the flac format: - // flac encodes from and decodes to LPCM in blocks, each block is made up of - // subblocks (one for each chan) - // flac stores in 'frames', each of which has a header and a certain number - // of subframes (one for each channel) - size_type m_minBlocksize; // in time samples (audio samples = time samples * chanCount) - size_type m_maxBlocksize; - size_type m_minFramesize; - size_type m_maxFramesize; - sample_type m_sampleScale; - - typedef std::vector SampleBuffer; - std::vector m_decodeSampleBuffer; - SampleBuffer::size_type m_decodeSampleBufferReadOffset; - SampleBuffer::size_type m_decodeSampleBufferWriteOffset; + Mixxx::AudioSourcePointer open() const /*override*/; }; -// callbacks for libFLAC -FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder *decoder, FLAC__byte buffer[], size_t *bytes, void *client_data); -FLAC__StreamDecoderSeekStatus FLAC_seek_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 absolute_byte_offset, void *client_data); -FLAC__StreamDecoderTellStatus FLAC_tell_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *absolute_byte_offset, void *client_data); -FLAC__StreamDecoderLengthStatus FLAC_length_cb(const FLAC__StreamDecoder *decoder, FLAC__uint64 *stream_length, void *client_data); -FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder *decoder, void *client_data); -FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data); -void FLAC_metadata_cb(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data); -void FLAC_error_cb(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data); - #endif // ifndef SOUNDSOURCEFLAC_H diff --git a/src/soundsourcemodplug.cpp b/src/soundsourcemodplug.cpp index 0859449db4a..ce848616f7b 100644 --- a/src/soundsourcemodplug.cpp +++ b/src/soundsourcemodplug.cpp @@ -1,22 +1,36 @@ #include "soundsourcemodplug.h" +#include "audiosourcemodplug.h" #include "trackmetadata.h" #include "util/timer.h" #include #include -#include #include -/* read files in 512k chunks */ -#define CHUNKSIZE (1 << 18) - -// reserve some static space for settings... namespace { - static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) - static ModPlug::ModPlug_Settings s_settings; // modplug decoder parameters +QString getTypeFromFilename(QString fileName) { + const QString fileExt(fileName.section(".", -1).toLower()); + if (fileExt == "mod") { + return "Protracker"; + } else if (fileExt == "med") { + return "OctaMed"; + } else if (fileExt == "okt") { + return "Oktalyzer"; + } else if (fileExt == "s3m") { + return "Scream Tracker 3"; + } else if (fileExt == "stm") { + return "Scream Tracker"; + } else if (fileExt == "xm") { + return "FastTracker2"; + } else if (fileExt == "it") { + return "Impulse Tracker"; + } else { + return "Module"; + } +} } QList SoundSourceModPlug::supportedFileExtensions() { @@ -33,151 +47,34 @@ QList SoundSourceModPlug::supportedFileExtensions() { return list; } -void SoundSourceModPlug::configure(unsigned int bufferSizeLimit, - const ModPlug::ModPlug_Settings &settings) { - s_bufferSizeLimit = bufferSizeLimit; - s_settings = settings; - ModPlug::ModPlug_SetSettings(&s_settings); +SoundSourceModPlug::SoundSourceModPlug(QString fileName) + : Super(fileName, getTypeFromFilename(fileName)) { } -namespace -{ - QString getTypeFromFilename(QString filename) { - const QString fileext(filename.section(".", -1).toLower()); - if (fileext == "mod") { - return "Protracker"; - } else if (fileext == "med") { - return "OctaMed"; - } else if (fileext == "okt") { - return "Oktalyzer"; - } else if (fileext == "s3m") { - return "Scream Tracker 3"; - } else if (fileext == "stm") { - return "Scream Tracker"; - } else if (fileext == "xm") { - return "FastTracker2"; - } else if (fileext == "it") { - return "Impulse Tracker"; - } else { - return "Module"; - } - } -} - -SoundSourceModPlug::SoundSourceModPlug(QString qFilename) - : Super(qFilename, getTypeFromFilename(qFilename)), m_pModFile(NULL), m_fileLength(0), m_seekPos(0) { - setChannelCount(2); // always stereo - setFrameRate(44100); // always 44.1kHz -} - -SoundSourceModPlug::~SoundSourceModPlug() { - if (m_pModFile) { - ModPlug::ModPlug_Unload(m_pModFile); - } -} - -Result SoundSourceModPlug::open() { - ScopedTimer t("SoundSourceModPlug::open()"); - - qDebug() << "[ModPlug] Loading ModPlug module " << getFilename(); - - // read module file to byte array +Result SoundSourceModPlug::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { QFile modFile(getFilename()); modFile.open(QIODevice::ReadOnly); - m_fileBuf = modFile.readAll(); + const QByteArray fileBuf(modFile.readAll()); modFile.close(); - // get ModPlugFile descriptor for later access - m_pModFile = ModPlug::ModPlug_Load(m_fileBuf.constData(), - m_fileBuf.length()); - if (m_pModFile == NULL) { - // an error occured - t.cancel(); - qDebug() << "[ModPlug] Could not load module file: " << getFilename(); - return ERR; + ModPlug::ModPlugFile* pModFile = ModPlug::ModPlug_Load(fileBuf.constData(), fileBuf.length()); + if (NULL != pModFile) { + pMetadata->setComment(QString(ModPlug::ModPlug_GetMessage(pModFile))); + pMetadata->setTitle(QString(ModPlug::ModPlug_GetName(pModFile))); + pMetadata->setDuration(ModPlug::ModPlug_GetLength(pModFile) / 1000); + pMetadata->setBitrate(8); // not really, but fill in something... + ModPlug::ModPlug_Unload(pModFile); } - // estimate size of sample buffer (for better performance) - // beware: module length estimation is unreliable due to loops - // song milliseconds * 2 (bytes per sample) - // * 2 (channels) - // * 44.1 (samples per millisecond) - // + some more to accomodate short loops etc. - // approximate and align with CHUNKSIZE yields: - // (((milliseconds << 1) >> 10 /* to seconds */) - // div 11 /* samples to chunksize ratio */) - // << 19 /* align to chunksize */ - unsigned int estimate = ((ModPlug::ModPlug_GetLength(m_pModFile) >> 8) / 11) << 18; - estimate = math_min(estimate, s_bufferSizeLimit); - m_sampleBuf.reserve(estimate); - qDebug() << "[ModPlug] Reserved " << m_sampleBuf.capacity() - << " #samples"; - - // decode samples to sample buffer - int samplesRead = -1; - int currentSize = 0; - while ((samplesRead != 0) && (m_sampleBuf.size() < s_bufferSizeLimit)) { - // reserve enough space in sample buffer - m_sampleBuf.resize(currentSize + CHUNKSIZE); - samplesRead = ModPlug::ModPlug_Read(m_pModFile, - m_sampleBuf.data() + currentSize, - CHUNKSIZE * 2) / 2; - // adapt to actual size - currentSize += samplesRead; - if (samplesRead != CHUNKSIZE) { - m_sampleBuf.resize(currentSize); - samplesRead = 0; // we reached the end of the file - } - } - qDebug() << "[ModPlug] Filled Sample buffer with " << m_sampleBuf.size() - << " samples."; - qDebug() << "[ModPlug] Sample buffer has " - << m_sampleBuf.capacity() - m_sampleBuf.size() - << " samples unused capacity."; - - setFrameCount(samples2frames(m_sampleBuf.size())); - m_seekPos = 0; - - return OK; -} - -Mixxx::AudioSource::diff_type SoundSourceModPlug::seekFrame( - diff_type frameIndex) { - return m_seekPos = frameIndex; -} - -Mixxx::AudioSource::size_type SoundSourceModPlug::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - const size_type maxFrames = samples2frames(m_sampleBuf.size()); - const size_type readFrames = math_min(maxFrames - m_seekPos, frameCount); - - const size_type readSamples = frames2samples(readFrames); - const size_type readOffset = frames2samples(m_seekPos); - for (size_type i = 0; i < readSamples; ++i) { - sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) / sample_type(SAMPLE_MAX); - } - - m_seekPos += readFrames; - return readFrames; -} - -Result SoundSourceModPlug::parseMetadata(Mixxx::TrackMetadata* pMetadata) { - if (m_pModFile == NULL) { - // an error occured - qDebug() << "Could not parse module header of " << getFilename(); - return ERR; - } - - pMetadata->setComment(QString(ModPlug::ModPlug_GetMessage(m_pModFile))); - pMetadata->setTitle(QString(ModPlug::ModPlug_GetName(m_pModFile))); - pMetadata->setDuration(ModPlug::ModPlug_GetLength(m_pModFile) / 1000); - pMetadata->setBitrate(8); // not really, but fill in something... - - return OK; + return pModFile ? OK : ERR; } -QImage SoundSourceModPlug::parseCoverArt() { +QImage SoundSourceModPlug::parseCoverArt() const { // The modplug library currently does not support reading cover-art from // modplug files -- kain88 (Oct 2014) return QImage(); } + +Mixxx::AudioSourcePointer SoundSourceModPlug::open() const { + return Mixxx::AudioSourceModPlug::open(getFilename()); +} diff --git a/src/soundsourcemodplug.h b/src/soundsourcemodplug.h index bf2c1f99410..f7302b4c389 100644 --- a/src/soundsourcemodplug.h +++ b/src/soundsourcemodplug.h @@ -3,12 +3,6 @@ #include "soundsource.h" -namespace ModPlug { -#include -} - -#include - // Class for reading tracker files using libmodplug. // The whole file is decoded at once and saved // in RAM to allow seeking and smooth operation in Mixxx. @@ -18,40 +12,13 @@ class SoundSourceModPlug: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - // apply settings for decoding - static void configure(unsigned int bufferSizeLimit, - const ModPlug::ModPlug_Settings &settings); - - explicit SoundSourceModPlug(QString qFilename); - ~SoundSourceModPlug(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - QImage parseCoverArt() /*override*/; - - Result open() /*override*/; - - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; - -private: - ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor - unsigned long m_fileLength; // length of file in samples - unsigned long m_seekPos; // current read position - QByteArray m_fileBuf; // original module file data - std::vector m_sampleBuf; // 16bit stereo samples, 44.1kHz - - // identification of modplug module type - enum ModuleTypes { - NONE = 0x00, - MOD = 0x01, - S3M = 0x02, - XM = 0x04, - MED = 0x08, - IT = 0x20, - STM = 0x100, - OKT = 0x8000 - }; + explicit SoundSourceModPlug(QString fileName); + + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + + QImage parseCoverArt() const /*override*/; + + Mixxx::AudioSourcePointer open() const /*override*/; }; #endif diff --git a/src/soundsourcemp3.cpp b/src/soundsourcemp3.cpp index bde92428a8d..acd85768724 100644 --- a/src/soundsourcemp3.cpp +++ b/src/soundsourcemp3.cpp @@ -16,48 +16,11 @@ #include "soundsourcemp3.h" +#include "audiosourcemp3.h" #include "trackmetadatataglib.h" -#include "util/math.h" - -#include #include -namespace -{ - const Mixxx::AudioSource::size_type kSeekFramePrefetchCount = 2; // required for synchronization - - const Mixxx::AudioSource::size_type kMaxSamplesPerMp3Frame = 1152; - - const Mixxx::AudioSource::diff_type kMaxSkipFrameSamplesWhenSeeking = 2 * kSeekFramePrefetchCount * kMaxSamplesPerMp3Frame; - - const Mixxx::AudioSource::sample_type kMadScale = - Mixxx::AudioSource::kSampleValuePeak - / Mixxx::AudioSource::sample_type( - mad_fixed_t(1) << MAD_F_FRACBITS); - - inline Mixxx::AudioSource::sample_type madScale(mad_fixed_t sample) { - return sample * kMadScale; - } - - // Optimization: Reserve initial capacity for seek frame list - const Mixxx::AudioSource::size_type kMinutesPerFile = 10; // enough for the majority of files (tunable) - const Mixxx::AudioSource::size_type kSecondsPerMinute = 60; // fixed - const Mixxx::AudioSource::size_type kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 - const Mixxx::AudioSource::size_type kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; - - bool mad_skip_id3_tag(mad_stream* pStream) { - long tagsize = id3_tag_query(pStream->this_frame, - pStream->bufend - pStream->this_frame); - if (0 < tagsize) { - mad_stream_skip(pStream, tagsize); - return true; - } else { - return false; - } - } -} - QList SoundSourceMp3::supportedFileExtensions() { QList list; list.push_back("mp3"); @@ -65,330 +28,10 @@ QList SoundSourceMp3::supportedFileExtensions() { } SoundSourceMp3::SoundSourceMp3(QString qFilename) - : Super(qFilename, "mp3"), - m_file(qFilename), - m_fileSize(0), - m_pFileData(NULL), - m_avgSeekFrameCount(0), - m_curFrameIndex(0), - m_madSynthCount(0) { - mad_stream_init(&m_madStream); - mad_frame_init(&m_madFrame); - mad_synth_init(&m_madSynth); + : Super(qFilename, "mp3") { } -SoundSourceMp3::~SoundSourceMp3() { - close(); -} - -Result SoundSourceMp3::open() { - if (!m_file.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file:" << getFilename(); - return ERR; - } - - // Get a pointer to the file using memory mapped IO - m_fileSize = m_file.size(); - m_pFileData = m_file.map(0, m_fileSize); - - // Transfer it to the mad stream-buffer: - mad_stream_init(&m_madStream); - mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); - - m_seekFrameList.reserve(kSeekFrameListCapacity); - - /* - Decode all the headers, and fill in stats: - */ - unsigned long sumBitrate = 0; - unsigned int madFrameCount = 0; - setChannelCount(kChannelCountDefault); - setFrameRate(kFrameRateDefault); - mad_timer_t madFileDuration = mad_timer_zero; - mad_units madUnits; - while ((m_madStream.bufend - m_madStream.this_frame) > 0) { - mad_header madHeader; - mad_header_init(&madHeader); - if (0 != mad_header_decode(&madHeader, &m_madStream)) { - if (MAD_RECOVERABLE(m_madStream.error)) { - if (MAD_ERROR_LOSTSYNC == m_madStream.error) { - // Ignore LOSTSYNC due to ID3 tags - mad_skip_id3_tag(&m_madStream); - } else { - qDebug() << "Recoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); - } - mad_header_finish(&madHeader); - continue; - } else { - if (MAD_ERROR_BUFLEN == m_madStream.error) { - // EOF -> done - break; - } else { - qWarning() << "Unrecoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); - // abort - mad_header_finish(&madHeader); - close(); - return ERR; - } - } - } - - // Grab data from madHeader - const size_type madChannelCount = MAD_NCHANNELS(&madHeader); - if (kChannelCountDefault == getChannelCount()) { - // initially set the number of channels - setChannelCount(madChannelCount); - } else { - // check for consistent number of channels - if ((0 < madChannelCount) && getChannelCount() != madChannelCount) { - qWarning() << "Differing number of channels in some headers:" - << getFilename() << getChannelCount() << "<>" - << madChannelCount; - } - } - const size_type madSampleRate = madHeader.samplerate; - if (kFrameRateDefault == getFrameRate()) { - // initially set the frame/sample rate - switch (madSampleRate) { - case 8000: - madUnits = MAD_UNITS_8000_HZ; - break; - case 11025: - madUnits = MAD_UNITS_11025_HZ; - break; - case 12000: - madUnits = MAD_UNITS_12000_HZ; - break; - case 16000: - madUnits = MAD_UNITS_16000_HZ; - break; - case 22050: - madUnits = MAD_UNITS_22050_HZ; - break; - case 24000: - madUnits = MAD_UNITS_24000_HZ; - break; - case 32000: - madUnits = MAD_UNITS_32000_HZ; - break; - case 44100: - madUnits = MAD_UNITS_44100_HZ; - break; - case 48000: - madUnits = MAD_UNITS_48000_HZ; - break; - default: - qWarning() << "Invalid sample rate:" - << getFilename() << madSampleRate; - // abort - close(); - return ERR; - } - setFrameRate(madSampleRate); - } else { - // check for consistent frame/sample rate - if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { - qWarning() << "Differing sample rate in some headers:" - << getFilename() << getFrameRate() << "<>" - << madSampleRate; - // abort - close(); - return ERR; - } - } - - // Add frame to list of frames - MadSeekFrameType seekFrame; - seekFrame.pFileData = m_madStream.this_frame; - seekFrame.frameIndex = mad_timer_count(madFileDuration, madUnits); - m_seekFrameList.push_back(seekFrame); - - mad_timer_add(&madFileDuration, madHeader.duration); - sumBitrate += madHeader.bitrate; - - mad_header_finish(&madHeader); - - ++madFrameCount; - } - - if (0 < madFrameCount) { - setFrameCount(mad_timer_count(madFileDuration, madUnits)); - m_avgSeekFrameCount = getFrameCount() / madFrameCount; - int avgBitrate = sumBitrate / madFrameCount; - setBitrate(avgBitrate); - } else { - // This is not a working MP3 file. - qWarning() << "SSMP3: This is not a working MP3 file:" << getFilename(); - // abort - close(); - return ERR; - } - - // restart decoding - m_curFrameIndex = getFrameCount(); - seekFrame(0); - - return OK; -} - -void SoundSourceMp3::close() { - m_seekFrameList.clear(); - m_avgSeekFrameCount = 0; - m_curFrameIndex = 0; - - mad_synth_finish(&m_madSynth); - mad_frame_finish(&m_madFrame); - mad_stream_finish(&m_madStream); - m_madSynthCount = 0; - - m_file.unmap(m_pFileData); - m_fileSize = 0; - m_pFileData = NULL; - m_file.close(); - - Super::reset(); -} - -SoundSourceMp3::MadSeekFrameList::size_type SoundSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { - if ((0 >= frameIndex) || m_seekFrameList.empty()) { - return 0; - } - // Guess position of frame in m_seekFrameList based on average frame size - SoundSourceMp3::MadSeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; - if (seekFrameIndex >= m_seekFrameList.size()) { - seekFrameIndex = m_seekFrameList.size() - 1; - } - // binary search starting at seekFrameIndex - SoundSourceMp3::MadSeekFrameList::size_type lowerBound = 0; - SoundSourceMp3::MadSeekFrameList::size_type upperBound = m_seekFrameList.size(); - while ((upperBound - lowerBound) > 1) { - Q_ASSERT(seekFrameIndex >= lowerBound); - Q_ASSERT(seekFrameIndex < upperBound); - if (m_seekFrameList[seekFrameIndex].frameIndex > frameIndex) { - upperBound = seekFrameIndex; - } else { - lowerBound = seekFrameIndex; - } - seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; - } - Q_ASSERT(m_seekFrameList.size() > seekFrameIndex); - Q_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); - Q_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); - return seekFrameIndex; -} - -Mixxx::AudioSource::diff_type SoundSourceMp3::seekFrame(diff_type frameIndex) { - if (m_curFrameIndex == frameIndex) { - return m_curFrameIndex; - } - if (0 > frameIndex) { - return seekFrame(0); - } - // simply skip frames when jumping no more than kMaxSkipFrameSamplesWhenSeeking frames forward - if ((frameIndex < m_curFrameIndex) || ((frameIndex - m_curFrameIndex) > kMaxSkipFrameSamplesWhenSeeking)) { - MadSeekFrameList::size_type seekFrameIndex = findSeekFrameIndex(frameIndex); - if (seekFrameIndex <= kSeekFramePrefetchCount) { - seekFrameIndex = 0; - } else { - seekFrameIndex -= kSeekFramePrefetchCount; - } - Q_ASSERT(seekFrameIndex < m_seekFrameList.size()); - const MadSeekFrameType& seekFrame(m_seekFrameList[seekFrameIndex]); - // restart decoder - mad_synth_finish(&m_madSynth); - mad_frame_finish(&m_madFrame); - mad_stream_finish(&m_madStream); - mad_stream_init(&m_madStream); - mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(&m_madStream, seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); - mad_frame_init(&m_madFrame); - mad_synth_init(&m_madSynth); - m_curFrameIndex = seekFrame.frameIndex; - m_madSynthCount = 0; - } - // decode and discard prefetch data - Q_ASSERT(m_curFrameIndex <= frameIndex); - skipFrameSamples(frameIndex - m_curFrameIndex); - Q_ASSERT(m_curFrameIndex == frameIndex); - return m_curFrameIndex; -} - -Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); -} - -Mixxx::AudioSource::size_type SoundSourceMp3::readStereoFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); -} - -Mixxx::AudioSource::size_type SoundSourceMp3::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer, - bool readStereoSamples) { - size_type framesRemaining = frameCount; - sample_type* pSampleBuffer = sampleBuffer; - while (0 < framesRemaining) { - if (0 >= m_madSynthCount) { - if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { - if (MAD_RECOVERABLE(m_madStream.error)) { - if (MAD_ERROR_LOSTSYNC == m_madStream.error) { - // Ignore LOSTSYNC due to ID3 tags - mad_skip_id3_tag(&m_madStream); - } else { - qDebug() << "Recoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); - } - continue; - } else { - if (MAD_ERROR_BUFLEN != m_madStream.error) { - qWarning() << "Unrecoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); - } - break; - } - } - /* Once decoded the frame is synthesized to PCM samples. No ERRs - * are reported by mad_synth_frame(); - */ - mad_synth_frame(&m_madSynth, &m_madFrame); - m_madSynthCount = m_madSynth.pcm.length; - } - const size_type madSynthOffset = m_madSynth.pcm.length - m_madSynthCount; - const size_type framesRead = math_min(m_madSynthCount, framesRemaining); - m_madSynthCount -= framesRead; - m_curFrameIndex += framesRead; - framesRemaining -= framesRead; - if (NULL != pSampleBuffer) { - if (isChannelCountMono()) { - for (size_type i = 0; i < framesRead; ++i) { - const sample_type sampleValue = madScale( - m_madSynth.pcm.samples[0][madSynthOffset + i]); - *(pSampleBuffer++) = sampleValue; - if (readStereoSamples) { - *(pSampleBuffer++) = sampleValue; - } - } - } else if (isChannelCountStereo() || readStereoSamples) { - for (size_type i = 0; i < framesRead; ++i) { - *(pSampleBuffer++) = madScale( - m_madSynth.pcm.samples[0][madSynthOffset + i]); - *(pSampleBuffer++) = madScale( - m_madSynth.pcm.samples[1][madSynthOffset + i]); - } - } else { - for (size_type i = 0; i < framesRead; ++i) { - for (size_type j = 0; j < getChannelCount(); ++j) { - *(pSampleBuffer++) = madScale( - m_madSynth.pcm.samples[j][madSynthOffset + i]); - } - } - } - } - } - return frameCount - framesRemaining; -} - -Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) { +Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { @@ -417,7 +60,7 @@ Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -QImage SoundSourceMp3::parseCoverArt() { +QImage SoundSourceMp3::parseCoverArt() const { QImage coverArt; TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); @@ -432,3 +75,7 @@ QImage SoundSourceMp3::parseCoverArt() { } return coverArt; } + +Mixxx::AudioSourcePointer SoundSourceMp3::open() const { + return Mixxx::AudioSourceMp3::open(getFilename()); +} diff --git a/src/soundsourcemp3.h b/src/soundsourcemp3.h index 4410356e257..11c2c994693 100644 --- a/src/soundsourcemp3.h +++ b/src/soundsourcemp3.h @@ -20,21 +20,6 @@ #include "soundsource.h" -#ifdef _MSC_VER - // So mad.h doesn't try to use inline assembly which MSVC doesn't support. - // Notably, FPM_64BIT does not require a 64-bit machine. It merely requires a - // compiler that supports 64-bit types. - #define FPM_64BIT -#endif -#include -#include -#include -#include - -#include - -#include - /** *@author Tue and Ken Haste Andersen */ @@ -46,56 +31,11 @@ class SoundSourceMp3 : public Mixxx::SoundSource { static QList supportedFileExtensions(); explicit SoundSourceMp3(QString qFilename); - ~SoundSourceMp3(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - QImage parseCoverArt() /*override*/; - - Result open() /*override*/; - - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - -private: - void close(); - - inline size_type skipFrameSamples(size_type frameCount) { - return readFrameSamplesInterleaved(frameCount, NULL); - } - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); - - QFile m_file; - quint64 m_fileSize; - unsigned char* m_pFileData; - - mad_stream m_madStream; - - /** Struct used to store mad frames for seeking */ - struct MadSeekFrameType { - diff_type frameIndex; - const unsigned char* pFileData; - }; - - /** It is not possible to make a precise seek in an mp3 file without decoding the whole stream. - * To have precise seek within a limited range from the current decode position, we keep track - * of past decodeded frame, and their exact position. If a seek occours and it is within the - * range of frames we keep track of a precise seek occours, otherwise an unprecise seek is performed - */ - typedef std::vector MadSeekFrameList; - MadSeekFrameList m_seekFrameList; // ordered-by frameIndex - size_type m_avgSeekFrameCount; // avg. samples frames per MP3 frame - - /** Returns the position of the frame which was found. The found frame is set to - * the current element in m_qSeekList */ - MadSeekFrameList::size_type findSeekFrameIndex(diff_type frameIndex) const; - diff_type m_curFrameIndex; + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; - // current play position - mad_frame m_madFrame; - mad_synth m_madSynth; - size_type m_madSynthCount; // left overs from the previous read + Mixxx::AudioSourcePointer open() const /*override*/; }; #endif diff --git a/src/soundsourceoggvorbis.cpp b/src/soundsourceoggvorbis.cpp index 66138e32cb6..7824fbac05f 100644 --- a/src/soundsourceoggvorbis.cpp +++ b/src/soundsourceoggvorbis.cpp @@ -16,12 +16,11 @@ #include "soundsourceoggvorbis.h" +#include "audiosourceoggvorbis.h" #include "trackmetadatataglib.h" #include -#include - QList SoundSourceOggVorbis::supportedFileExtensions() { QList list; list.push_back("ogg"); @@ -30,127 +29,12 @@ QList SoundSourceOggVorbis::supportedFileExtensions() { SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) : Super(qFilename, "ogg") { - memset(&m_vf, 0, sizeof(m_vf)); -} - -SoundSourceOggVorbis::~SoundSourceOggVorbis() { - if (0 != ov_clear(&m_vf)) { - qWarning() << "Failed to close OggVorbis file:" << getFilename(); - } -} - -Result SoundSourceOggVorbis::open() { - - if (0 != ov_fopen(getFilename().toLocal8Bit().constData(), &m_vf)) { - qWarning() << "Failed to open OggVorbis file:" << getFilename(); - return ERR; - } - - if (!ov_seekable(&m_vf)) { - qWarning() << "OggVorbis file is not seekable:" << getFilename(); - close(); - return ERR; - } - - // lookup the ogg's channels and samplerate - const vorbis_info* vi = ov_info(&m_vf, -1); - if (!vi) { - qWarning() << "Failed to read OggVorbis file:" << getFilename(); - close(); - return ERR; - } - setChannelCount(vi->channels); - setFrameRate(vi->rate); - - ogg_int64_t frameCount = ov_pcm_total(&m_vf, -1); - if (0 <= frameCount) { - setFrameCount(frameCount); - } else { - qWarning() << "Failed to read OggVorbis file:" << getFilename(); - close(); - return ERR; - } - - return OK; -} - -void SoundSourceOggVorbis::close() { - if (0 != ov_clear(&m_vf)) { - qWarning() << "Failed to close OggVorbis file:" << getFilename(); - } - Super::reset(); -} - -Mixxx::AudioSource::diff_type SoundSourceOggVorbis::seekFrame( - diff_type frameIndex) { - int seekResult = ov_pcm_seek(&m_vf, frameIndex); - if (0 != seekResult) { - qWarning() << "Failed to seek OggVorbis file:" << getFilename(); - } - return ov_pcm_tell(&m_vf); -} - -Mixxx::AudioSource::size_type SoundSourceOggVorbis::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); -} - -Mixxx::AudioSource::size_type SoundSourceOggVorbis::readStereoFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); -} - -Mixxx::AudioSource::size_type SoundSourceOggVorbis::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer, - bool readStereoSamples) { - size_type readCount = 0; - sample_type* nextSample = sampleBuffer; - while (readCount < frameCount) { - float** pcmChannels; - int currentSection; - long readResult = ov_read_float(&m_vf, &pcmChannels, - frameCount - readCount, ¤tSection); - if (0 == readResult) { - break; // done - } - if (0 < readResult) { - if (isChannelCountMono()) { - if (readStereoSamples) { - for (long i = 0; i < readResult; ++i) { - *nextSample++ = pcmChannels[0][i]; - *nextSample++ = pcmChannels[0][i]; - } - } else { - for (long i = 0; i < readResult; ++i) { - *nextSample++ = pcmChannels[0][i]; - } - } - } else if (isChannelCountStereo() || readStereoSamples) { - for (long i = 0; i < readResult; ++i) { - *nextSample++ = pcmChannels[0][i]; - *nextSample++ = pcmChannels[1][i]; - } - } else { - for (long i = 0; i < readResult; ++i) { - for (size_type j = 0; j < getChannelCount(); ++j) { - *nextSample++ = pcmChannels[j][i]; - } - } - } - readCount += readResult; - } else { - qWarning() << "Failed to read sample data from OggVorbis file:" - << getFilename(); - break; // abort - } - } - return readCount; } /* Parse the the file to get metadata */ -Result SoundSourceOggVorbis::parseMetadata(Mixxx::TrackMetadata* pMetadata) { +Result SoundSourceOggVorbis::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { TagLib::Ogg::Vorbis::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { @@ -173,7 +57,7 @@ Result SoundSourceOggVorbis::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -QImage SoundSourceOggVorbis::parseCoverArt() { +QImage SoundSourceOggVorbis::parseCoverArt() const { TagLib::Ogg::Vorbis::File f(getFilename().toLocal8Bit().constData()); TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { @@ -182,3 +66,7 @@ QImage SoundSourceOggVorbis::parseCoverArt() { return QImage(); } } + +Mixxx::AudioSourcePointer SoundSourceOggVorbis::open() const { + return Mixxx::AudioSourceOggVorbis::open(getFilename()); +} diff --git a/src/soundsourceoggvorbis.h b/src/soundsourceoggvorbis.h index 784df556c94..ef44671229f 100644 --- a/src/soundsourceoggvorbis.h +++ b/src/soundsourceoggvorbis.h @@ -19,9 +19,6 @@ #include "soundsource.h" -#define OV_EXCLUDE_STATIC_CALLBACKS -#include - class SoundSourceOggVorbis: public Mixxx::SoundSource { typedef SoundSource Super; @@ -29,26 +26,11 @@ class SoundSourceOggVorbis: public Mixxx::SoundSource { static QList supportedFileExtensions(); explicit SoundSourceOggVorbis(QString qFilename); - ~SoundSourceOggVorbis(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - QImage parseCoverArt() /*override*/; - - Result open() /*override*/; - - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; - size_type readStereoFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; - -private: - void close(); - size_type readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer, bool readStereoSamples); + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; - OggVorbis_File m_vf; + Mixxx::AudioSourcePointer open() const /*override*/; }; #endif diff --git a/src/soundsourceopus.cpp b/src/soundsourceopus.cpp index 09623d963b7..a3dd74adcb4 100644 --- a/src/soundsourceopus.cpp +++ b/src/soundsourceopus.cpp @@ -1,5 +1,6 @@ #include "soundsourceopus.h" +#include "audiosourceopus.h" #include "trackmetadatataglib.h" // Include this if taglib if new enough (version 1.9.1 have opusfile) @@ -7,11 +8,6 @@ #include #endif -namespace { -// All Opus audio is encoded at 48 kHz -Mixxx::AudioSource::size_type kOpusSampleRate = 48000; -} - QList SoundSourceOpus::supportedFileExtensions() { QList list; list.push_back("opus"); @@ -19,103 +15,7 @@ QList SoundSourceOpus::supportedFileExtensions() { } SoundSourceOpus::SoundSourceOpus(QString qFilename) - : Super(qFilename, "opus"), m_pOggOpusFile(NULL) { -} - -SoundSourceOpus::~SoundSourceOpus() { - close(); - if (m_pOggOpusFile) { - op_free(m_pOggOpusFile); - } -} - -Result SoundSourceOpus::open() { - - int errorCode = 0; - m_pOggOpusFile = op_open_file(getFilename().toLocal8Bit().constData(), &errorCode); - if (!m_pOggOpusFile) { - qDebug() << "Failed to open OggOpus file:" << getFilename() - << "errorCode" << errorCode; - return ERR; - } - - if (!op_seekable(m_pOggOpusFile)) { - qWarning() << "OggOpus file is not seekable:" << getFilename(); - close(); - return ERR; - } - - setChannelCount(op_channel_count(m_pOggOpusFile, -1)); - setFrameRate(kOpusSampleRate); - - ogg_int64_t frameCount = op_pcm_total(m_pOggOpusFile, -1); - if (0 <= frameCount) { - setFrameCount(frameCount); - } else { - qWarning() << "Failed to read OggOpus file:" << getFilename(); - close(); - return ERR; - } - - return OK; -} - -void SoundSourceOpus::close() { - if (m_pOggOpusFile) { - op_free(m_pOggOpusFile); - m_pOggOpusFile = NULL; - } - Super::reset(); -} - -Mixxx::AudioSource::diff_type SoundSourceOpus::seekFrame(diff_type frameIndex) { - int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); - if (0 != seekResult) { - qWarning() << "Failed to seek OggVorbis file:" << getFilename(); - } - return op_pcm_tell(m_pOggOpusFile); -} - -Mixxx::AudioSource::size_type SoundSourceOpus::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - size_type readCount = 0; - while (readCount < frameCount) { - int readResult = op_read_float(m_pOggOpusFile, - sampleBuffer + frames2samples(readCount), - frames2samples(frameCount - readCount), NULL); - if (0 == readResult) { - break; // done - } - if (0 < readResult) { - readCount += readResult; - } else { - qWarning() << "Failed to read sample data from OggOpus file:" - << getFilename(); - break; // abort - } - } - return readCount; -} - -Mixxx::AudioSource::size_type SoundSourceOpus::readStereoFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - size_type readCount = 0; - while (readCount < frameCount) { - int readResult = op_read_float_stereo(m_pOggOpusFile, - sampleBuffer + (readCount * 2), - (frameCount - readCount) * 2); - if (0 == readResult) { - break; // done - } - if (0 < readResult) { - readCount += readResult; - } else { - qWarning() << "Failed to read sample data from OggOpus file:" - << getFilename(); - break; // abort - } - } - return readCount; + : Super(qFilename, "opus") { } namespace @@ -139,14 +39,14 @@ namespace /* Parse the the file to get metadata */ -Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) { +Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { const QByteArray qbaFilename(getFilename().toLocal8Bit()); int error = 0; OggOpusFileOwner l_ptrOpusFile(op_open_file(qbaFilename.constData(), &error)); pMetadata->setChannels(op_channel_count(l_ptrOpusFile, -1)); - pMetadata->setSampleRate(kOpusSampleRate); + pMetadata->setSampleRate(Mixxx::AudioSourceOpus::kFrameRate); pMetadata->setBitrate(op_bitrate(l_ptrOpusFile, -1) / 1000); pMetadata->setDuration(op_pcm_total(l_ptrOpusFile, -1) / pMetadata->getSampleRate()); @@ -217,7 +117,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -QImage SoundSourceOpus::parseCoverArt() { +QImage SoundSourceOpus::parseCoverArt() const { #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) TagLib::Ogg::Opus::File f(getFilename().toLocal8Bit().constData()); TagLib::Ogg::XiphComment *xiph = f.tag(); @@ -227,3 +127,7 @@ QImage SoundSourceOpus::parseCoverArt() { #endif return QImage(); } + +Mixxx::AudioSourcePointer SoundSourceOpus::open() const { + return Mixxx::AudioSourceOpus::open(getFilename()); +} diff --git a/src/soundsourceopus.h b/src/soundsourceopus.h index 82bfc6ff571..a52c1cccc04 100644 --- a/src/soundsourceopus.h +++ b/src/soundsourceopus.h @@ -3,9 +3,6 @@ #include "soundsource.h" -#define OV_EXCLUDE_STATIC_CALLBACKS -#include - class SoundSourceOpus: public Mixxx::SoundSource { typedef SoundSource Super; @@ -13,23 +10,11 @@ class SoundSourceOpus: public Mixxx::SoundSource { static QList supportedFileExtensions(); explicit SoundSourceOpus(QString qFilename); - ~SoundSourceOpus(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - QImage parseCoverArt() /*override*/; - - Result open() /*override*/; - - diff_type seekFrame(diff_type frameIndex); - size_type readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; - size_type readStereoFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; -private: - void close(); + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; - OggOpusFile *m_pOggOpusFile; + Mixxx::AudioSourcePointer open() const /*override*/; }; #endif diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index a5050df358c..640ffaf96a6 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -299,41 +299,41 @@ QLibrary* SoundSourceProxy::getPlugin(QString lib_filename) } -Mixxx::SoundSourcePointer SoundSourceProxy::open() const { +Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource() const { if (!m_pSoundSource) { - return Mixxx::SoundSourcePointer(); + return Mixxx::AudioSourcePointer(); } - Result retVal = m_pSoundSource->open(); - if (OK != retVal) { + Mixxx::AudioSourcePointer pAudioSource(m_pSoundSource->open()); + if (!pAudioSource) { qWarning() << "Failed to open SoundSource"; - return Mixxx::SoundSourcePointer(); + return Mixxx::AudioSourcePointer(); } - if (!m_pSoundSource->isValid()) { + if (!pAudioSource->isValid()) { qWarning() << "Invalid file:" << m_pSoundSource->getFilename() - << "channels" << m_pSoundSource->getChannelCount() - << "frame rate" << m_pSoundSource->getChannelCount(); - return Mixxx::SoundSourcePointer(); + << "channels" << pAudioSource->getChannelCount() + << "frame rate" << pAudioSource->getChannelCount(); + return Mixxx::AudioSourcePointer(); } - if (m_pSoundSource->isEmpty()) { + if (pAudioSource->isEmpty()) { qWarning() << "Empty file:" << m_pSoundSource->getFilename(); - return Mixxx::SoundSourcePointer(); + return Mixxx::AudioSourcePointer(); } // Overwrite metadata with actual audio properties if (m_pTrack) { - m_pTrack->setChannels(m_pSoundSource->getChannelCount()); - m_pTrack->setSampleRate(m_pSoundSource->getFrameRate()); - if (m_pSoundSource->hasDuration()) { - m_pTrack->setDuration(m_pSoundSource->getDuration()); + m_pTrack->setChannels(pAudioSource->getChannelCount()); + m_pTrack->setSampleRate(pAudioSource->getFrameRate()); + if (pAudioSource->hasDuration()) { + m_pTrack->setDuration(pAudioSource->getDuration()); } - if (m_pSoundSource->hasBitrate()) { - m_pTrack->setBitrate(m_pSoundSource->getBitrate()); + if (pAudioSource->hasBitrate()) { + m_pTrack->setBitrate(pAudioSource->getBitrate()); } } - return m_pSoundSource; + return pAudioSource; } // static diff --git a/src/soundsourceproxy.h b/src/soundsourceproxy.h index 2b4899347e5..631d8d1f355 100644 --- a/src/soundsourceproxy.h +++ b/src/soundsourceproxy.h @@ -45,7 +45,7 @@ class SoundSourceProxy static QStringList supportedFileExtensionsByPlugins(); static QString supportedFileExtensionsString(); static QString supportedFileExtensionsRegex(); - static bool isFilenameSupported(QString filename); + static bool isFilenameSupported(QString fileName); SoundSourceProxy(QString qFilename, SecurityTokenPointer pToken); explicit SoundSourceProxy(TrackPointer pTrack); @@ -54,10 +54,10 @@ class SoundSourceProxy return m_pSoundSource; } - // Opens the audio data through the proxy will + // Opening the audio data through the proxy will // update the some metadata of the track object. // Returns a null pointer on failure. - Mixxx::SoundSourcePointer open() const; + Mixxx::AudioSourcePointer openAudioSource() const; private: static QRegExp m_supportedFileRegex; diff --git a/src/soundsourcesndfile.cpp b/src/soundsourcesndfile.cpp index c126a0cf124..9040b69b92a 100644 --- a/src/soundsourcesndfile.cpp +++ b/src/soundsourcesndfile.cpp @@ -1,5 +1,6 @@ #include "soundsourcesndfile.h" +#include "audiosourcesndfile.h" #include "trackmetadatataglib.h" #include @@ -17,82 +18,10 @@ QList SoundSourceSndFile::supportedFileExtensions() { } SoundSourceSndFile::SoundSourceSndFile(QString qFilename) - : Super(qFilename), m_pSndFile(NULL) { - memset(&m_sfInfo, 0, sizeof(m_sfInfo)); + : Super(qFilename) { } -SoundSourceSndFile::~SoundSourceSndFile() { - close(); -} - -Result SoundSourceSndFile::open() { -#ifdef __WINDOWS__ - // Pointer valid until string changed - LPCWSTR lpcwFilename = (LPCWSTR)getFilename().utf16(); - m_pSndFile = sf_wchar_open(lpcwFilename, SFM_READ, &m_sfInfo); -#else - m_pSndFile = sf_open(getFilename().toLocal8Bit().constData(), SFM_READ, &m_sfInfo); -#endif - - if (m_pSndFile == NULL) { // sf_format_check is only for writes - qWarning() << "libsndfile: Error opening file" << getFilename() - << sf_strerror(m_pSndFile); - return ERR; - } - - if (sf_error(m_pSndFile) > 0) { - qWarning() << "libsndfile: Error opening file" << getFilename() - << sf_strerror(m_pSndFile); - close(); - return ERR; - } - - setChannelCount(m_sfInfo.channels); - setFrameRate(m_sfInfo.samplerate); - setFrameCount(m_sfInfo.frames); - - return OK; -} - -void SoundSourceSndFile::close() { - if (m_pSndFile) { - if (0 == sf_close(m_pSndFile)) { - m_pSndFile = NULL; - memset(&m_sfInfo, 0, sizeof(m_sfInfo)); - Super::reset(); - } else { - qWarning() << "Failed to close file:" << getFilename() - << sf_strerror(m_pSndFile); - } - } -} - -Mixxx::AudioSource::diff_type SoundSourceSndFile::seekFrame( - diff_type frameIndex) { - const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); - if (0 <= seekResult) { - return seekResult; - } else { - qWarning() << "Failed to seek libsnd file:" << getFilename() - << sf_strerror(m_pSndFile); - return sf_seek(m_pSndFile, 0, SEEK_CUR); - } -} - -Mixxx::AudioSource::size_type SoundSourceSndFile::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, frameCount); - if (0 <= readCount) { - return readCount; - } else { - qWarning() << "Failed to read sample data from libsnd file:" - << getFilename() << sf_strerror(m_pSndFile); - return 0; - } -} - -Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) { - +Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { if (getType() == "flac") { TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { @@ -132,25 +61,6 @@ Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return ERR; } } - - if (pMetadata->getDuration() <= 0) { - // we're using a taglib version which doesn't know how to do wav - // durations, set it with m_sfInfo from sndfile -bkgood - // XXX remove this when ubuntu ships with an sufficiently - // intelligent version of taglib, should happen in 11.10 - - // Have to open the file for m_sfInfo to be valid. - if (m_pSndFile == NULL) { - open(); - } - - if (m_sfInfo.samplerate > 0) { - pMetadata->setDuration(m_sfInfo.frames / m_sfInfo.samplerate); - } else { - qDebug() << "WARNING: WAV file with invalid samplerate." - << "Can't get duration using libsndfile."; - } - } } else if (getType().startsWith("aif")) { // Try AIFF TagLib::RIFF::AIFF::File f(getFilename().toLocal8Bit().constData()); @@ -170,7 +80,7 @@ Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) { return OK; } -QImage SoundSourceSndFile::parseCoverArt() { +QImage SoundSourceSndFile::parseCoverArt() const { QImage coverArt; if (getType() == "flac") { @@ -211,3 +121,7 @@ QImage SoundSourceSndFile::parseCoverArt() { return coverArt; } + +Mixxx::AudioSourcePointer SoundSourceSndFile::open() const { + return Mixxx::AudioSourceSndFile::open(getFilename()); +} diff --git a/src/soundsourcesndfile.h b/src/soundsourcesndfile.h index 54ddad02586..28eb5d1c97f 100644 --- a/src/soundsourcesndfile.h +++ b/src/soundsourcesndfile.h @@ -3,14 +3,6 @@ #include "soundsource.h" -#ifdef Q_OS_WIN -//Enable unicode in libsndfile on Windows -//(sf_open uses UTF-8 otherwise) -#include -#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 -#endif -#include - class SoundSourceSndFile: public Mixxx::SoundSource { typedef SoundSource Super; @@ -18,22 +10,11 @@ class SoundSourceSndFile: public Mixxx::SoundSource { static QList supportedFileExtensions(); explicit SoundSourceSndFile(QString qFilename); - ~SoundSourceSndFile(); - - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) /*override*/; - QImage parseCoverArt() /*override*/; - - Result open() /*override*/; - - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, - sample_type* sampleBuffer) /*override*/; -private: - void close(); + Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + QImage parseCoverArt() const /*override*/; - SNDFILE* m_pSndFile; - SF_INFO m_sfInfo; + Mixxx::AudioSourcePointer open() const /*override*/; }; #endif diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 9a17f5bdce7..841aceeb462 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -2,7 +2,6 @@ #include #include -#include #include "test/mixxxtest.h" @@ -11,12 +10,12 @@ class SoundSourceProxyTest : public MixxxTest { protected: - Mixxx::SoundSourcePointer loadProxy(const QString& track) { - m_pProxy.reset(new SoundSourceProxy(track, SecurityTokenPointer())); - return m_pProxy->getSoundSource(); + Mixxx::SoundSourcePointer openSoundSource(const QString& fileName) { + return SoundSourceProxy(fileName, SecurityTokenPointer()).getSoundSource(); + } + Mixxx::AudioSourcePointer openAudioSource(const QString& fileName) { + return SoundSourceProxy(fileName, SecurityTokenPointer()).openAudioSource(); } - - QScopedPointer m_pProxy; }; TEST_F(SoundSourceProxyTest, ProxyCanOpen) { @@ -31,30 +30,29 @@ TEST_F(SoundSourceProxyTest, ProxyCanOpen) { QString filePath = kCoverFilePath + extension; EXPECT_TRUE(SoundSourceProxy::isFilenameSupported(filePath)); - Mixxx::SoundSourcePointer pSoundSource(loadProxy(filePath)); - ASSERT_TRUE(!pSoundSource.isNull()); - EXPECT_EQ(OK, pSoundSource->open()); - EXPECT_LT(0UL, pSoundSource->getChannelCount()); - EXPECT_LT(0UL, pSoundSource->getFrameRate()); - EXPECT_LT(0UL, pSoundSource->getFrameCount()); + Mixxx::AudioSourcePointer pAudioSource(openAudioSource(filePath)); + ASSERT_TRUE(!pAudioSource.isNull()); + EXPECT_LT(0UL, pAudioSource->getChannelCount()); + EXPECT_LT(0UL, pAudioSource->getFrameRate()); + EXPECT_LT(0UL, pAudioSource->getFrameCount()); } } TEST_F(SoundSourceProxyTest, readArtist) { - Mixxx::SoundSourcePointer p(loadProxy( + Mixxx::SoundSourcePointer pSoundSource(openSoundSource( QDir::currentPath().append("/src/test/id3-test-data/artist.mp3"))); - ASSERT_TRUE(!p.isNull()); + ASSERT_TRUE(!pSoundSource.isNull()); Mixxx::TrackMetadata trackMetadata; - EXPECT_EQ(OK, p->parseMetadata(&trackMetadata)); + EXPECT_EQ(OK, pSoundSource->parseMetadata(&trackMetadata)); EXPECT_EQ("Test Artist", trackMetadata.getArtist()); } TEST_F(SoundSourceProxyTest, TOAL_TPE2) { - Mixxx::SoundSourcePointer p(loadProxy( + Mixxx::SoundSourcePointer pSoundSource(openSoundSource( QDir::currentPath().append("/src/test/id3-test-data/TOAL_TPE2.mp3"))); - ASSERT_TRUE(!p.isNull()); + ASSERT_TRUE(!pSoundSource.isNull()); Mixxx::TrackMetadata trackMetadata; - EXPECT_EQ(OK, p->parseMetadata(&trackMetadata)); + EXPECT_EQ(OK, pSoundSource->parseMetadata(&trackMetadata)); EXPECT_EQ("TITLE2", trackMetadata.getArtist()); EXPECT_EQ("ARTIST", trackMetadata.getAlbum()); EXPECT_EQ("TITLE", trackMetadata.getAlbumArtist()); From 98ef76c05135795dd9115df16d002e74a7d5eb0c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 18 Dec 2014 00:07:20 +0100 Subject: [PATCH 025/481] Delete binary file from git repository --- plugins/soundsource/libsoundsourcem4a.so | Bin 1300576 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100755 plugins/soundsource/libsoundsourcem4a.so diff --git a/plugins/soundsource/libsoundsourcem4a.so b/plugins/soundsource/libsoundsourcem4a.so deleted file mode 100755 index 527047e91b9a6d65c86da43aab9c8622f0fa16bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1300576 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%0DmpVUH+W4#Zo~A1+~L3Y z2RrBKI{$^pkB3T~{)C+1C8z>c;T(?FslOs2H+TWERHG+8VGJsRSal*NP!H@uG<$=G zc8$|ZV}}2{Pv~^*@Rtzsa0V9Vt*V^7la85+4Z}4xTXd%kz!(^QZg*j z${-vwCUSz8A|@&kTwnUn=6Q{dhoQ);;{iK;1Zut$4`oaU9t&|D589e+oNk~a!Hb;C zVJ*%>ovjjWpYbtlZ+K9g@5BR-a|*ya>AG>9pa{Ad7lppyzt}E%%jU&qtI!+xGOG)# zam+$?qu<+~&8tg5^Y%@~N_R~rS4I2}D$}wGTGo($$HBUNlUX!L?c2If`z9bqRQv9A*T0YDc}~MTY2M8rQNq38b#UC-p?NPt>#l?w7bVLA z27SMfHLL3(t=n6SctX;$uyU%~+vA#fXv{K~Y0U?rk!u0T9IrLK!GZ)|!#rutG(EoR z_MFb#(!YJdk!a6Mr#`nl-0Y2Q2JWp40b zZ{3<6NU7e*Y1e$D16G64csK4Cjqo+1@vn%+Uu3=v@jpb3`5vQ>GhZIfsgmPO4e~G^o!JuO%@%k4 zRC~OcBqHtwV?oL!l#8eg&q7OKjSrF8h$|Wnvk*FMrP%aDG84VQTsUNMf^YaI?)pE1 zz*?SjP)%odg!UBqC0agygkB0oSHc;s_IR`QebnE?XBFa;v$D)TzcPXC+p5PK8j}NK zyumAEyjf1;u$l@UnAMfnJpui=A@+c?4Ls|{bQy4*DZv*!o6~UJ9civ+UB*M@W9$>oq(WTbV6P{Y$M+zIeg+?}{kMFG;`%zPPhI*Q2?mM*{6&j17 zNB7@Y^UFeHiyD7+y6cZYp78uqEF$g&y+Q8V-s@oy+q)UpaC=`bX1$WkL~n33R!rr3 zf=K}}DtH2;8Dmo2*=9uQFUA}BcD0QZoyM@%P&ETL2*9&NFeHQ4X;N>0fQ=Aw5uqbn^+-i_+{uzc-gHM7iwvqY?VP7=Mr zIp~xZqj!Jku0I`8!Sn07dR%i=XWdz*txpllU(wS0)7DYz%gygP?H#qA_=A|y-QKh{ zCs>C5t=E@{GSx({Cys);Sncg*F>nXdP4}+Xi_D;#9%x2m`ht%#pwXr-oPo+J6|6M) zhddFZ3=*BEMDF^J#iv$-qkrp%EsrCG4W6nS{OTUM!QcFk)8OyoE=GeN6?!S5yMOQ7 z-pi0}-}b%%d7SpXK_uJ@D#7uw7TP-->kIBF>XTE%XN!_OxT3vXGMYaqMm?bugMUSP zV?-bRuDhQ9-FdU%xY8QUhluG*w0!&)=ju_9<`L_SAK&S2?}+jIOfjQ-Jg2QWFo*5H zC5AUR4?{nmS5gkw$@+?eBsGP%@HSWQX>X1S;JF4i6&>nQ@ILgfBu0dbI(RU=B`0{4 zFZgT)YNrz3hD2a9rt7S(kGSr91O&7GL$0mrMw*L-4Lzo28NlwMCL6$!Qso)YNHEfX zLV}?Nq!RqmfJB1h2(DYrZSx9X`>6+?3#7>UhOZHD`l zO zOitY@sdTnY%@H79X0RR+oz* z56{GLiVjHYu$$hdNPrIhgbL=C(OkyRd!M(7CFCIPUvvl z%VM?za1)>(Xz0zG-h%gZyzqqRcp|y2+Y4uiio26lw-+YB3+IRz+;5_`G0qtdA`dTg z@oK!lKx~A5^X3aICJH-Ynv|oTVc{JCv_D^TaMy2wkW35O2i?=c3Q539KH518enhX4 zg(6baaqAJG-w4Bl-F}2m{Hg^Q3~J9hFuM&yC#n#c@33n~`#$t{ql-javu`8SCVU~xRHuaH;ib!hRjgzNzg3xhbkp= zi0o3%t1yb&kicUJxiY5Jg<5--n2qVjjAHuStq@J6c0!MH?p6rn?NO}z+>MKTYzmp- zeP)1iIdvoBoVwK@H%qU;Ri?A>0I^v;C8g+L>{;GpfOtu`SCYkydjLX*h!dj++X|MN zE5aC4QL@Lj(Hd#%lWQnCh@$%ZPBYGYyHpHB=r{&)4DxWWl}Tr-N%P z3bD05c{y#hr-W^4iWEX$u2Iu@4`0p^4&g(k;{ZbU{y@zk@s?D(9?b;@YNOVIG+h)~IIQ!^-u- zX;akW03r1wH6Kxq`Phr2iIh;?RuZqsP^K2HhR8?}0fXRHbIBa#Fz}6Lu33Dx{ z%y=?A4I{lLlgzQ(wM-Lb&LY$CU#qO+$XxwB89q4aM>s*CF=o#yFjuu}fjspc;RFLS z)fa?k5%TH47F7o9ZNL!9KHdyv1hP2!rvZ&jdDsA1oc!GYIK!nD8IVYDEdd;ODzxgC zu6%(_YQ7PXdH!+{=@5Uk{|i>I9*OFCQ#_o;l2@X<;>XduLVw@R%83|%efz9OHX zo+mR)7-It@|H=t27tX~F3IN&SwgVP0Q$H6F7w9NMIyeb zJ%|MZ(sQwdI2RPE=nL#nX}ArQ$zW&fJb}#V+RpYnasq3oH@0u!Nfk4+s|w?68A&D( zz?92ym2W~jo@Wh)lDvi^D<002;3{icIBWQ?{z<;zFm*D_^bP;P@5O_bc8o2n+8@S0 z)OQH8j+Cq{*Nbu&zQI~=QMI^+j-|GM9G`bW6csT~r8-m#v+m&-j8Y3(4Fm(*A6PxW zy2-qP#7z*gOd9VCo~jm8+wgDw*(i!8iTA%?k+RX{Ub^;WLeTCmYfYCR*v6T68o*x{=VP^>N!dM)!!Sd{&v17qV5NS=U?CcqE#Lv<`pky+wT1XX0@o>n3sTm}vYrPJyP9ixDq-EJfwCosT*$A-=`P=`F zf2iEWEXOlTV68WZ2}x~(dl50}hSwdZs&)v$S)n4dH>>q;;PwD>z#>i#>QQ8Be_e;M zdIib3fo<(y={8Y!BN6?g{XL3v5a14Ao)oi1oq%HK?$7o_n4M0ur?OmxbBZxr!s97q z2t5gDJoTa{|MMDe+e3GPqH$*-Q>cOID2?L}-E}ZZ3)M)bRiUZo`i=M=y|$HMa|86S z2V8?|=oE-Z4?u_Bsb)(NklLgQnR>RQuAy`uFzfmu>oD3Vz6OfLnJoHVRuHDhta}}e zpU-9k(`k$=CHctDAfaP1u2%C+Vc1_)dmEI{l?woJ1K+i8 zqVjYUV!2@;2egS$#{umKGoVR0`~$f{x1wCyHwY5wZ`TWyS}uPD$(1xqqJ?(c1B&I{ z#-vM-1bgId43sPHZ?u0!3p!ybubbf&e%-A5`({~}q+!JF%DOlQ?}s#`|Js3P3V8hx zkE^fyZat=CIauHoUT+c4?NS?Y_2LnlH#ifA26!+U_zo|W;2iD*ywVjuhx^VOEW_i2 z@A+KsukiktYB}=hbD1(c*ZZE&^}co2Z$+Oo=dP`)K}6gOJ_N}?z&4{6rK<&~AI$p; z%!6;zHmFg?7N6L{O+CJ55bVR3x$z*+ee>OTobAKgHdyiEfnEl6+(%zw0gAJmLad|K zcpGZ)m|$GOy39l*yS(c%kw>nny!rx#9{l0aQ)QA5YW1SgiwEr&`2t_79cU|WAR9Zt zmF@ky{prKbJ9sIc$A4D6(;JxNX+PM!_YuHLl6k212J+chvh_;Fh7_}ZZYnp;sK6O` z=Kzs3jrVil+;Y4FL;z16(e91oS08qU`7kBIufE~P=RZQc%1|060`B_NtR5Z*S8%i( z0TQ{{u%lTQEDE{J9K(Bg|A7Y`UaZ2Em(&7;$p`NGtB-<;1FcopZ4 zdYSIPi{n@+qp>j%;XMZbF>Lmb_?<}&Rf#zbHK}R~nZVcX7kKJ~Iwo>oz}3F`US#_w z=hS7T_Ml0s9Sm)(dx_T$^%*4^@UkasmulWl^r#O+Wrw;PVpKUZJ9u2OJV3gc-;G8X zoRMq}WB)Fi-k^o@?XQ{M1_S{PAMsv87T;?ahXxo2Yq8J5lO!&H@sA%?4&Ua#(6j?$ z8tytbSufo$6qV~Z7+_iN$Lsw=lGQj`&ZcDt^-}^bczK88e5Lvgnuw@^ojKC@rcw9+ zTm4aL+@bChr5$+nOFfKRp4ojZ58FBf=LQeb&6gW&s(|sTXazllArZ%PAJAp4f6>hy zYPTeGs5au@ng2@0cVXc0nOV?^5d)`PK0J*q?96t*aaeZ4rFiY~v+4pi$xQEpH@$(r z$^LQPUM9fdGEgCNhk64;l5+yv`Qm{4 z=I0LQh7jld(i?_Svpnl|vv`2ugLg(x-$ zLrqTLjrKm_ap0$%;Cw!wt=^d%$hF3qPFRR>=Ev%t$b)ePtSwW?K#p!PQqx<}ib&DA{nQ9%eikX3jan8D_qC0W#8YyN8(+ zJnRu$9A%hgC#xN4+^>-oT>Z6mNK#sV4&)kCOsAl%Kdk&pxOh z#_0~Vocg4vV$;pRlAY^ukl{np*PdrCZ0Znh)Q80kdcX>>6m4cIuFFsc{k5DJD~v?- zFeNY~pmiQhZj$k;t5>odHlBC0GOzEdw&$3iZ%d(%^Q@v4lIk>Cj3;dbcmW=d)A;Qi zcYPm*It~V!gy;LO`b6$i%yv9ANY%o8%fop^^=TH+knTZ=x?_GRmhMuUrHp-M{3Anu zfHGNUNGYS46z%`QB~y%X>rw^F7FLzg!A4d<}TfAGW-@MR!8HGl&^P<eMV$+RL&qpydB7W3lN~&o-NzSrSC9Wiuo2_`A5Gu9la&lk!f=-Vn|(7MF`aMv zW+nP2W&x*f9w&V>7jwP(4lT#Ni7BuBK)hez!`qF)(U||6+5i3;?w7v6dYFfo@!pK+ zm&c)B=AmCsGX3&$#wT7_#9AO&vcLT@A2vkz*@sES>X#Xk9@Q^LGURl>^y1d(mpX^k6Ed;;^rdA-Ky8p2~?lH5aFWyV0Ha>|1v$e%Ha7bMbbA-P`m@f8TywhhDDV2RDzpo$i+T z4C|8q!Z%KLE0@xDsd@CKJgq5lkw5l^j z#Jyk=$Z&s4fgMhN%Y`wV^h|&If_`X7Kisfe)Y~A~-#!oraoxQpIEbrmZ=eM)UAb@m z44OOZPBWeDOsWn}mY3&ZFS9xMnWc3@CKe*-0q9R2&RsddD)cL?lQ9wIVV$k6MWMR0 zt?gb{@Ac}}l-RGTt^U@G84vvp{g9&_m0~i)m!yJ!LVwGZ{>F&KJcxH8ISDL z8)iA(4f|Or3$KUv?Qd-`J|YhE{DyZ5!t2ggMFjI3$Z&sK0YmuBH_mTgwCVoV%E(Vt zWpsN(`ZB|AQJ0H@cByH+a?c%DlNHS1*yRhX#9Joro5#aQYnS^nl=CD5Fa6{+OiILy zf|&kb(YkEe-Qt=`?7Tq8he0{mr{WLY0}DrX3F*)1bYoOM6svpOy7q(kek(Uzc*QxF z``F0&ZKE%kkN$>zuMhsmf_4B_$Lz%3)SPgCLr287i*$e6@A<6;Arax{sC8$)q(}9) zsUS^%n~7Vezm@Vna(*izW%}D+*-qj9HW0l}&u_&~Kqhv7>%)Suur&P*^IHO$nEmZg zDIFtPU8d=8eaWD|J@`1U9qJbe(#UmZ2K7bvx4|I8{p~{J=B7B@-(+7rAX%Nq1gOn6 z>d!F5IcC5t-l?Q?@Qx*mT?x7QyxPR4e4Kh zA(4;ncB_=VOZ}Z!%x*bY;pBGDZij`M@Xf?n>&O~ph-iQ5XO)!J?}Oug7ZGtUm<%%9 z&+^0qy^gGg8QkB=!9=3!4S5)sEi4_fQyjHR{lY880_kUic1QtFcluc`-bc=BJxDqGJGN7}pM8pN z3+sON>oUm1?q~Ol!|ioslN23mfA_G++Uv*%$)KN|D+P3@CB#wlS_}0>_p^V44EM7y zAEAUbuVItHy@vXb3Gl`jxY8?YPz=NB9b)Nct8mvnuj%hN#-7*Y`=HX#nwh`gvONXRd=F9XDH~^=nH)03tsHZxAywBd%k6Wh38vu@S@21))!EEKZ(Qo zpgHF`I`k9biSv0*z&Yn}*Kg#|kF$DoXVnWaIoK*i zeAv%nypeeEk72x?`0yPX<8^>0M2s3q^6TwR`|>pb;ZG;uAIkMUC(IvCp3g?2_&pDR z?}Ngk_)X*obl7|?%l$X`Z?-vn4!%_4!%?w(10&S&ox}503LcxuzCU!)u5KPL1bqZ4r(N6cWB%yVu5+o) zIqkB)hxP)4*8~w>j>CQczeBO$a8%u4RSPtRdXT{&FO%2#2r4sqF&>@CYc%g8C$CYY z%;a?g;;4J_Dnh|}^2(GN^Ql5z_!)>foctDMbE=j_BDTD#it!nHkr@ zZgjM3#U&l;W8hHEURmh3f8qTftg7Ah{>>0-sKfx%sxE}ULX;LNAgR+|-rAjhica^0 z9ww>N2cu6o`44o`k0+_q`W<^m{^cFhHC1_YtK|{9XE-4CaKG?HUs1Iyb? zEV2E$q+tJSm=t7E>i~R#PJSVD5pqy6NsUJ-s=+{rty4`Rwwq$bVX@OGHXX2i3uGGA zY^HG9+M_CP8>;S-Fi;a5zFp6ZAwp+E-=wN^X2dPfg)1YOel_8yqOB zB3xl@d#mG*Of9UuMv@*r1NWi(_TrzjP%uq>38m;7)xuTkWx($DSKbj7I9~%xN4+Yl zc;x+0NoiH>_vq~6fqOyv0=wHEq1UdXV%Btrx&gQCFS0ZrN%3@lmX<3r>r}d-lhhtK zQ4Iye!#H)mBtyj}HBxRn)LqEN;c2ibe>Tde^AC4K_oyTzS|OraR6^M3y)+ilG*mLl zOGUIFCNhffRv+#DM3~OdG~=MZLydM&0d2dJ5CvWzroE)=0GT^Tnh!p=gEiH^QHt$Q zt)S?|dBoy`GD&Gw58bVE=ZbPvc!wGpjZO8Eqf5O)8NA7G^Dp{(4X&oA_CO6na}56%TzL!v zOLOj~NPB3EQ0qdk=_``hi7s;r9WR<&)umFXUW)V3gHdsW4WNlTMa$OZ3 z8asj8AE#{(gk7pW0|~#%f80XM!Yd8ms~`T(O0?#n<89?FTlp*8aNG$=nmGJkDFMI# zAk?bRQM3a3`DU4^`?0it7s?~QMaq?|Q>EPY&<8@T4XvPvsrx)p*P+gae?l)?34cLC z=rN1PC2^lcoGV#d)hM}k*L^3j)SXM5k5P7L=TBi9KV~8xd9G9U+)Z+eDn`kw2|&WR zLrr2u*=y9xB7y$5RN71hfxUQOfW`W3JZ|TMob&kJQf{!=rH&Wj4s|3OT3+%vF*opS zJN%0C`VQr0xlQ8db*d)--pAa+Vv;!Ee}yvJZ=%~y*b@$!Ga$t{?JLx}&`&>$XIZNr ztkq~@ULchDOf1%?Z5?Ai-wsDbUF$CgIT?K-^slyd+3f*A(#!_6R1+cSI zL*Y+W`7a{0I<)>LX$)575Lm<>I#QA()OH({skqXeZII@*1y@s#YSyyZ@C|a61J?Gnq?MwNL21pU)(MQhg#7bP$~W7U*KrT8**J08p|}}kbA&|9L`Quk{hHO{d` zQs`+cwSVJ0J`ee$&I9m0?ki=>W6lK56cP7=V(u0&0myqGDXUWROQc<2e!=Hk~N@oo|O7mH3eqUT9gykoRZjleDaQNb6DewG)FwkW3O1kXop ze8JcGmJ!R@{Wq3_7me_Pejk&#V~N&!3$8R^9<;ajllKCcyq+}PEnFwr;olos6y)Be zpZQxHc#$x-A>7bMg^tJRgx)8|IuB<2M)*~}ufNr9=WG$dbC>RR&QzJ&j%wJU+qnyN z(vHz$hjSu5E|KrY;2(Kd@v&~_EAUG>@!EMK)I``5)y^w)dUrc-k@`zJzl}7fo!dZ1 zxAW)ZOgmSIds|hF_`gFHN`3H4EeNPo`u!iGcZ<3N0=@=(UkYBodaXX_>QaYEZPBvw z&fg9o+03(r4x~n8@Vtao8k!Kp1R_;+EhcDH2T>npYGOXjM)Y2eAB{1;ii{ubLm96z zr#g=kP?U<-`w$;I%Ej$owSWc4@g*M}$gj4oCdil;_7TTgzd@r>wdz}eeU@?ag~Vd5$(8dTzwA{_`@U-aiwk* zJ5GZgI<6iOe|4x1+7mwCyi`I8|6I<8V68f?PQuUU#EYw1DU82^W8RmIimO7M-W^x$ zf95#Dk2EK)7J`nBt9o)Ku98eMdsq*+FG2ij#nouhyG3<1LcfWtWIP4n0cw}}7u%d8 zhQ!rfC^#HfPa#8$xO#*%8t`fAg9gEf4iDo6@u0tEd0y)(C#*K#q6>}=tFve^o^|tG zCVk>l|1utT@?{OEaehA6U4J`Bo}PPndLDjOTtQLPHwjwoxJnYM(`Tc2Sc&oKV+b&= zPC(td+w)3iVmd0c4Z}?Q@oELK?;BVARqF6r^=%^JUht4TUNxfDi0@4+ow&Nl*l`Cv z&L4WgyL&ke{6QK#JEre2FRjyY^&wu(i5FKX(e^~eRf10Mj;mqP!ZKbRhcqXyQW+;v z^U@jQOk6e7el+v?n|Z}~gw)51s}89TzE2JTC$8Rp!-=aRi4G^OMnX0mR~J*Gefa3b ze2hUmLHVStRyC0-5n6&7A)LDr6Y|UM{4V=0^$g94juanWu?RnB2}jB)U|EB0(AwUY zer3Hn8)!xPpJB0%lc%APak7h+hx`40#|hi}cjBZ>M55wkrncj~W+zTgHFlJX9lseT zYwC0yHN& zP9DUo0sF?uHGEwMHCPL+-Ep!$NGsAu!(tsLH$o%hm_W0uo+k4+QS&2O1 z@g!SB+zXxri8ARltbeo}uj4=VgeqfP>ypZ2{Udh7j+51TJeiF5*mRskz6Zm}eIwLF z*rT_mhlw?G?u%NWJ5V-@1Ni%;>fcCn0;Lsncc5&Rh3;qMBv4kXa#jF&=H0|=hboW? zNuc2EJTmu);x07_60(?u->-aCN6HrEmipo)dYP&I3lxr&UQ`(~QhpBTLVHprmd?aE z`0rKlg+O2M`b_b#%s2AG$AP`FF2>7AaJgQ^=r@xV<2@a|m>zd|yfb&TlDL8omVD z+uhR}wb7{K-%w0$@Ob>XE(*=+`r9&Wk{ECm*=b!_cza?2er3A@na%THzVoZ!E38mz zA$|>*KO$TJ+ob8?g?C97_-f~Ht<(axT@uEQ|TWoI$zoh54pg0|HgBoX|pz#f|rqDD+)t@dKzse<3DWX zS4Z%ZUvNeloZ7wQJ0|odo*bidJI~)E_l?+BsfADz)t|5$*T3kle<80`O%oaSf~%nn zh3gJpaHAOX{Yz{)cJSkkLDT6(v(d_F@M6KhM^W76NK-#66=G8#%HO5ix1E1PO(WWw zof2eSC{J>M9hwS?z&I%!rrq83Tb&rI-IeR;CL! z-;JyXn(vkybo0$4-X~<9k;-+c5=fyL{`M~?WKvKZ_8+F3_kmbAWc~nIXOBaJiSaXV zDICVKSek4|?8d%JJ6IjSR8(uv0@{Y(lLSVoccN6?_|L*fHvZ2n-7L;>Q6(R@QXE8+ zLan_xi1Jw%p1bec?v2n8tKF9&o73*4BI91L1YEe?XV9jG^h05bZue7+Epx;cy}^#r z?(efJN*6!UiN`qYkIL>>`)fJf{?ZD%{l!+_9xrj{^Db_f4%nkJmP)ZUn+_A zhZNf%|II`$2)3vc6lb+R#JbxbvJveMjP}oy_GfAPw12z)CZalkscio`l*aZ4MyU^= zRMY-2Qre%T$8P_3<)w@G?GFvH+Mhq|5N>~waWAL`*WLcKsUiI+*kan>*z*5)dlTrW zitG=#I?015NCyOrtU-ek5e%{=tZ9>AH*}OyKok*XP{vV5Q4$9g6ilMDO)D-aDk^Tc zq39s)3J4@9E-0c1Vj^oj8wd&nWXt#a-FmOP1L&Fm`M&dU4xL(V-MV$}t?gF5TQ>?V z_2rLo9z8t}`jR@?-2NH*jqyEx55#=0NrtH7=#lSzV87z`K6SbpEC9V1lP%4K=OXPt zgfm9kry!ZqMy34DNBa6nOp_UXg?>u7iSw(w4R<5b5}DD>H#c8MOX_qP0MNuY7P&&s z7kRjyyd%J3Le2qfqL3MfkVmpjR}v4VQB2LQA>whvO5FijY|7~bfrLGz<$t-~q^AjO zcY`*g3NN7~RiTD7o=6qi0#3h$V)pwm=ALN%&3Rxd&>XK1yx~MIsj&A+e#yd^9*i-B z{PDJ{2kjg}#tR|!^&nyVZznp=@xNJO|M!*^ku`E7=&1f3YymYtb3%*$5+5&c|7R*1 z@-6J?&V8VDj90@*EXxP8jwNGv3pJu6Ysq679Jgs3$;Xcfo0%FL5lYxHG!#-ph^W z>6VO-#39&B^nqkNE=#L!l^zH#P|xnYn~X4@XYmG_Tv`f*#B|v#dgY(#R|bloK)=p) z^-D>9NgLl{tRtz~`skfhXe>hQ@_j~cu^NqTrQE@%Y<(m0?O(vh*!qU)d3prX7b3mh zy1`qvfOws?!$S})O>0jw?YO~yxiqy-PX;FNVBTBqIM7!#!W{?N0xUKTWCJ#F9AMm? z?1v$I{9wPqX=h93Ld}}suifMT>E`##w6HMwY#~MzxiPAD6`A}zUXfcdHKxe(fsvei zdy1)H4r@52j>KGzyDsa0X9x=hd;4}bw8C~nr|xh=f8WKbT$=0|V676G6M& z`m--X?=ZsnY%;2Bi7Ms58Sk|@jr}{)aqPp9y`@lBqc1{|97BO3-Ay7j+D0auz&L<; z$ktM_s&J5~H%RJsy~ct5(1A`y9H7y4z!&|QKX9obz7l@CgLrE6FZk;u-Gf5*M39KX zIaXB*{l^Ry-RzuL-3nwyp?%RcPW_LfDBxKCD5w5Qo%;Ja^_L2EIkd_VkUBlVtq|u| zk_7)Gk_7X2m2QS4)M4KsWQc>28o^nmdrNil+`tY5V`Nn!!n+{b$Y`u>m+2$7BU{{U zOLP~bi=aZx`vnmJ)ITu!X{2%5xzC~QA*rNFe~wJ2*>^ek*HNFy_p1y+Z1*@4vKWb^ zeS`o;qYD@bAfi*i21njmQo(k8GwU~XI`Y$3nI!7?sl?~+uM%_D*FfgMXmO7`MaDb+e=ZLp8(V$sIer~=mIG% z%>6@xHM*ODA_3bFqzbolleX(+i8NIpo%#yXn4maIq_H#ly(8FpPJV?$^F0nBUrmZN zdm@rZ=DTJmYs1JUAYB*VtvB? z4^j$`CZznplnkaAFI|<0o1XD8a~dEAq_EGtj(GGZr-^K@a~Lh{7z+Z}4me$rb}oVB z+0HdiJM*0UWe%4!gf-jswXDu`gj0}2E)O?JZ0N}-mB{6TB!SLQ$(ptj5oLxyDL_&i z(oJFe>w~cttw55!7;&c^TW@t+xB(%168PhEp0FU9PNoBz!+8NumW;ND|(cizi*^B;AFiZngH`Wdx8ayC8>y?lmrOjXv2SxS3OE zuw)5z9fFAh7z1rd6hM`e?+ReCpxCY(A{{KVJ3;}#mgrpk*r!3O`L{~+p7^1wy0N2^ zWhiRzmn@{O&}Bfu;bWQo85qL9A4@Bi*>?z#<0mdsY}!*O?SelYi7&I?fPzW4w67Lb zljdXtC&mqX6fwCo))17i1BofQOi4aKSXQjF7`OM6`W5z2Sw{%te?dAY%>E{cn)elw zDD(#S%eHP4VM^r-1$vjg4plk*F;D2(uGeAaM@5uN7vWr$USX1m^Bfe6>#J0Ni3!*V zUqm*QJNh)#RKZN6Z;&j-g;CeE-^q7J-885Ek)|?xq2Vl5(E_ObJmOMMmEH#s_7k`` zac25ckrMqb7EM9az6Aw9GMTpb7Q{y)KJ!ofFFB7sVr_f*OG=6VrT1iYJjXa%MW-F= z(072sb(Z_V$OIfi!Vfy_OAWDIi`kdl?>gfCmniRg>-{1;sUYv9aYxT#p@~3v%c&}e z^9z8X`XsW0mjk+e>nyF=<4Z|Ir04v3W+7gq|0|-{uG#ZsnO8MD_6guW6wql4n# zlLSJ$jvr$nt6Kasa3L6rDTax#=5t17Y>HSoR5O=ul`Q+;4GqJolJ zghUqon3?!1aYz~_4 zoTm#D?mc`j=NHabgU)GoNplWT+4@tUk78E>{U>peo_9Rw>dAJ|zU}$}3MzI!c}rYR zDj;9?eDZibS&8Du>j~dWbjO)_0^u#cGf_{TmG<`B`>d-cV;wS5g^Uv)PgWQ`8H$Gf zsh;E~2ko9s`==*fKPV@6LTAN*YRZ@@vwb0sf9PpXg* z)02k)b)23=oT9Us2^11H$I_3;Jm9=DFK)hc4LKH$Io-ywm2v&kkjHKma{LxmNjoF; zFR-1BHj9tHB7asI>G|N?nEZj49rOVT;6&pD8}S$NNAct3e_x{f1;Se{&d=R>=2?&i zGEN2=W}F!7kdZ26{N*@t1x_;lT>i(>bFBPLeuDg^mm2vaFD8HXlVjyCeaDeM!<7H^ z{0sR@UZVVw5SKroj+ehvbQUuyf5uNRPB`)>$M7*6FVMyZ1D-E(wu1EC8&D-UZDXJR z>N-fjG3|k*Gn>dGJ?ox!1|3eC%FkY=Lo_eg9v|eKm?7FZU}FL;2^Y@B`-nl+mR5H`xAgVPVTdi2#@DDhpg4_ z6To#DJ=^uyY=pBE3q}&g???Yqj;Sbq zyd3|LAjfSG8Ev%}CCc%aYlV!D=eTm53n;3SU&xq%72%DzMtu>cSsosnQz%&xwzMbX z1maKo?aXIdn_KO^EK%Ohd-PHGE_kn&wxo^L+YR}_?vi-bT#5QKqT044ia4cm~pum zAni8Tnag<-&VLwRi=8pzaDfa2`cx*-fMDj`So)!>k)B=~Psg=yy^V>{eTn7QFljq> z?QVI_2cIx87mLjxcQ^8BlnH&d*G=0F)Fy2sO#lM73S^k`iYu_A#rBr1-vnIbYb*I{ z>=`@s;c)=P0(h_d-7az2e#71`Njsx!9rM}EqFy!naUkHH`y1qoWuQvmgQOz3S$8d# z^8kuZnrJB!GthV^|2>Ee*z^d*qYF_lBewxhbe>STU*CWTsVo-cq_POKk3fu6UL=3( zB<@%sHlP>SZNMU-vPvHkkV7#GL~EgX4~>jsWZ2;3(|o~=|0=}opTrP=>+s8T^dE?S zfjGFpDmNfb_HD*r(;9xQxyPo5zpX1nmI?`F$v=5$>l;HJe$D zl^`le%~(SK)o*l7VT?=1t5nBzePtIUxqYjigMV!Te(XE{WCf|8~Z ze!7`Kq+dBwK9!MePQ%V}h`$Jl*L|iSNhX~vChWuS-e;@CLqfp4MFmVoItx<_%9;?y=kQqN4snS;o zv72cT6G^s{{LN8llPK>SM0T4&Chsz!x%Sm!u@ej0F4WJ)o`t^-+crqjetl{@X{D2- zBuP|cktEgWonzP#!Sx)HNbq_jIpjPbP}_B-Bni}HNy7YTpzH@7;+mMhGi^VpBb7gj zowL$@^$^rLJ|FqR*5j)%uZAy9HT!e==Lj|24cEt!Tk6Vdy|k9NF3?MNz;*6FQ;W*C zc*{zeJauwriYghx$A;lujW^|TkH8;tt2lme1CJx>5;8J(XUXmW@6+)Ij z@LlN}Z4U#0oXcaO=c0-he}nP<4taC6VKCefPjX_-)Y_jLEzweCbmmNhg(&tcGh~2! zxi!V71mRo1&hX2;*YMC)?9X_f^^pDn%!0a%vVR6ZefuN$@yh3a!H<$_9e41;i<`iU zoyBPXXuMl@4eoeuE;%WhVfGVIf4DRIl)ep3qjKI%YTyQX3uy*lMva!1UqKpT>x0Dh z5RY#JC^`)kiFEkU;1PJ!NRuxHAv0H0SJc7L+`wS?KOm^KxK|*26$lzH1f@pXi~TD` z51;lRJ->hpLJx=C?+_tDpvyPfca^;yacM`mp?fo^IC;-9$YY>Ado=UPpa4*T%L4f9 zZA38gpHyS9Jv`hWS}TvcgF}H_INtSP0ThhfnBj-3JggjAK@Rkqg-e-uEE8Uci*k>6 z%l0D1N$xP26`2I!T^QK(PEZ2K$XDBo!=WZ-RPKrPNg znqGl&_}-W^YiVUhaVD26OPLn#|LRKA6o!Y|(n=U`3UlDK57$}oDv;0n4m$5|=4O_j z2nGa7@#K-mh)WxB`L6Pf!Ak;Ih>B&i$f55=5`wQ8GvGA{LAK-qX~_h%1XJ(|eWn9> zN4zCV1TxB&l=rGL{Dya;xC%&R&ibVEu1?BZ{xOOKa>&R8Q`MhlD{4ng~H$9o8X{h?I}ZTxJMv=Q%K4oQ!0aP0k%0G>Xz z`wCmJAI2zer#tlk(T0+i(H#aa-hVFt)?2oRq=(id;JrD9_fp{H*Xvb~A&2*uK)Bw7 z2*zdot0ZpM=U>6`#W%t?JcyTWeZvB|`vRd@Z!r2EgvDOEL9JdWjW5190Bc6NKX)Dc zxfXXL`ww1a*e^W*{x2LRf_r6Qs) z!|)|p?s{Bb0}fX4l2qZSW`*wL0P`Anwg!*F zKheMIsR8WlUm709_0Jz#*X`RF)#GPU|6X?W&jd-~J}}kjAm(BCCkL8t5YPIuC8I|B zLkIlcf*-(v$Qvw!xiG_CBWjhGs8_*ydIcM{(W@BkO=I$} zr&s9BKiOE}k(_Qz?Arw~^ebxT{FQ#4DkY#_G5l{qVbC|Bx7063Z~c+}`#Aq#@YHE! zLb!j2<%maGJjdin&mSard8(a+-0?GUFcvxlx73(UQQ|A0} zu%BIqLiOJ}`HS&8UB>TR2svT=K6tqqlK%^-#mDa*h;h{G2VJ7zM0+dsmk#ho;Bdz8 z+ZlAmZ+s`4IDXTlV#aSv=yk_$`TDlF1vOzl5c(q}IQyECtW|tVBRe)~<2!@; zqc+9?=^r(^r$bl5xXn?JEw~Rz@zw=3nB#UM2XbP(1>C?w-ZE|vfLN(VX53D9H5-#e zy>S~?VdJWY@411j{&_8AXVXS{V1CG@1A5S6sf9W4B9J)@>KdvLM;{78E_OiME z|Mrg(sr3ZwniKSoSHOjY{?W%F`2X5Jl2Goi`$si|!utPT_K(K}4d;dbZ~KQYhPPh- z=yOS|f7Ahj{iE_y^bfYz?H>&gX8*Y8Z^!o!u6VH8!BXc^%t1L=gwmef$y=69(=Jvw zo#1j653u0n9Neh)mMy~AC$GC0z;tifjgoRFU{DE;oy|RaJR3oG_53s|!&}w>$cn;~ zkb>{{en1x}3jYTYY%iXX2rRGIiCsSvTQT6-P;zp#x7g2NY~k9MmWN;1h{zq;{_yo_ z`UNx*kqq4zP2!4U4cI_~?N^*xfF(^&0|+7+IvpTn%bNnSq1eLo6@3(jR>0kurY{$` zJ2Uj|p@iG7ryEqo*yrw&clBU2h41#8(sWk=y(>en5YRe38lWT`=HePV3*#yciWvs= zRcZPtvJuJ9vmDf23<=({TD01a&*-p9UuSQ|7)(leIDfqZ`TWIb55_>uW3mU*mmq^O zybl?2H#$buY^eoQo##+hi3XFZQ}_-NwyL&01T~PfPBGHPxuhK=KD7EFGO~O_aX*sP z$xcEEF;>xbfjyZ1R^V`d&>^goOW6BN0AasfgukWBa}wrIN{Ey2qf)j;e+3ZIj_Kdw z=ufB>kqkXiK%F|@-q03`;tz9faf)&Wvzzy6-Q;f9i&B>n05-o(BZ z1=$2#dN57!3ix(?At^-$pbT$onSdsA#b3&cTh~pb73+ysl6Du;mK*w^iVcuv=6~O1 z>_36f5pkJ`ZGFP-S=X^>XgS0hC|#bxi%SzTsL3yp+{i@S%egaKzlumWIZ!z$9V&sI zH8CCGG=>9sHq94mz-^&782X(mTm;}iXb_G+ugB(hFBLvDTFm)7^pkPg8J0R>lv)k2WKVp3=8sr6-lbG*|N9;ez}2#NKKj#4bc z?hp68`5iPd@}clKq0yD4-0$+=$JzLy8>+n8N{>I(y<{)~NhMd}H>Kppklk&`Qad@X zc16k_|7*o3SK>+s!7EdclvLcfu-j*dB#kc!U3|23tEV`*q9HkoEr{%gBOl`HS5;x8 znz!gYqAq@MW+6BDl}V*LS@6Q6tg3hx_+hY@?o43?(Ngpr%I=~7Wr*)5@Yfly=&G!A zN18v}FwTy}8dd4PvDst{2M@Am33{SGd>nf&C(_d8Ff*@X?>D#4t9agUF|?P}{@~}| zDqSUii7TpSCe)7|Z|ROaXmm^Y#1Ixr$3xD9Deh&l=YE@-o^%kdM-s<(d2t>N1MLKGEApmNF->6T}T?tIxEuiaRYZ%3~u)zvL+?d zULdn-rG7>_Msn%3jXnB#(H80QD`jrLeN(^-XE@)|ezaA8iKOTTPMxKI2faMf!g(eO z;|gxJo(}+hnc%9_1D#~7mzbO<$t!r|PJTyBcL2|pOrYnDq5g=uz!;2}XGZ*$1(~VP703); zGq{@0k((o2A>%nzWB)4%HhD3nYOjvJL4 z9CvBsiz1UZqz#VTum*P+{)v+VJVpB$aNrU?x@J+<)Arf+lar5VgAwZxVePh~OBfh*uGEH+su11|F;@y=9Gvu=I$hxCp;V#T~g` zOb>)dpt?f=-kR{1tr8q`OT-}>YY_Mf*nqcN!(PDnLyf#;-v|Wy%drr>1)@9l2Lb{2 zBXJ0jQBGs1y$nClBh$Y>g*gsn^3(*aj25RC;Y_6{cOM?a!Zq1lz(=+p| z0R)c);tgGr{E^~ls(v0T1m01Yz*92pf4E?PVs^qZXmKa-RPepzQOl)&9ry@yr z!)PHR9PesK%0NLGyQb@SEVBXZ51T?$@5sQZ2i7gB^!jv9N!O`&bU=JRZb$jU`JjQ$ z%T^SYLABN}4I|tknM0~{veb4ZZv{6JCKX>T4B~|iw=56i*;u?qvlwub07^yy>XYRQ3U0=ZS*veQsf z_Uv962k<;z*-v0kp!8^xv6mr~>@5qMw3Om&5x0sjz;A=%w*FGNo$b%685}M+FV#Jj z^p^4I4wLz_KWo?EaIh;f>7B;B?|IP44upOVWbL|GvJ+?Y&tv%;0$I@^;mMs*9Q|qu zO*vo2aIg1g{W>^Yd=4_3IEuO?R&re+OJ5u=?v#plH$lzD!_k|u{PlsXUoQ?{iNe0R zM!1%2yy?(DB7CW%j-Yvl#3A9}SxyTKo|WKMFmmYugXE0*NS;KdKa#)OpQQ(fhh?B( z{K?asq+q@l>TW=uudYaeyoy3Ti29)94ylIxP-O?FO7l!3{lNw!qtGsLlFCm7PP9z)8#P;a2?n5u^lgz7;h%Bk_@90uEIeH9lr zJq}6dF!LGD|Ml5`w%emin)Qw!^{44eWZ`9|#=>x_ne~o7dJzb+XW$3p?%?p`vr_e=1CY`Tq|X%Y z=zAF7hE>i?d@v!il5&2*$IVcnA1rVtj@ZNBLfik9Fc%xbMUG z`(Oa^os+6tGQJd}0P8z1RX1S#fRo=fRoC`M{A?#5@okLLNjS@QPSxKtp6TQRUj^gc z7)O4mRQ(R)KE}y1vQIDL3TZ1yimmrvnMh6Of8zvq+)*eM2;Y@zAK0HbK20-lgJN*& z`VkJZy?yF|1bmkpxOOqPHv#7lH?qBea@RK)h`SlM+MC>Zrwd$n`y{~O8KJbI$k6m) z?)Px5g%;QiK{J;sodV;&_E)B?L}SEp*jPy2^?g}Q~SnV_^-(!tJs96XK4n zeX72k&F7b9ebN8Lp4k@~7vSTCWyvOl;cyMeL>h*}=S?W3_;$pt;=kdy0hUf!j`1)n z3m?}9+c}+=-B7A&m#Tm7i*|F(7P5=7eklqMJDr`;9hFN?#ya5iRQ*K^s-_6EhbOm> z^>qOOy{|YRemo4C1=nW9tpH-iK&h)$s=glD{Ou&{NQ-CY97bL$#gWvc94g7ZB{Bzk| zDs>c9{{^qd3dodo^%#xio6rD8=SdxftAkn9;4)Y!b4g65;9xsKlVoB< zod-3S0Z*<1uGQCasfa`J(69c|gGnVsECc#-|>5=5(3-Fs#oQ+?rxE+2Q6gPI^R|&lNFl%u_ zcws{LrG)U43E>A24&?q6j9h=fTQ;5j9xS_^iIFJ>^vx!)TVHDeT8}Y-YCXaP*6AV> zSgi{rFnT4Km4h&!WLd6vK@Dk;t)R|=MPv0Gtf{ytOs5rjtp1pKYM6BkrH@+zFJe5> zFR}pG`8xjEHzUOz-+10wXO2{0XqmQSl1xTc&ljNP*sWnTD_i-TnzIrR9hm)qe6SRPjDuKE{0# zm~(x#6&`$)3p1AQL&AH@r<{y=8fGkSdAO}rFkmynqrqyQK6Co;XCv6;{-AGQT-IcFxx=ZK<4+B%|@HLecKI3T$TDk zqEYn|iJ)tdO!E|}yv_mrCIp$+Vuqks83zae3tF_cNzxJLZixaG8L6P(zEtoO5s^vvaYhwY$FRZJRd-Mbrpr& zNJ7EqXD%VKt*i)36GrG7Y#&B8e)&aAt~C~$3sG%TWa<>|BwvJurS#v}Np_h!g*(Yg z#PJ?CslhIBSs9Li<{^nj$o~>0{(gX}pcgR<_ZZmzkr7S3>cJyY`L zWEwx7Rl1VZfIE^7?)(5k_+2FPjdyvo0V!cq9k{7?kv~DCd~ycR>CIB6nBz&MeiRW& zX5=%;G1yJ1hQx#6XejW5#)50HGNaTuSoS@JmLVw3hjR(bNi;SNbv}-mBocENc=QP2 z&3tK)5#$#TtqAg5Bgo8?*_7ch3Ao)*%*681R)YW}Vs_cC5+w_noz>6<$ zTJ_8GmOTUuK&c+zFUfmPL(t~zhYKRt!;rDPAhfFh*Y@9F)17XT|4-tt)UP7V^e9M9 z`68nU52oLp0DGDPduI$b6Epxe$-s_CfQ^nfcrS^;a{mX|Wa)o*-*>pK&QPCWN_oq0 z!GSiTx15scZpm_VIlM%PA1vCp zH!>K$6x-H!-$Wkp__LQ*pIjJvYz|R*7cBM9#tir}A_d-eMr9Xdvhxa+~bk-8XKfr;%*nuA(hwtaWcQ){5JboI6IL=qF zwI65ie5g0_!q&tm*d1wW3nD}7rXIAs_xy4Q5^w4) zKM8;B@9;xAj*(jff=#N+n_34ry7VMmF8F$0_3$0m@Z+iD!AbuWQivGu6$mDq-O-@wzpD>+q`eK?d zoLx#A=A*<9qZ9MLko-p^KM=kJZ{r$q`oed#(TkDERSFBc6IF50mAKfN`m^`9#1C|< z`X8*ffAZfkonDDL^fR5g?!LDgVLKn~rmz%n=>{aY*?yBk?7BIsioR&9A%0jxzQQ^h z!W8d)vA#P73zNtMi0LS}+6>&JnEt&FU=|_?<4-RD7DV#E{{w*3>|KC-n2;MxzcO-a zB5*?iM+M<~UxZsqxP^&u+dy3b1n(2c&vxK$BwX0QIeJ>7M`7Cbhs_$!_SIrB5akz^{gf!gAd~O&TzgI{7BLY!e^U<*^L0eM>5PmL-OA?Wx}_@I9Qgs z1e^^|YNJ~sFNg&b@nMnT8Zuy-W(5v4xIE)6+eDf_z>l+D5W4j|lK3l#GSSBnrL1OB za8C*4AIV}&7o7vZ+gv~{SFRUWv(&L$L8J!7utXI629@}Rf*O4?Q7l7>8PZlE${{T~ zHl%S0PE)hei_d4d7ISBz5h1RXaW7fZ%3xcw{sSncLQQ(-jTG^n3mx% zfilt^QN2Bm-6s5>v|!{yXncjYT&7ZfP!gZ-s3l`bl%pk$h3+y*D+uTK#0obP+@cW~ zT5`CX(FB&^-j{DJQ;fdw?QL|8WEzACI|wg|Q%MLXN%euxw9Dya{j6h*^v46PgtY`xxzy6o#Q@CXTH{%$hGVg3U`?MIRp>!NP} z!0DoQN&efWjMGKQFLu$1$YU2JzJxAX+{a(%>g-GL6a5-(o*8|_e#k*j z71EP|-mU^wLXZgeeIy0>8nOr%P9&TfZe|TL@MABafMM7UM(}1mEXU&cO7C=TNyvSPVwfdxy_jaS%-DLT2-}Qmlq-OxE3+U{&{h@VbiXk4 zxi9P2!rY}kZ*YAfE{5LEPYVljHw41HV4W)PUWAQARnfQ&LEdh}DZ1ewFUS^V-I*Hj z^ePM;EyV5Q`~4^v7*|o?*%$Dv!0V-%cSYSbOvI&4Z`mary-E!i!F6tyz3+UK=>gGsctoX~Mgy*Z4xj+; zXBS_J{nAC*1vxWhDdH{P#PslpoM7%FI7vt0^3_Nw4Bgr3!bm@?!>|V|$UWqp@eoo2 z<9+};!7IHN%S~VD(WBn-70^ppDuv-YhZXj{bENm4?~v*PRdZ1aORiI(3$i{TuSUWH z-MAHg&u9Mqzp?4%UTh0PdZ}*j(cc{4@`|c-wZD5C(JgQJ8axJuEgE$%;jG8t8@I-n zRRyAgkv1(sG)}$S`7pl?<8pWuy0tUU%VZlwuc`(?dc>JvBDN6n>>CE{3M|5z0p2Mc zkg2lgIrpQzp<6NQ%Ec6%V_lAE1lM*!1^N9f;Khcn~y z)6OJ8YwC#~hEE2vAjCfo z?v5p^FcIU4Zh3~wzh5!vz%ZW3zmudU>(?115gReNnU}B=Bmb~?Id#Mdc49LnZ;rD} z!TiOFK%%5WtRG`!<19jUCCnnYdr z4eaL&syR}pxzEx7DYCOW&+P0jNTJY%uxat7W))1_oMO)bUj98F_32X}^h(td)W$nQ zIS+nA_ENjO$NuIIU+61e=`N3V`X;2(L@_il?)&^mO0QBI+oNp_{j>d%HkZLgjWnE3-fKcB z#n&K?>l*lNP}~le2%cNB9+CwbrPo|x$il`wN6 zGfR+J4=;9mDc*PQ;I?lH@{;Qpls4tFU?J;ll$eWl678kT9nRb{>*rp>Tp%t4V&TV3 zY`Kusy41!+uxJ{Zm4VFB86YhU)t9i%6PqBZqz%wE;YPF-Ff=A<>E3tq87OQCp!|~o zRM;G$Aq*8YL+H{}godXfG>)NbnjmyX1B9kDMrag6qfZ9@6xNiAB!o~?18G1#CXm+h zVNg`NN_KHmAs=I(=VrDV5uR#~)X%naI~`sNIJVP0f>ly5Y?Xxmq|*^6I^C>c$8|bf zx#BvHJ&J1Ex4ltA9z^Mjxc0($EZt2EALfS3*XRIZPBUF?KBDMRW4V%q22U`lE_`$?fP^T9V0HX`j!nv}w*n*)q zd^1c1b^2K$*W|ie3o=Hw7F@ptF;KB6DyNarT&8*lRJ!A?+@)F#i=EvSlIGAl7vRQJ z0e|dLyD_zf)39&wmIYWl4HY%|OvyD9sw`kJmd`|KU-(Y!f2DSV4zq}n3`LL&QVCcGZ3_g#~@^$Ud0Mx)8sC_L8eW!$cxW+&x4CDl+mu+ky3zZ zb(QeNATxR~D5e)vz-6Ns3#dZJjM(KDAzSQ6|7nHx=OJeH?z1_WE(JT>8ubVQJcSei zpbixanTD_C0d;pGJ%_L#0XC2RDWGB1Z026DO1R5)Vkf;8K!3UbSt&DBi&x6I02$$I z`;T3DfXkaMg7}YJfEX)$5X)<_moxvrAYHa(uEcBgZGzsJL+eh$fsQ#{nt|1i3ukzI zy`*4~{DGNA@!nWHTt{^m;Ns%48n~T4XGVy2C`z?wrLwN^mj5QRlB{jMz~0(f+dK?u zVcCmT#waO217;La%%9W2%jT|8g?vAZWr{zHF~IgOW{vLYGCXgKcEDQ54 zn1T8fP~&IX*ToXviWSRqiitvA$71cBlqEzo34nGEsuN`y%zR#is@GX`^6AL`vyM`m zAsyw1(qlWyf#Cn#QJ!N32_5CXz{!mO+ZyjEKO*Li*rq!$g^L$&`Hf(t=?-s*bm}i) z$_FA_T6pbgXkoWRuHPednNDVNnPFIZlO%s6nr7c5403qiktl|^fYic#VS!NydmneJ zuAKg83E0(0F-Thh(7wk35Q(fKz&{d;R4}Cllv0XNFSf|ErLWv zrp8AE$0L$f7(U;uE;FRjx5(^RDl_}r0^oOSKXZYAwx9nC+Mz%Xf$ke## zH`HF^biHci>LYEDQgzgDVHnT5acI-TtRj?I$PaEWo0YYkOtVClZ z*zH&i?PYjQs7AM9^XS-Zz1SYR>mQ-NdM|!>-XpF6v3!i0=k1s2)~8aj&Fr9ep%V6{ zmPizn+V{k`5~;GY{arBrqVFZf=kSB^l+QRIQ|cdvne9oUfKj6pX`u1*zj`FWCkg?| zZiMEENlJ+Qw?XX^dyhVsuovRT9>n1Y>s)Gj`E_WbPGeqY=Dk+SJbpK4=aD67&%^k! zuat<~b$h-w(`)pQW~2}6jTJf@K{l0|pp!`_@;f8ZvB6D15!%+$3Fp?l8}#Z&G1lm} z@H5lChfy2PpV13<-UjW|>-LL$t392gI_5(4#e zghLzl2ts&WM~n;|EGYL2Pwicjxt!&`g^XC`by5zF2lwj*Kt=IgCP^!7Z=4-3P|Wza zx&sh^7d9D6ec(GMFf>=`i9*J9-C8JqDG9w;|NBzcUTMAqowj2-hNya0N}lt_t&}j$qrWpP!Q7OdhGAgZau!W6H<|Tv zIbH>-4Tcv~Lx;T6f5x)X7rCbnTYqf4ecnLD(gTe`OG}q}aH%iT*>v|U8tJ|F1;C?M zWcvCJ@8m680h=7|pj3FvJ_3dSzR$%@bC_;V?T8ffcpG;U`5oDA6bsUJDn1%$4$Tf? z0hz~_33JRn{MSfIL3qlrg1%Em-g;MIw=LnxtY^xwiJN?-%RM|uyR{QuZdiJ-{nB4s z_&neHrfnm|zO3&HJ+*~d*qw%U2a*dj0--_-e+!Av`_7>3g7BbJUaKfyQqt7BV9+rC zY@6ek;Y0YwVWm4g-UTZPvX&s@9b|ah7jQhso)|tIU>^;a48`e>A0|**GCOe=%#ImG zkF$!>3Gdhf*k*_7m$A~JeYB|<;&F$5GtgX4D;3qzxg-Wn3yC&5xMk>Z9`uq4 zdJm>D`x>;!=$Es&b~5K-MfazydmQ63KHt855~(bS+iR!&@0=S%(IjwggrIadC!2Y* zn-+in39R#$7l3wQUsrQ9S_NrBUqD6trd%x<4!)%kEB>aVHs43k53B`*OXxRpPFm9l zNj!c^od4x}(2_LW%%O=xLijXU|GmZ_PRG&V^K_Er#}D7QEn5NrA6H=enp3@wKYhE{ z6^WdCa!q5`QJnGhN2F)y0+1K!e{UY_ICQ+OSDp+o>yVKz5{!3fyC+h}L=vbAP+{~r zY1Qd$)nTBPwJ^?l{XFZ%my38%C?@Zip0q|mmni!=@;<-WZBqXl2W=%f5afOlF0+!RdB6_P42$ zXZjFDYC(HRqNKxA)k1JTA_4$n_bU!>Sy1%D0Gds9XQVh?nXj_x4@k3vioN)Au0|j_ zwjP^g`8Lv9)>7~oLd-!uQ$!QbGn#lUW|m-B+7&la7b21Lmdg?c{^d}az7{O^eFRZB zrfNGVvSnM8(NmYZ_W8(ht&%(y<^tq4;G%9-6Fi)SOVGHp&UFGNk}^&Q4r=)f+WBBgb5- zxC@$WxlF<6@K55!-Koi`qrrsSL?Qg;XwZs6cKSK>>>RF5jvEbl3}3&` z^J8kGWZ{^Stwh?fV|4kIu9j6NqlGk5$2#y)nP$wiV}FRYlMw0bZxeP3Qq1_U)|ro% zfLrz?H<>d1&_O{y+6@!Oi<^mJGh}N}JOn`O?;jUoo54?CM354s@cg4F@;6vD@Y&|6 zAkjW6k#oDvgAbr**|QUc(@u&y^8GrIZ{K+U`?EH`X`aV@eM!Zv>F>fZ4PN zCf}l5AKLLPVzejyZ2rOlB^o#h8MlM!_H_KnI#gdz5k;;;=5@&2$;?9{7RQ7(gs3lP zWibp$sk31arx69)KALs>dLmM3)A6HI`b&THl=Rox=o@+nJBY`%eIdQ0PG@>|{E)r_ z&|c?)7=@_&0uKx=dR*O6vl(2b{RqWOm{09ssF_?W%v<$<40tUn?Hj2 z0p?%r=3hzG5&NN@NmMS~sF@~P+&FWu4n6?Cc z0Y)Ewek1lqxi)tMGny>zezgZn$vEOooLpVR3xLzUMw%?G9WI%;KY4^)tgwfpuwbpx zgO#8Z$I^m&nXUvB8#0vwc)}_ADH83)K7WtJMo2-g2BkWRAG^WtMu{NDJER7jFB1bT zk`ABOaGG=2VNXPHM|p-g@O!YM1Cyf9h~>9Ncb9a$2P5gQfG*QZ5VZFiCfYlkzdmrz zeh>Ii6Hw|W3Di~a^TMl+V*W;!epv7>j zMf69}zao@RxZmg-{s{=E69-Tsx+U||Gg_qowrq}E^tcqBD-&vWL5U}u@WB)G+*gOU zaq*dx`x$`wdMHEp0~p-=@CjAiV#lYuXw()wd;vjVH&>&tByK5#6HL0sf(kxSLH0gn~eivTC-zO4TV>A2}!)}fK z!O5ySNG%lTw~1-BGZwsyd={xfK9ZvEIl8jg!0Sr76(Fo3g%ZN}Nd*_B2eS#@)lupco-sr&?q}xK4#@f=o-H_Zzr-$j5Jk z;tYNc9|-+uybEN}{XpCXkORcbr>o5@IA|!KlbTfL27V(aGl2W5S#a=BOxHQ_rTtH! z#o%6NJU56BoA?HQek^|jd^W&q2fgDM*9iDtDVAw)2Z@`@Ja8%`8*RI^%vr z91c169^7gsN;UbbPry~F2DA<(lG137y#ZsHtIxP3jV=*CC$I-&{)yYq;GqLNYl)Kf zvnVwaarkp-!1ygr9GGh{bJ>MX9Pry1Z^1a20ycin_%@DYD31nLF#bN{zRN=2#M}?a zfYu4Ov*GKlH)czG3yg<;{NvyVEi(Cgyj_T#(C7Y8KU}J=(oM0VfXl_;xElbEVJ)V+ z1>u%%AIE+k>G?{Q zH0Z{&+y*H$2zDH0&jb=z{*j*34BVg?+`8Wh2fuS~)q}g-z_p9Py$Lv2GvLRr9$YsA zSKHC8ce;!3T#y&9_ap=NRSa&dzzt8etLx$W1*!!Nn-_!2k$SIDa&+S2<9f$GZVde5 zU5L@|hAQ~QyP*<}ptv^J;Vruoid_`i24_owP)$+RF3c`;iU+q#Cv=ME&NM}+O5VTX zJE(GJr3EAmhj+AjLqZ5)aX?uigb|9v9YQI^QPpq=iFy%`rTiXs>8K0#C6`FM$dT!xha-9(wyhtpaC!NmC# zKu}BK%AnrzV~XV8g>?bkNY!hz(1EFVlvMH`tPB3M**Qc~T9f104yK~<&U$+QmOu7K z*s-G%y6au>|ArFwd}y^Zzt`3vK80}%gdlqx<3pV|)(77+o)L$yV0<^{f5Xgo7+)rF z`2R)6K>xVs47b0fBey7gntdCrUl`b)HTmOX`3G>Tq$u3pz7Urhkw4Ak4~pflLq7ew zJ!8uM-Q=GY%YRSulkMkBzR%=0h~>|A%6B#RJDB`!9bEb*Ipw>Ve2>Ymh~Ho3(9OT2Lv%LZNaLI^WDZO06v!(tSO2&^U;_j@@1uQxt+*#ol?yT?&cUJg? zJ1hLcofUo)+*uvoiMAV0mU?$WcuPWfT|)SagzyrC;WQD>=ituj+axX)w(HlJ7@3@T zKtJn*cI(HSkk%0=RIN*$&^mpa6I!irlF*fzE4h682O`nAFhb~bw30@G-OwstB*cMm zU%jlBOpyruO9euW^eCa_wMkbv>_H5lROOXQ0}^T?NV z(0}0zmvdMe_vM@*)4|1&tG~oMfN-76Pi8nDU{i_NfVK(D2CjEhU5gKX1&qE(Fzgpk z`CphsWEA;(DD8&lCxquFgda%=PfrL>P6$sx7*rIGaFLB+rs&deC$w7+aY9=AoKUsy z?S$6pE>38*K9eE5|3U&!Lpb_k5cUnciQPv}$BM1K>da#ggz4c8`CFrFe?3~KA4EjY zaB?1Ta(+TieSiP`e;f_b^0U(2B8UE-%h-Qg+|A@j&krRR@1t!(F5ctA+cf440P*49 zUOv0(%v;MPJ@^PlJ^TK>9B9!M-sM6De-~2?#HBu0rN-_7KM=+!qq%KLL|Not!+IF?D{Vu*)Om6Fy2?*z~}EN8&t-v01toIBy=N7^{+RWcP+V7$jo;q%aV z`Fc9O{}cc3y@48GxP*KgP(|-^%$LPo^)ohPVy@$D;hF}bnITE;^}+!_BnFuE z(J!5NdbSwFXl9y#$i!Jq1l65p$eM)TKA(*1JBw3}F+M8re zw^UiO6|vMK$rhYDy^w4@W~sN6tvQxjo@_m5sqd1lmn^j<*;)YjWb0i^{gFZv>O4;* zwel-qweP1Wv{lxUhdoN~s)r>bWy z>%~*mS}Xb8Q`J5zeN!{FxPkR+GxbLUYhN=JX=s(7s(x>nROeMsrnX1=j#TTpX6nV0 ztPh&0_fE1Zo2jQyPX4Z$`ms?2yqS7RTjaiZ3UgPU(iZRsP7yp!EKvSt6JlP`!~(VJ znpmK1cM}VgO!u}$z3+J$UhQT2Rxe@pd0DD54NTeH6hGz740{@%Vr8h-2zSMQ%B+j0 z17)_r137s<@>oAw$+eyzjpXibpdyasrZ}?uzrxpfW_h3wO11d9t;yCOz11rz z$q2ul()O_&^<9efQjR*3V%?jo=0NLm)qabR(;8f{B}YwfXjSH@_ZwP2<*0oPt?B*L z`>DwYFHdbdub(|b$7M4sWrQY z3O7sMk)ytCMwp+Pjah%bvQM?1IA1;9+*)wHTHD-u7vUDk2*1#x?SbxUc?)ZL5A{cj z}TRxoSnaHLZ^dot}*FBd520rjL62bgQb5`t|hW z*ZQjewj#_&t*+YIOI5bEX7^J2TU)R9Qj6MHpY~Fw)v~r$bzk*W+vMGS z)prv9sjY?dEo}+&TU+8e+Lkng+mZg+?Z}81+mTuSZAV5gZU;I3*3JUApKEV@oU6WX zZ!PMfHnq1l_E5h{%KrAueJsNQZ&zkmYkR808P>+0YI+CDK2Oc;z?7#tF!#d_M8C2_ ztNXLnuN|zXvQ_8|YkszR_YCWkY_+>Ldgn~**KD=vOlyC(`Y4ky zpJiGzx~Vz|&*|9q@ows)j@G%RW#$*vU3>s_t8`>VxW zDVDFgS_k^6>aG+{O;>AoKXtS#d&idZAgJ=*_}Rgq2m4x|^yRcruV=FFA3n+R`$?Sv z(tYfEK{d~V53?70va1KH_mV6e#)w+q$&#?~Jfs{2mKn&DTUonkHP@7ZvQ_4xqLy-lnS2B-&`5azKarvrXT z6YIcW^<|URoBS%;#5(9#dzx4;_|>5%)*`=x+l30hdd+LC_p7B|Yh!_$kp_mmn0D2@ zh3es^8Q@J*>xn{D+4Q{U3f1Cf)~khT*{OXHE^TgoP^cbgZhcy)o@<`9wov`j+^QPj z+0)$Gndft)uSz~dk3kx zEv?yu)MqV|cU+(jwFCu^oHiz$uS(NF!Gd(_>3sEW`nmJ+)%??~H}ciOR(%lO)5`iJ zU)8m;D)QAst+Rg2SKqX@Y6o~Wx3-?j^E}YTisY%s+7RZMHm3uAWgF|w0=2eHyF~>Y zB!0=qAYmQMSNF8F!jN@aYfgcBzisl}KJ{x`P*B=#?6W?#ryVGGroHvHPrcN>>qkEI zbcVIur=IW72jR^ftnYlPri1m9Pu+b6UZqe=&#>O_=UH=x^-~{D9r|`3b?=#kdEm^` z0sr2a)+dA1iZk1NHArnc)0!7hyUw)U4XD~Pt(5^aHPiYrpkB#L-tSXCW`cs>GcSMi z0=1zdD7g15>y-=C17~%8>jHJp+1C3PsBot~2(Rj7eRhHRp_BE^1?tyMSz9h(KmMej z=R@@A{+{jV&HYqO7Gd^hoeuc9=U9*U)$8Z9n;uYKony@ksPE6Q-W{TTKF3-)MCo&^ zABU(1J16fPqLy_A1wVBjHFb#k;M{>q9X{83Vz8Rs1s&(ZE|~~MyMV68&<}IfGhNT! zk*gj&&#KH-k7Zj3FU;Lc!-+W&3bZ(dbyjmV2E1M&3ZYg)^)Sq465DTsB{Or zcL4ZnSbp|V@AYW+L?88I538(?`lW|eo2%>|*313Wo*vdC{nX<XBa7ks<1(URkq)YFjVs)xMqsy{yu{o)>ystAc8NZ^FFYoA7IU zTj$C}?)z1Y`U z(O2Eq4++onXLUd8vHogme*{12Z>{gI*7mo`2dJq7kg$k9zYVY=1J(3_)~bQ(@qyOV zJXJ9e$%pte$A_N<{CV&K{Jg-Qd-Cz~IDcL%z|Y-<_?g9@mj>Y{I>?G#sHR_t;95U^ zG`|mz493sP{P~bS_Yc9(tNhXYsR-i7S%hOm{JN1D{pRJW=Q;X)%{`~baKb{3e=ipYgK{zIXQV<0hi+l??|?g zzBjoenw2u<{al5&zR)DA1WNo&1MAfR>a_;eiUC|JuN|OPHL$i0P-`T8LxYxp*^6;= zfVvOsn1O0`L+icxg_ULL5HH?-axsJ_5jXQ0~E(E5I$x+m4zIZ&-ih1Gd)>X@S- z^(54E_@t8i3eZ zWc`d4R--o295tu0bzeXALSt)AKeeK<^+rFnv9YzXpL+fj)bzzEW4^!?-UKy$*Q8{B zzWUm0=>oOEYwasgHD0T{P#yAGvoJEIF+H5t5-=~MSuYl<57Mmn3f0OqYe}J6o7R4H zq1vBjRTrwdG^?giJ<-&fHb{Nh6eg{`O~!n00yhdVsPy?bgr*YHqsqS}!#}-KyxNmZV$X_flV^TQ$AZcar{7`e}fvO}A$B zRx?hwp6RWgINkbhZ}rmY?H6MWd%E>?Z}rXT*3Z3F?di!g2dI}?0dYmEh7V%wY=!Ec zX#J39QJz}R#@gCnEooyN=&!zNV@(~PHneF!D_)rVK7F_GDO} z_EFQY?CPWL@6cjXAN6Vn>*Kyyb}?mj2j;Hp0K~s_8234rf@dIj$4Z z)=$0B>HlHx&BJTD+OYAp_Bs3HIEf@jf`~*;<``lOH52nZQ(8(8QxPSGs(wKPtSS>a^p4xnAyneP8+ZeB-r28;StHFMV*MF=?+O)>~0zT zcmf+$R^O4pCY9B{DbHq>CH(@N%dsitba+@<4wbW=P8aDkHUX!VbQ(+Rv^jKIL#H!z z+EoFkD;4yt3T!sbCBLFmT1A{P=yZWj9n2R6es93whB(?>9m+$ z4x8xom`*$Ct&m2;{|q`!u8GrWI%U%7Ih`iZD`6R(cF}2cEu3D`X$-v*zM)fwxT5z$ z&RH(*@9ScW?|dkleZdPec9844qu5D4>`)XtuIU$}*d1`#&C)ES z|HM)NoFxdnM6+)#;mf1hWs7TK1e;;i*GI5rR?F@Pwpv_ouLuAdEM>+SmQq3pRGQ;oTK-g+3##yjA5v17=dP`1EZpB+Zy`pPi2 z!(0C@j2-sY4~4PQLVwA-065RRgD-}$89w@*FgD*ue-XyE`REf}?12wiF^Vk0>!tVQHu$$(5D=mh7MC13Id_#VSVKXs)yV&9YJ;TK|2k2*Am_6&6E_PVx zPX`nL=eL01r!F=nP)`eIYXWs>HU**{F9*JLC7K<|4`09JAF?-wT`iy=k6?ET=yxJm zRssD*1O{<^Y9yOlko0p3764~U!Qgd~>|#NEdn9{SP=_X~AWFJA=&jAs7`Wl%s}6 zIC?joR)pj9RRm5q=rk%)UmVGT!G;~%WQ!;0&z89hvWaggnt_xU^Y#3HUYIUm1c)7J*_ zt@gIK{w7abT>t8r3v{tR>G${(=NJEITrYOk#dTUf(yz^ToR18&%nB?xx)3`Sc!19; zV7XCXjCLo8Jqd#Kiy$4EtRNkvNrj;Bh#$@Az+1-ZM=h+ee$C1zbGk&>?3#s*)(YSn z0jQ6x{6|gy+{*EkZn5xH7X6l$AF$|8EbJ0Rqb6A$>#TS@9lNdUnN|PGswR9{bIR~2 z8tC58A9FsS&@|10SRZ1$XE-VTqKXm$UX>kdJ*tm|oKND^T;FhgQz3TCs!s{#SnuNc zsIHFlZ$@kp9H)1_yA`*tYg9Swf)DT)g|UbW+#13*VVPE#K553Z z_q*2>O@=(K)A&-YzW#%YbNu|s&_DOmXN4m$ zrk@XIXYBg7;cRT4rntVAN8c62raMS~(s9xPD%Qd1iGCC5dR%~hJBpnN(9cA%>jC=4 zXtn`Qs0+W<91YF<{3CEZx}g3=G+R(mpC4`6Q841yD9hX+;%p3}Ozg+%HQI77=mTgb z2a^WR-q%s=V6cACWw{+3KEY+#SNI(ykH-j{%OU!yNVYswKN86{hwA4e*kpvGTx>}Z z{aY8?RD`S^6u}5-nG_arJJND1EDX#O^c>v_$AD^?5E1@yq-A6zU0;nPBKq3eIbv&EslNdKvb74_tU?NV-NlGjdAR6KYeK&Tjd{z>r4Ln*f{oylk|rv41Le3 zzl`GJ^F=(3vE0e0{~E)`21NW2WkIDbi)C2>pv(=_cg3=6f%=wMn#b;oW()Jv_4WMv zlUVjRe{H0_zd#^fq6PG+v21rieRecERZzbe$vzFD>-j;Kcg+bIG{#c7>X(0`lA@O(>oeABYn^rR{H1=Y?Ay(K9s3U;iPNo%Yv{#E z|2cwP&adAHXZP~!D`VN-0#N^0fKtAPGg`8sekhzTE*QQ&#xjxy-g!a#-6-}`kp4>) z`z=V{7|9M7(&t66D}_oTFAIZfA<;2~!{G7T!us?`eztJL50MtESKtOAqu(QWW=Qy& zD9iTHFnG8Ts-M6VB~*VB!DkeSm>xwP;z|TMM8vuX%P1FlnCc>Qsf%n!h7 z;IxWP59xG*I>V3jg8Q8M!S|vw(CI0izNH>l}s8ipl@=?-faf= zPaafxVDf;zN$>Vd>fOI{>cFIKZ+E6k7L$_ND|w(RyqYVuSMNd0Mg2Is3~N)XMN`(W zPPrjvS)0anJFqtG+qY*P9+LWY9o$cP@X&hrZPTJIOlvo9%Gxw+Q-&qBXjq$dOBphd zrS=<;!g9|crP;Ja0!vD4)rNgJbYLp`vgg17|3Ic(xmfMy?drB7I@z{w*rGu^U9_p& zv_*4yD=%BsZP6sLL;b{t%{nJGsnxb=XS!q_+BU5cYd4l!a@??4!!`{Qo6x1;BZY>o z+sNzA>IP+N(Y$rtR;+%j=51RvY*t@p#s%0FSFcs`W^Ib1*mYSVRh27oU~2C{sjPMi zia#iMAgezxW$*wr8Y@$>Oxcp92*0$Fh(W!Ipje&{n2O-kz4xqDJ-lB-%ZSF^TF znvfPrr6%>2hLu^5fl2+6jhjKehbLEN(t)dbHCJiYx^CUZo$I!0)w~sJot*r($V2b` zE@6ef+N^xj-ou6sD^sR5^4%S$f!&hpX6y0UZ<!DZ4$)%b~6+)=n9cJP^&}X+Ho@!Hn8Rk2I-<=e)-3&*^UqR`m22Rr7y+ z4*!wY7x~B2TBmpS{;{d9p2?{$WW<#?xO?vuSM8MkJ$mO~T$Np^Dd+&5yS|&6Jg9Sb zk%8`&rNe(npYnjN72l5<+ar0i{S!ReA0;%K5mO5B$A`CKe3(Bk?BivhjB%FqAvgy7 z@G=W;TJw+e2{?K|pU2cneiPhx$wl8C`u5O!<=~INeT!W5)1YsVi+(lqHFMGLg}x&6 zIrBpwNG+X<{&(o(a`EGXa=UWTM?oKyi@qxK{yFqiXKkRjLyxyRzK!bZpQg3&wbM2J zsQ&1)riACr?*!;sF8U?V|8eG3{x;|z=Au6X{jFT|_n^O)i{7eXjhTx+1o{)X=o6qn zkc*yv+hb=g`tHzwn~Qz~^y{Gap^8y|R6o<8Uy*}P^|~7Rhdr`9>a~ekuTj8{03Qjv z_T)>{doy2GJBP1-qOZLXeqEmai1#~ql%F?AA$To)T^%H^jt4IU9iQs=zxk^J9`&dH z#_J0nJsx5Akt9_c)l{ zZUk@!JJ0$cf775p4?9nPq+bpF3Fyhp_>02DUMLPgUy+n}p3}8DzCmf02EMK&-yqZ{ z9)H#b&^_?hWn^WA;dx-KeEla;zxdQr%hxs0TFWMN9(9M9|E#TIlnd%aP9J3_y1PF9vb5s{C^$Oq*&75er& zUuLbNfzQ{!o^MbiKkuRTk*r2=m15@}xTImQYb?`P^4w)3HxoUk3G6S!{`>{lV*+5UW#ltQUljQieX*(U z2}_GyvxA4X1pJjiK33qpi2lK>PV@tJeo)U1kpuY<)I|=co;rg6Exkvno*oz^;rVti0hk`Rhi0@W~B3v^;ySvfTb2a+po57 zqBr>)0Y5=0Sy{8G-yVO}Un6s@F#FL@xzeKVo#C)FIV`hEdk(wo@K(6^{Y~+K=cNu*`HeKlHVp~ z{fhoC{4NXp&wio+vY&D0;xeE%4T;N?(*IRodhYD{H`A2@~z3h+gg*Nbo z-&mu(ImQLg{)_hAlLzga+rZ%AcP{+eM`dO0!^Flq$nY!1Pj|oZ^t;Yt+2RGiwO@HY z>wD(+clez=H7o0jK=^HbQ}xFj`E|b+>iaIVSVp)T$ny(tqc%o&4)u9Gc zwI~HIY^gHxL6%q#wb}eK>-VQwS%LU>T63X`g4_U^0C@zm4&-&nHjsZoc83gnn3a_Z zSpsr2WF5$9kR2hHK&C=&fSdri7jg;Y8OUvr*CEe9S~HL?WFVvue%&bwG72&QvMOX9 z$TpB|Ap1ggha3%=3ON^YG~@=zX^=-Cmq1>J+yMC(~p`;jlmX^?Xv ze}~))+2+8@tb33fAOjyGKL?RMWGZBL$TN@=AQKKDJ;-U0mmz9+y4UkLl z>q$$FLXY3qvK|8u_jQiLFTNjm9dZNYhEw1HKLhzY?w4E!{wH|&)swl9OTytBa&H~1 zckmNJ3GMN}QQ;@5LsnJ^Dvzay6y@Mpo86$W+KC@He^>xbVNXGuB~{zeCn} z4nLT$rb4#qmX&oGGPMWv_=4yL%oCoyW@wE5+JWb(oZm@zLS;J5nmwP`ytYToHhl1z+VFCgRc*@ z!SjJ7V^-=`;E=y>0~hI7w`XPj1sMg|7yLRqA<6F%;FRuBaNsXn$Nu-`-x~P02L7#q ze{1018u+&c{;h$3YvA7+_`g&GBG!zwN`H>U8UBp>E;;TOs(V4o<>Wdj?bp30!I2;2 zLi!J?Yia|0=MYJ94V7}|Aa$QAePHvK0S(F0I;!S!FvtRMN1hsc?T?;tQ2D`a|hCQg5`gVQ0vihb4ZEe*6)C3+Z3P z;IB({W!GBD0qTBdFL__Hrm9P=HG57{A4j;XvKl%&{ z{SA$f^2tl-Zy{}%;7^tUUp!X0ai5suzD17vF)m?l+IP%>_sMZTOJ&UPKSE{9xc@!} z-cPz`s#=)6n^fdg`jFw$QWcZ+IxFvu{W+sv3n~5ph2wiC;?JlrgqTGAj;$&62xZ9o zTJjzt1#xfW6Cnk0zeM2(Nr-!+-lf(5yXRX+jGD+^#tN!^>M7Yy$sS4$Q8Hc08A>iw za+8u7N}g3RQ^}`F+Pceh3n&?*WCbPbDcMfR9!d^TGF{0TN-k4!lad)qo>eka$)`%% zl2!Uj#wb}q$$Cn*Q?iGWLzGNca)y%2l-#6bhLUHM%vAEJlC~ZyeI;X*te|8)CEF?4 zL&+gZrYku^$z@7zQZhryvr1+v`BX_;PnEusF-lfYvYwLdlfvYnDWlpLaD zx{@=LT&CnEB{P&ft7N8E>bl0B3hqGY;~Gn8DWm)ok{L>#RWeh_r%Kw?i>ZK;F-lfY zvYwLdlQ2ZWgXyAV?XZ)l z`b~)6B1I;ih8-j?&%E!Dcpf9fi{V22SyhO?-W1~RX%KpiSQI~hZBds6H&9(#dMxLx z;KtIp^vM^Dnh5GxNMkH-V>o8q_b}QfsB;<`hx^{HqOqWEYv70b7RB->sQU)G_G^lN z5gGIjzXnn!4*w(2GN~QJpOPsY3a#@LLHWjd!O^=suc7u&LH#loKud6+&RvkjppNq? z6F(h-I=4)mr-W4|exz$76Ui`poBTedl#EQg2Muk?3$61nf{aW|G1NvT=pWU47pnOz zhC_TZSBQxk>dm{*r$ItYN)Tdl8zDYR5n{@CA*L=BV%jbtre6_a#$zF7W(hGX8vi%c zyU^#2gqZ!d5OZb-G52dB=I<9`;VmH+S&Ndv7hysysVl_tWFc0L7h?5FA=c~@V%;?% zzGlV9Zhf#2-y{gJp_vdHdkL}WBOx}|Do)IA+Y7N}m=NF16k_WpA-4S_#P&ah*b!2K z40gRH#O~LH*fUg!y)%W_w@HYMBSP%IC&U5oc(OZKOo&4bg*e8@lt^w-^h)TNh7-lM*i z|KLr|f*-A)&)Gx_JNWWq(DkY&=}! zjQI3(I&MRmdeC5eO*`1Z^Wbs(2%MJ0p$j*W1(mxGjxWE9o=ec++&H2J^6|lVxQX+* z$)P+ssTNq!t(WjqfEUCMN0Z_k;*15E1@yvg(5;JrL-=_-A#{I=U{Q%d?_=~1cJUKE zxu~Sy3`1#MC^@AE3y$I$XcRIG-i8nE2FLLNE#&EejkHn zh-k=ioSwx{QI1NyAU<>09_{V?l3eXwq|cW;xPWjF$`sE_T&Ao z$a~3R-i6DrMq=KDD_kf=;3H7T^12TwlnG49x-V#u4m;(&i$EfC;rDvP{^DW4wQ8d zQ6n$?iQZh6G96j48DpWZ)kN_k$N4~8s}YQ%X2&7DjeGH|RK#ucNIWeeJGSCqA<#I~ zG!MR-g~PusFcv-O7o@f4&l3{t17Ls&#$3e!6taIbsy+?MaRihltC;6_phFHXAIb51 z8u(RLH!!j-+#OFr58MdlP6Kd?LPkIJMP>c?>F0P#kmYkguNVv#y=gpr{`@7vJp}#^ zz=|@91V!7)1`M)bS{oX!&Bag0QeNV~5#HrVOcP=<-$X9%zg>~BDuC-{bHid+Bajxl zjc_`(ogly*#VsgT?olTsYY!LDTF zuq?mq;%q8A*2HT67Ls6s{yHPqOlta0jd`)%^nK}hAF1`bZE%~g&`1~NW86cj0 zmCWK+P?kFI?-2N-4*DxN&rP0i6&ivdVMtfL={jSDe2@Uusz^n4tk4&qz{T4<^gGTf z<%S=(pgvRiJBb?WVB#XH^uA}ILw|<rgi+4%Sd~)b^(2JGdb@MDAlx<@I-Cf2b`@Ucp{*wZfhSbn?q)3 zH+@0V-+2r50hj8Tz4?8Hh@+4RvdR#2{;S113lzziG9%gVYOz zLD@(wemKVa5W8j7WR=DQ;8Tbr#`02gFm{DL;co>Zcgvw(pWVFFJ1b!xX*o~=?)L*b zYTzvXMFBv?EpH5y!yeBBbdO-+IsP2p*CCZH4S(Y7V=BAu1VjhH@^P1(#+F`KYQIOE zqQDb~W0sPYOiE=TT`bQrS_?N#0KcByji3y&*q$pdDS(G(_Y%MAG+JVb<$I(gI`LHC z^G%L?7zF1l%XlnG1ZOkw-6lsq41%-CG7PgI!MOxH)8xp9p&w!?p>_DIu_~=B;0|oo z$x_L*1g9-8hdou{ClYwYY#fiFCtk3Na?2Zv-4e_slWmSHOb|nUwY)g4h{Hh|lZ_}j zp+P)jfa1&tzQ*KubglQBqRv9B-9r%@*#p8clO)Pnnx^8Rk5V!8iy~wK{>`MwViF~! ziS;QaX@cVj!0R4|yDB`3DI98iSt&JQRRptv$@VCwaNEn8H?MM=1XAB@L>4bN!>kwZ z<`hZe$LQH~ljBj$YT?M^B5S`==zuFh*kO{y3)Ibw7tE8^8Yfl5o(J=m$reOeY(czf zO`oRn$MJX-#^LVQV*K?iJDG&`{GP1+u$ekRdORlVB; zdzBvFQROim%%%Up_G}5V3!TS1ey3Xbd)S=M#m=KSL{jJVf_NK?=J*YC8^RYJ?JaUg zCQKINq5iPCVuyiQDHpadk@YPS&}_k&|A_=zf!@v*`ws~pFq(10LxtNGzcvS7Xwrv$uJq-taS}rDLjj6Jv+3Jo~seA?c{#;Dlv*ozz z^kSZ)eS_6EmE|hf&rJ(=H*}Xiu1Fp9sTJD$8M4BhL5vm0kz4-|U6BQ~uZq5Y$X2hB zT&Fb$TXs_F5OVfca12eWXB9moDFbt%#8XHm^pv*pAhJqt*iS%ONGXfAu$bYS8D^zU zE&c2+};0=x!*KLfLvixAydzCHI4=rLD@wRHy z&LE|jM30PyW4;}-(Cct(BhM;%4{8l+t&(IcY``l8o`*-zC}MZXBr9h2yh zQ6h%6w$$jNh!z9@f^g*C^uld*yI*CMei<+mUuCm+;kL77Oqe3R0a726=#f#{%nW(U z{<4%xKMka*Ceb6KL=5d?DgU}6t_EqBNz7TJ!tML^phl|xFM@gJRW^$kZa=id>@&(A zis&njSDOAjO)}@(ulXQuj0cLB0I8};q-Il(2Zg9ZzP8s`h!)QTu`NhlOd^$t#S9}t-h8E`MbBmTS>N#vnjKW4SNiXBiHILJ^j6YL$ zjrj9#bZx|+Q^U;oGo@?9pXoaH_;Xx1%32#%M*NvFt>Vx3Zs!(%o`(NtItZm#@#hwp z%u{&pD14m8;Xj7DoQglMK%|sF{Jt`KXo4#K9Jrq|iwnu)@b5=fD*l}Jb#!ScBiw+D zKR3hpRvyZl2H+HhRPpDZ@yJq(ybiRJ!7$>_MMOQWB&w2h0 zD*l|b0S^ZnYXOj@ZdApeAAQILx*fleQj+?Or9D@#o%LcF_Ldyle6@(YKiqfA0J_ zMh8IhfhI_HEF=Cr0aJ=Cx#24QJaz;^eefjoGydFdILeHG zn@m59a5|as=N0LQf15Zpy=+ez-=4}VVm8~s#6|u^`HcAUf>m+=9Y!3HJvy23XZ!p3 z1sgz%aLC?ba%4Ri@n^jWXZVFww$&h-@#p?Y2(h8L&G_?WP<}RPM*R5^G=Cdf`QRDx z=e+12K{#Xv-leW%#Gm7!u9gk*D1;GzZVE!re;|3Lbd(PTYl6u%TicU#hR+3Ib#{^% z17vcS_%>iCP26m9>BNjb{|v@`gOxr0Y+K0LhbSetMWa!0cof`-Ki6HvB&Qhg@+L>L ztr35o@V)ZW40s2VBl@2af4)9aKC^rv@L>kWj6ZAbuBJo}=u zE&-@4!D3L6@B;1TI4l^+x-p=ZZfg~Pu8XWu)9^lk2Do7re{On0jv#y-ph<36#heB>?LDuXBX8gJ0IgoCF@HiXEj6Zj6snYO?#V3$($l@FE z=evKPgTz8zmLM|?X2hQhuZH{jz?vGk8GqioUJiS_2cW(Li$UIqKOaNj*o$foztzkp z8?b!bjrem(td9Sr>iZJ-2I9DLpyJQ3W92K{907bTyBigM-g-cJc>owc-t6hch(CXI zPj%veIDBFQhfGO~0+REU0UOX8Gr6SUlmhF6d*qici(b~Vw4iG z+(p*sc<1n9Ak;8P;sxri5F`GaHdeW84`wfuEr_z%D*n9hOGO+7(wtX`o@F=U&zPZ$ zn%V&R!R#g;6`E6Ej{3%!Vdg zREO;1D*jw|lXBY?q=6<;c%+jVe?GcjdHe*V875IoPK@|-d90O5!Pf%ck&Po~F-H9P zJ{DM_j?RO0-z1u;I%Ok>DC{bIic96uRszcf965_lY|oY;JH(&&VsRi+sSca=x!8F$ zhe+zYeh)D*Ve$^>>HlD2#-H0@$uHOoz}%7x+oNGb0-9|-A}PY;6zDfi6VWi@%fdZ&%sMrn8br# zKNl1CY}trEXQ+p&GuZE%7Vd87F1-aq&&)nV#J^CUskVwZ-i!CIOM8GRJNI6GyWX;EuO;iVAUk1co$Qy&G>VD zM7$}hyaVvghBq_*y!{gPWC2zWr=SBbl8BM zX4QyVU`G781HSw!*w?|NxRWfH`AjQfU9U=e6)|sVtaoroqUlx=L!W8KG8C}_NcBvj zsCG)pj6WB{hs#CMok2=5i5^L-`18b5YWg=8B>cDCoJp$^srd8$mWsUz%#2srX8d^= zJ`*9*zY5X=ljxCA6@PBsOL?@H!O{oED|Mve&w)lSDFaeHlW5kF%BYGzpT)B!%MWJC zt86p=JmQ8?evoFFM30QB_;b}qszhIdw96!V)ToL-uc@m%UIgiZNz9p16@N~AqTJfc zqWm~sX?hiZK7tQph!T|nDbXZ)WK_kUn|`W@Z-CUtBzk02#h(MWC}J8&vrJ;njH>u^ za5=^P8q8g8-rBM3Y zZR4SI*%t&uz~av#kT9uvpR;w`{vj+6G5ZIH5E<3%bun^}9BF0cemRD3VukfQM%g+3 z9$MC3WHf!#>b5WR_QzO1+U>{bwQpaZkFwLeGSVW$n$z(S->^x4lHsC`Xn%W?FUb7T zT>9#kZ&@-w9jHS9^T!}4o< z6f7#ZFqH}m)#xMD6*j~n4pBs-aC32Dg%!f*|3K=6!+$dM!`Iqj(GiA^1r9b;>MK6J zZ8eRBW!aEpUa`#*_|*6ua5xX3WGdkwOt3Irs z@RxV~6=TS4puBTcO6iu$!V1c}niXF|1%?%tcOxrTM?-}bk#~$&&cgSh!XnhIR+)7M zH%8rBE9XNE;j2F)hs<7i!gkn}P`Cb-=U}%itdzQSRh~5tXgPITv*L{3VOv4ouB-SM zbGWcd@~%#f~rv>mct~m34UV!+LY-*}M||-A~w1{(>CY1Fk_-!he`yVZ)`$zsggrm%_$z z>OQHXnWGE2W|muv zaos8IEzTm=AT;Ml`h-j)80yE-m9G6GEx93ha)v#H~b4)-1{Rd(xSQm-rMk=w2*8TK!va+ z=LOTJ%M8l*?ynKzN~XNBh}!r#uTKxUw%14)g^j(wZ-GMp6UIM*2vRu!|8x z404}Io=a`TTh`W+AHWMmTLtwd19B1@%PRB=S9^7;7~9v%52^Fh@qVIA-DvfvL;-7Fj7Vr}qqxOTHG!}2-r}3W2 zOB%FK%kPlM-JJRluRtL7%IB;KlwhxHj8qdJZ5re=Q2z7wQuRwLnP&*yMZXoU@p_0a5;wGSm z;W!z0!n>qO@AWhu1x6{85WE)9M#GPSv$VJHwyaL@F+gWb zYq2|Ivs*SId@HQ)1A1&)OW0|7Jex~vpPCqOaHzED%L}$(%b|3{_{e#2KxGJ~R$yZD z$QEfCwFjREA-EBs7H(_WJhGLrEc;AZ_Xd>Wwia-COUex{t;YiT)NM^4;J0KN@Wb192d;pIarJVujh#RKOBU^uqK0?7a0Npj=`t;=m>x#fw7-m81mzb>s zP{(5NqZ%Xo3oJob@K&W1;y@~C5EZ=K@~jztl7Qg)fSM94n{7mUSg*Cr!HZZ}_W;z_ zZ7txfmUI1-^%y{(nAW9=B6Mv#W~qJwju%3`%CMAOzf??1;I}O=Fl_Oiz%mS+m7;wK z+cV4agMcnUooPVI)IzP|w4TNnj`^R!crBwnOH5s8Z zOyh0oG{vk@^-ple#^6<70*C+NG3aa!g>pg_DC!wXvHoDu#qe6NeKvF{hAkkSaC9Ta zmt+eR>;Q)=EXZs zm923I@DD_xj60u+@{wtrXf$sgya7uRwAv$Z{?5*Gw_4eRXv}fv%|?Si#Vph3VI_

?o+Ni=+wkS0g0_pCOF* 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,h)Y}>hG0xF!w z{q6;C*1bxmbr-HjcxWys58k+2mP=#~+n=hU`Q@ocC2E~=IqOla;siBmRp=QckgHb^ zVV$WN*L6o9y=a zu^jDP#O>0B{DJP`G-A9_-BMBE#w_}l7DE>#f{3m zr1inAnagnd*2zPEYX0b=(n|Z>#!_WfF1AG@N;f}E`XZr#OD`o%rE<4&R&J+JT0Zgb z%9TbicXJN1E~+>bTKC??N~wuR_65E!+X*m|4ZA=_IMC1kcSWf766#?*fKr1#c3x+o znxddqlwJsrbZ4mfZ$LwJm(IE&w%omQW6RlSP}&ZM=cv*`OBX*Qv`7K#E`O`RlwHS; zR{tsoy6o!l)AiBl1cohQx&MA`<^!4vKo|{s*p80OvY`^$$?JT#1A=Su`Wv!K_m>?^ zL<2=uAjMn!10Jftxw2NSC^66Cdb$`@11Do?u96cS2EcZ^0lEW|eg5=5(<>V-j z^rS7Wl~ z!rQ-*qd$c;>Z%3VSlU5hsffAZ{iWgk+R~|jQCht) zP`ZX)t! zH@oQDTTPxGlth@3Dg{?4wD=$zMq=1g=^x65GnYi$>kcXjN@Wvf=Klcoz(4c8Q4!E_ zih?n_%!inEK3u54PIW+WliZ=Vzi1deVa4f)WuNM;N2ZoA^0%>-mP>8(<|SKHX`JTI z^n(dZ{|8T1l1hrhhX#Fc4mr@esy|#LKm0$yoRsQZ?aS$u1&BS($pq)8WZuAiffQYf zc@GM}hw%;7CWWnZ=0=`jAVoI>w4x~g2?Tm4OYDK+#whtC{o7FfcarjI++?hKL2_71 zIAYTb@YFZ4rn|!LRTLiRa&e^BW-w%kI{ED;5+h%d+sJUnd~>+6lD`}hrF>#jN^&z_ zpZuMb>2w9QRIBN-#*yYeXYfkQVg7}Cu=PBC?QYk{!BULAKotoY7u0j(1`#MmJJ+~I z6QS}aWP{KeS9qSNZ4p0{Yz;Iz8FC7&@6@s1Ez5Tv!3vjIo`$p>$Ct_#0edH6RCOG{ zD>Y-kT|TXQH-8@CfuCLYeG=Fx*V;j$?7Uf>S)eZKyJ1>~ZbeZ&_1$$!IX-z}4XfM9 z<@?PpAwETGwj1*pUZ0mqdbi5@ue9*1_ArL7v8t+Eu6EzhjMYSWtQ&QhLSf{K|3Gif zf7@ah5Z4XlWdnD+xxe%uxfVi%3=K6lE=eULQM}sPGZ;(ivWxEh$3wF3Go~d-`mgK* z6>Ew{Xd)FXJ}o1xAj=D8*2d%rd;ot{#d!aGzvq{=HIy{|%48%@kppvT{_8syyYidm zQm58ZyH+|#XyyFHW|oUxGp*5R7HJK-G5K?g4BkA07_VLKW#y;R5_$t{R298UE`Lh! z{OC}7d2YmV_=t7@GzkH-^l6;zAe8K7MgFg~2zK$Cz^@V#G7>A9!lG7o{+#ic2?1`D z8Q?OA;^D1(O3$-H0Aa(ZYXEKz<_e%)8~4$OjjFjCsey0-wn zJ_r?iFr^5_bT`)MY_GnFH9wQjT!C?~M=^ASDeVroqnkD|+@-xyUa>4qb>LIBiA?1Q zK}t@FdN9Q%cD&@g@TK@^9aq!^<&JjFUNj{O#NK&KdognH=Y~CucY3@PtdC|1nWeX$ z6|o;5+3Ojl8=UEXU;diuU)}-eCzdaA%5>n-FT4948-9E+o1CS)t8H~PRcZgLbgF}4 zUV3|V%Fy0f&qZU>j*WKl<eEn=er#Oa*Mh%| zW9d!=o*Yzi_X#CTn<>-lBE^kNf41Vd3nfi;8;ifa|pN|lT2>}NJ znQ(0|1nzwHK~DRii+~bF!o@hCGtt~jEYN@oFd?W4?H5G!^??vt?tA(b_wamh#anGM zk7O+>sPH2rJ5DYa#EZW%)Qo%Dez}J8&18E7i+zE57|h?}Pl-TNlR<@2Mk&(Id39Yu zS@fE6wIIq!b+mgznW8RvqfS7)b2K?znotQz1PN8N~+)7{- ztJ&dxvl-H5i=0N0VyRgFgEAa_3c$ThqYyAXj#-MKQY3f_jqPf)DL)FDBHt0EuBEUBGnCey6xB9MQ078SZ6$ft#*EVQZJI z`h^%y4TEdOAqniFLGEW>p=`9Ou^t$dCHTvBkNos``SSYBuGZ!Cpy{{(3#&`)iD2z1 z>y9rnyse9BytMq#gP(9j*pu_ga736zvnQ{Wgb9Tq7fJt1693rTkVS?YrH()LZGTW9 zK*tP*Mg|fPDvL@I3($~j;eof$|6VB{6Q6npk(1A-F|2-JXjuC|GCn@$kUIO;HB9VY$ zD^HaZwg4gA-j7MGg4U4ONO2z1dZB*#b9`e@0gM{Rt*#}l*MY8$j*>9Qes8)<)!S`amqg2+iq|Q1 z4Rvi#0qqZv{{`570INi?;H3X~lR@<`$IQlbf`sCJpc}zUr$0s^c-zLkw;H)Vx4?Lc zc%_i>O$&MCkAI%n9^*?miU(Kx)b0;UH;|C^{Vg^H=|NF$yXc;R>J4fAuu^FsC71qn zHx2|zGbWgktpV=NTVLA>-LS^8YKRey|D=CgCzmuqvR_p{)t9C%Pp>4^CcwQsk7 z1&X+{LiGHSP1n754b|w1g@CL2N)L@H*s`Oer#48UX4D941E-IxQTmvLgmCtAdLbRA zkOjG=6gC%XcQ1j8#(8 z2kp!Fct3RUc8-{DZVlGm~7*^@V(=h%-FLXF4m20FOyluO3^sYTO7wV-P-3v zc;Tt|tkqRgT{o_)zcSU%j1QF>6`n9MHnYR@4GRYE%FaP1jz3Ne2kkahy9MQm{nq(w z&-ZzG1nS*;2Dspj$?0W3n)L^dEZ=fQ>TNtQCr$fqRXD}WYxC!A{fbswIqRt{cLXTY z%w+9-;gWCFvt)I~B@Y#hFaLj;9m1UM(3qeaU-CYVS^j570dDV9q&w%A>~8^G8bJ1G zYy&{@y~_m3cftMke--TQ7EQd_)ZlUOCq1*B9+tG}eJ{j{-*(eIZ}U;M;g#=iii8)o)F` zol3FzZx;D1ITTWQj;nYr{y=-E+2>#Lchfer*0f*%+I3T(2#Pu`F#+`&|^VLQmwMMF%XW6#{Y_Kxnrs-hoT#Y0gmX?Ds#htP?6sAjoi~3Ko>y`A-KV9!$X&2Y?3;6dgsn zr^tay?1~C(rF!6le^U{v9ub^5D)+^w5OHltv+x>J582;gX`cFdkrKuh6PZ}kU zV-#*u)T{Ppq+)Kx9<(AlV(fD&FbyNO_7-xZoi8l13|k#-i6w)()(CHd#$1`aQIBeTu@m1pH_r@{<@fVAmM1AtmM{8d|6-v&E+1Kjou#?>s^+8HOQNN>^JLA-G4j7Ji%6VfETrOum2hnQH!s+;J*>%V>YN&Z{d0 zWIqK2kVAdwAu8sKknmz7RAQFAI6!0|%wyAm*tW2~h8iByt-tX<(Sn-aD@)eN~7PQ0aV zRz_W?9a5N_%ufB5^JVTd4VpEpP?nt| z8uv&W`Pe-B(9kll&EnMT{sv!`9YHS|R63-f7~8oFF}G)YH$E1ObCJ}=z1Y>5F?179 zAhpM@ZrY&;3sf3N(M8~pasIBJy&1BEN{ITkt(P(!(MK{YdX#M~=TjF7#C1}yB5srB zP4rHauSMOLUwV25?X;tM)8Nei8$ACd)r5S+>H*>IPhO`$5uU}M^B0;=ka0y{<3tL( z^^gxuRg$Z(gIrt6_@^%Cq4M!)syy1L(UAQFNZY!*BT> zS-lovyolXCG36Y=LnVlcch`i)NW9vz3no2e$xiT;nQhHYa13-HVpx=K*Zy)v*kzh( zt^FBkle;)U_^U*7)+Ur5cj?=>Kf!h**l%k)aB84TWkWIPDC6}xG`t2&!nQ4hErh6z zX)iFOM{VVA$gqvKi4iLwNpoX(C6Tp{D^beIXtC z9!E9!2AA;Y{F)Qy8}9Z*9sgvyAHf&6%)ZH%?>}K0-_LaU#|MJTq8tLLvj zeW*EQIZx%=BvsU1+{Q%Z6G^viX!=`o*tE}gp=?meq-5URGydRSR;PS8avK(ep1tXu9P^{C4;pVVab15qxH> zEwkc6r1IeQ99iEq?sUuaV7?!HE27?az_KmcWYA>O<-Zr692satBWHZ{0VjYH4%1Izvm7AQl$ARn*z&<*9C8*{(0{-byB+&yw z1e6a<*4z*DgP3#EBAJ68HXDi6U3$pp@$G=~hIp&*_Z=yDSqyzmx$0OQY>s2;0a>2- zw{Z6>-{UL~Upst>ds2Co4)skoe2k5Kw%nCXh;_{vmqfiVL*w~McD^=npVi|vIXKP} zzT98=**5w@8~0$91EKT;wR#y7!DU$-OCr=fdw%>V)yA4LH!A`OM^4iEaK60Ih8I&x zlNM?KS@jTYP)#X$>EvemvXDK2NWYSYqJ9FB5Xh+Re7c#Ts-2!CsBO&vox%d{S0u{` zGdW_9G@1LbO{vb30@qtZk&YQdnnsP93`M)l5F%&U5sY6F2%rU{p1Kd7Qz@cX&MC2) zi|Op8!R(8dBL1Tqv{kN@=cx8cIixW4j0hR1SfFmstL@Z%A^LpzE`krS-6GLep3zIa zbA!LtbK-QNGvuuJumW%6yuLA=2d96iey|ll1LZ$S!Gg{}OUqbazkF|H(>xAFr21da z`Iw$~5TJ~~v?y0K}q@U*K#>mCpXcGAx>3Zjc8(KYOaa3oPmgYu0Z6s1~9J>&Kjw9ug z9iQXJLV1bW@8Hc+X*F>hXw-tRfX)GbM5#rcp%G;Pfcrj!Gi5?NFNL5x@SW~|r4Eu_ z0gym6pe65{|HY6Y#T7o1Kf96^8dFb`Q!RS=DV_OVbOPTBx>jVE`VIARTX8UB@nj;+ z;L?3y;3|`!au|yG0fykdUGPH38L#?bF9tUtn*3Pe7fnG80XrerjF3CxHp)%9Lgrt&R;SD@m{4dI4a%x|^->fmZx@i&&>C|m_EYVv|lDY(iyxQ&|4`)dN zGqV=Pd4{u`O2zWzQxZFz6$CY_;=f?aZRb)9v39nhtsJOxtCWhK4bblyD5Nqj&upl$ zl+07K=8LJuk#c8J`{M*3Rmg;ElpYsg4i0$6TZiKqcwa0^g!<8_&E!SfJREeOHU72} zlZnGcd>uV&S4Q<@#0!3^S+wR z6=}57{|ng@=2k;P0HKYs{{)Bsr%izh6(tt|;D*jgMB%4ilF$Qn**UXE?xJ0kF32BAfo~l`y(3J6Oi&H2-=?R5?pNEu z-HWiCAFMI&-eaSG%6>H$@n|$9)sd)iXgWF$GusprE{#r1rAJ0?B=x_cwF5TlO|E7d zJ0Rd$*>6VP_hymv6pbM@}JGs>eCWvz$wOfxf0qca0uX_ zEg&su$;8*{Sta-Equ`1Xh^U_e4612aaotR8zePV|Ku^f|T;yu(2CJTAb|m$0yIyk4 zo$M_W*Jk^dq?M8{#77zb>d~r$;Y2&4zn2NdOp%mme7T$1pVP3~bm*)w>yZRi^Y zNdg!dM+i7d*{)$^zSaAWuOUfYwZ@2o4Q~*?06ByYJadLT`;Qnuj!vC_(yg#5?Vxh^ zQlUCFp+z=_4#G>c=kE1MsGMZQ0P8mp9mGk$-WU<~kWQ<3$i^cmQ6we@H&y{`Fh>TV zuqr0t$o-LMJzrVYRPF3o)kD?3BP>knvs%NI2Zp=OOoz`k8h{7zn+A*S^oW8dDGVZQ z8>Xqaomxj>{^Up1jxWIR!1i124^9?Er=ma=#EIZFV(`xZQmT;28{!&8k)W7PhAoaW z52Kh?76CIIGmPO__D!uBf}`6t9(3d>ai_2zQ7LKCjKYlBcN-v-C^P&-9}qCz888}* zv;7oaCYL+$r6C)FhG0zZ7apV+5peHCV-XI@zuyHZ6f>?gvt z?97U<9~Mf42Vq4yKbpw?{n|86Sh)8yt3O_%K}rInVlerxA$_fZs~*o4^w&{DT5|Zx z2N;9}rfcI?RPhASMb78=8L+|iAgoigJbG-S6uIwtDP^wxbyF~c&t76Te4avm*e}L@ zt_Y4bIRUDF9flU+5csbPu5i996%IMWU#Irv=5vO=4HSD48MZd({(nThV|-oh^F4fm z#5L2Dk$4@45hc|GYfmPzFH)^3>{ZpjMI?MAZ6sl?Vk6 z-z3mp6ZkFuZmifB?f8uwc(3vxg8r^@eqxrjB~d2(itMCak`pdWkrvp)Lnp8nV#!Z| za?kVtKJ>0Ye9SJfIaqYI2BA(QDAw5T2a)o&dH5c4!pnx;Pt#&Ih|#wh#U*kY_O{az z_7h|?SWU*|YbpJkZWs62LKbX-`SJKFXV$hyA0v^7O{&m%!;>;* zNlxB(>NjCCbp|0{pwf+|B#_7N=vS1$#zj+SR2?TwgtqZ#gQD%- z%Cz|&6AyRuFvQ3!6AXin%;VpQD|}v(midNAEm~C_u!owep*gX>0SQt9q2C}Y8e(Kc zx?4GMa91?ACdo&^UHtIJ1j6*B*cU?bH&`G!8%A&8La8(Ue)${)w885giL0KxunCQr z6USa3|Aipu4Q|DrDKkB~Bu%yhPyiK`e;yUgBp{EQe+?uG93*7!|JM3rKS1A4k7fzw zc#=E>Y^HYcA2 z@(OK~_w6TQ^{YfM%q5~zLFo!>{b6k&Ovm87=TCv79c>LTm```qan`3wfW^xrIkyqFmk{w91Rnyg@nDjF4&XMt1) z;pYm@Q+9T%zkYA8Xw-}Ypi}+_d!aCcd^{sjK+tX!)ycyLKJdYykQWvs1tE`rlznZ^ z!HB+}T?vQ?l%pbFg}<|d6Pvx1e6uU90wzD1=KuvbWaF3y?~I69Dd$rkvN zp1jn_sJCiMFoT2R9eU!`Ef#(p>ls`6IDI{X`g#~w9_5{r^=tiLWh@hC%JDJ%K8&Iz zp7Rgb-^$;rbec@$h>xXlBnc~W4-pP3iu$c<1LEW9PC4`d$O?9f5L$Y{yiSm^(uh488*U7}T1CeP>mtwAQq z3&yS0W-yVR34Yre^Xkm{Q%-xg9AN@l0S_f4ntS=gc8b&C-CGP&`WIQ&^VON{O*C4! zXtF?F5?u~QDj~Tts<2Qn;mS>r5hf9ZVzFIjJMRy63?x@E$@5zxi%OjJ!@8d9&eI;(sw@-at;p9b8&AuIkX_5XIJFkm7d4f`|lJczFU zNf7i4QcT8wJqL<=A5g0clg{y;iJUTB36Q@fAa7zwT>Rdk9n7#JGtmNLobin((c8$9 z0V%KsZ?F)j=`X4aF1{Rq6bF9rh*4ffOyFA4XhwoI1ygYiqOF_y z8hEKNmr@*AlS>Es71WX8*u@qIvLu5cTJbg+IuVc&TE&S@AgJ(5b6+>0JG@A~jI+55 z1Us>~xhMim;t3Eu>hB*SI9FZ%Ja?QF(IEXe4Xe60(xhR~uuqNP(@Q5a1Dv09kV(#< z`AckXq3WI0rP+YJAY$O_IiOtOAp)w1Yllw0LHrWPjc;dmH2~Fg5GwY;7)Jmt^DspA zOx^glgO}1Y*LCP z;49DP9aY})C6ENsLOhJ;)El{|_bkJ66ZwPdse^*h+BGgr-s>7nxNhyniT$YGy1PW; zEtQaqZl+F@4#OSMj77oDa>F;3PquL<8WoiR%?+{x_32R<@^?SO91htljp^-rsc4uaD6D8zG^51Ayv zcl%LUo?7G*4p|gfv=j8%e6;O}fg4gq^C5jw`dNtqLtsPAB+|Ft}f= zJ3KbU(f*rJ0~cJ8GLZpl(zG2BK^*E8Z)K@*6q^6fd!iD78FX3;x@`W?c|K>l^Za3A z8Uu$d6*y8PnHbQ9%I<5<({=qQi9feG)3vNB&IH^+kloK@Em}0tkjGV zDPRNxvLDhO9>llFcHba8L6ks9p@@7unzlu%#NCZYg2gp&u{MsF_q0rLcl;yo;EZLb zw_Ma@cD0bAH^)*m=&vFBs&M2!sn%*H<-P8AJwE=&E*QQPj}NV`x4+)?KE|fqs03=b zN7N#9e<;c0viCRGs9XpS9i98`2>u&UK}#iE ztdBPoWOUf?0}4AhNQo7j6q~ZN!GG#=)SA?1VtlEq^$VA{7cFnYX+54f%_oYLjGS2b zP=Rk z84euVuHPEe1EaI5kHl_zSwv_l>q}K^=R>+op0VaQ)$8c+ST3!yBz11Ye)MsJucAmkF7?!0kelJ>^k}oW+9tOt0;?-M{Y8{C#n11TbE?#5 z3jf_zSk|^?14FbQ%^!@W5HPY^AS6owK5!I&i8mlybIO2bVQSyx>#~44c?B5QanXfb zOoK`@YHK<#SCjdSBc(6>{PU9JS{ZiR-i7E*oU1Qgorr1NyoqYBJJ2{P{7&x_$1C?T zLXMWHr2>=NhlCH*KthB1N06!?0bx{_GHpliaqxev0l>S)8-Ji+P$4py09!<~$+@W4 zqgOqQcZI;Uw#cRBC~!Q!wfN^q5aq>MXxE77;I;BkFIM!!tc@Yy;^WReX;0 z%bG6gDj$z$!|`jC2b&4YRxT10!r)sKiUlMvf*B0ZA*wC0j{IBCg2m_%u)n^M0$g<` z*ZjLPHNgkI_l*uCV<1y(BUCj@4f(Kwzb+y}O2cPjVIGS(5{g}ww-e&0U-qji^VDc` z@Uh{@dlxpccCb>dKxUW$IEMafYd)fmm=p%cP9Q(616BBAQ0@Z|O5t6SLM;8qi?u`0 zNMU|-1{v|Qcsu3_YJ7D^v(sdt(DtZ=^2NyGb1D2sESFs$6P ziulvoimjj7m($}Dp;`sIcn*#U^0^K8h&sEa+bMaeuYx27$1Y8Jsvg=4$rJZ(4=i(g z2aGE;@dD~NFKG87`yE3afiH=J#->W+KRdnGQ)t+0|h9(Q*r|_;5HuPWsTQJ z<_^0v7Jol$R#uE-hHT^{81l$54Ly=M2l(xcI?$YLZ|P=v#;hS&cF$fd8nUA5sfEQ$g>l1MQJ&z z%HM4*Gig_xGtb8!W4p^7lW;WgI7kbWAmcpl-!WBu-x%wofz$9E6V`wXrskCFILWI$>`D!{r1*6}LX^DnT@XGYf8p2ra)CxM8 zW^=BY>0Qyt@uQ>Rf_Dfd3Sv70(L|`3!PZ`(A9%2-_S?3P-{q-1b_6fek+xLQ#zS|y zHx;i8pk{ZV4-G~D*=`B-S9sz0q$h`HjwtxBN4K4GD)<(DcF1h-g1zm05c*+PqE&9M z4xa#{uTTOqAuCiZG?{qc_lnnNM%lQmk1elPgYp62L3RB@fBQe;49H+hkeUPKl^V2e zdg^WmW&Nk_iN~vT=sA|h*^{aGL;ez~+lj}qxOV}jOVI5z(IsKzn3EKE*YJf+(8^hT zGU=i(aLUs_=GgXnlj_Y`V248C0)G2TRjf#uoNnq{cLBD*2KzSCO5ILv>eX9| zW&;;Ep_LvOWcv9NRjNN|b^HDSQKQp6PO|g8Z91!P@Qlo7mg zEKLVl0qfmnqpblKg!71$)cvjvxR4S~Ow8 zJX?K$kkubDPyk6IvTt#)ArbijwM^t~DF_nImk7~43g>Ym&nDi=_Qd3#_)eC3J5jYv zG*pn`625}uki>FSlYCAJ$Iv(Qz_%aoJNYu_MNtQpTCG-o7Az1+{uo2j^u#QsHv^4p ziC)=lN8x|eckSBeHBi)M-17{qe>V*`T-xp9wT18GIu(c2hr}*@-r%TppG<^T%Qnt# z&P{QPQP(yWd`>9&L_un37Plqga>)DOixAcYH!ou}2QSEe$N;CvC-|W4d2o65&X`R-yq8PmsUGxo3JS9FGqX9t?*z9e*zZq`;o-GX$wQpG{ zb`z0`=|*OcZAYBE5^E*qi+6x@w}h$IdS z9Lj(=^@nXXsJrP1{D?X|qr<*Hk8JN#cI_pMQ)ZIL1;k?{`CF@ZKN#_V0DP*O$uwnb zj9_SN9}+@l)a6Sm5%xuP^uYVsH<1^NteJq|KUJdJe@->fh6_jfo)o5A_h0`0I$XXN z`&P>ypO69sARHrbpzK$8!Wx$d(Xn(lyM$Xex3*_OT{5W+X;Q zwV*KYx0u$6I)oru0?j}w`>%tMgqexJLm;3&94VJ}IB?_a(+lMz+cKKfjsPd!-qqri z`t!lc4yo4llpfBl#njSzxr1x?%!BsN44yLGljBaBR-00-FU=}DEpd(`Kg+pCwKP+f zsaXa1HszG3rtMOwRld1_`yvkr7xKLz#uVZZNszpifbLtrVX5P*9MXYZUrBZ|hciV? z3^7`l)&x%y)4+|51qyIYw81wG`b(G;cFgi1n7dXFB4$pO9&a^M9KI3O(MD{Wcq^^A zckx#Li#!A9X=MorQ}j_K-%r+u{vT0^1eYY5GM<#2Hgg=&a}d+f!xpIWLfrTA=QRv= z{Z@;)N*|SgOwlG#h8NTCxr%oH5Cy8#zP6LF3Dp^~n2MWJdN5%v2V35%IMF5JWr2ioWpo9ltnR&k@i)lgC@aD7b`o^7lj3sXn zUv??HdIRo6gKNc4Uc<8f zhA!D3QYALbXaLsqKN*(~pnxU>wBs8GEwcHa{-e(v8st8ywP-E2=Ub=MCS1sx7} z*Xl2nJr&12j2_Z^7WG(D?cf*%O_R#2Q?@0+$TS$hdck<6DZGXd1yMaLq%t@%%6d#n z<*KnE`C>xSLS_Zs5J}JR(1jdpj|WaV9Y==s63n|+n183%it~YD$y5EEBV#dDyh#SU zTMW!I5G{cq1%@ZR}rEq+nT zZ>C`vVm=&4JQ7w=32KFQ)Y$V1rTH2dTJ(iYU1%HS$PWmh~z2V!yG9?@^Kh=&LuEx5iI2LS7_?q@}BF063 zAEr^ch3D6uQ0}zv_TXULN{hM_Q86W0rrT&^IpL^l@>{yXeJAL!`q`l7 z<~i~MvVnzkxp6@m3JD0Jhm_YIgLJbklza_(GZqz??0iv?^g`ri6a6Wtpd#31AS09f zNh~Hbcq0Fb7z_3>?*#?2Gh+Sr6?mktj@AbbPlk?x2F8elpPk9HOiY!}<;9Y*MSnNe zX`lfZSG-VfGIK~$a^DD=0|>%G%fU)ckYYl?6+=Ge4&a6=-9mnf3-6}%@%?gb*eTh% z+fZVuGaZYG%cK;7oe~X(VVIh5TRbu^i$+I*`Arepw9U^x)0is{GFbq}tSwBslF6FTaKqGsxMz<)k_a~^eCL* zLAupihLgOMsy9Z`*dA|$nta(MQII)*uxY^M$0s}W3go>|DA=LYlJe%3?g;e`EO1-{ zR}!~xzl-FVGm3ir(Z|$qA5P4We2X$E6km(RzH+^J4Tm`P`M?f7QXex8)K2=)gZx@Q z+R11C5A9-y=wGgUW(WjE01gBgqtE-lB|W)8)06jD@=Gbq>*5vWuVE)8RsmX81)~(J zQoY(bQljSVEdi&_z+vQDy0;}sFH#;%!>P}Ig~zki3GVGmb7=N=r zksGPAC|@k+C|)>}8EeQuShl9_O@veXRX7z$E|4QF=kyJ#Q-x+HneICf+P0;ByG89r zG);u%^WRL~Yt!B)xbmB4i-k%LmLv=6xvvDl2?+3X2#pr>wbwjz90cZWn9RcGpaebRH*yO?ImlYI_g(u5R)I8nq5f=L(5v6>#^` z4yjNtAs4{coVL16gs`9<<2r}CS0;ptFv9(y&UO&SwR_3yrt9L@OTULHjN%sgZXwsP z(&@p~s${Hyv$EkQe$w0z#=!wV7yXLXv-URO6dl-!{!XiI^#9~sKhhjjZwykTpAGM4 z{^yVj6G0I~MhOObTjK^eaO1&cLOf`Jm+#M!@$YZ~H%V02=Mzc%Z-Qhjam+AH9IWN) z27Rw~NPHLE!K3FVR}wFp0#i8AGu}M2ZA^wjmz!S1)n6)Ap#-2;d60FFp7u?fG?%5y zf16b(`v3MW70C1WB;JK)GKcC_E!xTPSoB%tJM5u3YyLFFICU#r6LvcWr{6DWlBRn5 z1K~p#)KcS^ISxIHKFX9>&1i6L&P8|z$r2nTi}3r&M8^3*J&FU#Bz-L74pc~^N;o7r z1Qwm61tvcsN`Q*k-7GMFujw}qzbQPw~IU@<~N4aOdCwG^IWxmW0Y%XKfU%bVMP|ezLPP$8zKMt zZ-}}8d0hs83j;u;0@whr-Vbn?&8->$fGU}Bwb%Hx$YjYGO3FXHapmpiZ4KEuPyR#? zd??*l#g^PxhNodIqNIZsI4W(4fov{ng7u_$D>Ql^xqKOeeXYFuoi+H7#~;TIP6kE- z6ViRS-hR%cbyBXr-0z~#RN-#SK?`J6f)-ahDVsygSEXOmNyh9u4GGI23Hes$W_gw4 zid~r^AmpP!P%z@!&mtC@;DABM9zg1+6!2OMk%Umx)aylUHCimb3nhPhJhi}4!_CkE zRiF-3C*lwge$cUwFNHr{s34`2=?M+~q`lG#zHmO#o@>Ar=d!tXpK9} zk@Ji0yOvCH(rSMZH)>|U2ru;uj^k*NP>q=5g8X z3aZ?BTBK^TBot7h^s-hz5OGC56GWkQ7-gLf56%r#nb{>C}F~+ z#MnbpMMJU_Zde5B-beb9pIF{QLYcbX1B)wdtG~+-+>z1$cm`s*b?NSp<96O~aHh8H zAS9Dqw-E9TD-bZHB{k|RX=pjXz3@76sE{=0HYKa3bqib3AXO%~#DxaZEYJMOmvso} zStJar545&ASP*2lhBG58HaLw3XDtQ}Lw7bEvNW1kmSf2zA6c}1`7IzEkGp+Xpka^RIGU<4hKo5Be{V71=z*9>uG5phN1_Hx;_oj zN+$UO`Xi;tYmfk}q!$-6XO-fz=F1{jXa?Jbao$~3ckI%O2>AuX^DW9-J@MQ_aKFC1 zUYc&FgdW*5w! zDLaFM3EgWHtv!TC2yOyvywTXcb_Vm$GdS1iWIFL14*i%XWl+LPsZ9EOw)5!^oORmJ zPV_GOAWM905%MgYz==U6L5n>92-cYI{|Og;uxey5A^$O$#6XhiA5W|gsM4r?nx)5Z zU3^qi$vYDkR6@A#uO&)(hbST5!+f<4dCrwRd(e5f5;IC%bvsdMd`!-zyg9cGD)!6RTe(rOrN*yDM|3f9|Q8I1*3$GxCj% zE#VD&7@uPqrewomBblN!5wZ})5;yW#qnqexHPn5QbH?0SLg(}Ii8R4aE;Mf9F#ZcI zDUO+ZT**qD6RM;T)A}j+B%X^-?4#=WBo%5f^JJ>|HV#a-g;`aLF<^&j&F=>;tSrO3 zkx!{!FDG+WTiBFowo|K<+5!rDYFfv-CpO9fb;I^>N0TPPj^4z{y-<}bIFoI<=!^?F zV74xQC-CAAGT4JFxK>dFzh?k2{+hB?crI{8dQ@H=-|I<+0*F9uAcPOdpc_PWhbKac zP@?k+ZYbN`Uj*ebfVllFdB)1%r*$Q879i#5&4%`D{$dCFUYPuV8vnxSY{8WI#+7pG zJtN?Knbor!?!HY!EwHglwFKfRalbV@69~&3$xalM=r3OIKm2tM{@$Q-z~Kh}CGZqj z_#@3IPXR7ZBBC_{(E-n2SzudinzwPL+4SsL#$d)lGtN9&N_pC@=@m^g(;dPFTQgaI zS{Ku7PV~%G!Wgv1%1OH16oo9CO@X~V4{_oHrOmyp{sIseij7G zmK~K*ODT^%0(;!l-5hBq+lOjOm&?3F>5mg6 zFWQ3W0s6w@;X_u#lLA|kup_yfP%^*m?e%?a`z{4;A3P7K9)?#HJw-Ao@wpvoj14Wn zmL>`<>grT9MFlN#sB)&Y+s%9_rR$l0nQxTcHrcgfB+1mGBRj&kL}A0V3spy3&sU*u zg@tcm> z^Wzq9>)ECjK?KL(aqH!zL8Pfe1fJtd$@&s)Z&ay0y6^)RcJ3PKnr-k|#9O5%<1Jdh z)q3OCLl=@tZ@T(<>w?OJd$=qi2Sp7Ez}4q}uowUb1rDnh1dB0D4859o>4PR&kSIMW zjE_L&JEJ--W)juY6>t287IB)u5<&38x_1^0a#^Nn4Ywzy6cpKy(V^eCmI)yX2l=<1 zsKd`?AjhxH66o7_;3dS&16Gh&6s+$eNi8ma$-O}O!s9l>82VrC-~lW>8f(-j8{N>p z{XLyg2D@n+3Uw4Rl{gVnyDe#sCWI8!A}|RZpwXJFVErUIl^D&2nc!EjTw0xD^KH2> zKdC`8BT~f##n3}d1hfj)KA&6lN*6TeB}atk>@P}xrJ)2@lS{WU9j)ra@lh$WKCSM0<%PjX-MJ1R(7BlRwG?ZU~S-8BdwF_^Tdz+^uG-M-B# z3`QSt#!#x$&GxiwY{ANNHGqj6Hf$WKE)xNVW-OObFKdU0!*#dNW?q^efaE?Iyzq(m z*tMb1;fRWyNlw&E1q#;gy}Y)l2Qeh>pN9^fa~ag|qYZD(M7j!Q`J4VdrWK&}@t+hO z$ifTdHTdJE3D=9)2LO)V$VOc0Mg-$9t2pF1L5cDl{URdyUYvK2A#wR>@gUQ`Uv%6L zMjgp*ilD(5i;PcVBEW0bn~IG15b_N_Kme8>DfjaN=76X=$h73eiw=wl=#Fe`7iH07 zw@|lY?Tw$|Fkg5U*(1Ih%R1Q$^fT3F2~s@sgEXIwi^SzHBxqdqEtdq=%*6=9^x4Dn zZS4@Sh3GNZtr*0Rh56E4#*N5<8F5(?eKrsc|1mDNJrN1a)*3Wbj}%XRg<&MYz(7!N zvowmuXdaMi4ojtRY0{S`?{c$|>gh!ux1>*n1lJ~!71e5@A4XxGl?~C%<0>-WLB~Z5 z$rfa*pd@#d#2lPahN44n;3DIl;23zH6nDI_*rkNspgPR=lK_>YhqK>l1 z${=dGIUiXL#we$%9tlZ9 z`PncYcdsc<6nz*_7{M(|sng7GxQS{!tV}7FODZ~xn}=zQfk?e&7FX(3V=Mp$I|adn zDGplkUr~?o^Ao7OtN`C5&tyV&Yv-j}*UoIeereNldjiQ#+@BvI(BhWg{z=jcB3hGA za@`S}Y4}y|_eEcUURQQI#UBMNn{a$=R=9Y9!n`avJrnZ@ny1Og(Mw(Mk7>zSLJqqW z*Y}neULp#9zyvownIvsgVV|#Py9WMP9+e$+D1LC0KBTxne*@yAUXcz(Mx<3y zHbBY(CCesF;eu|PO#ejqyss1gG`S^MJjRF2lF&f-n>hd>CfoZ#CdLC6c+K@p9siEY zbVE*cOFpjo9%3ho5f2}&=8Ua7PWatQ3vSg}O$)zjBR^OGbz#XWO=LzlTYpd2CiRQXacr9_G>3bZ$47ugMV|rxT>) zZg=SzJsnwf$WE{n{0tE4ebdy_Hq;qUnb2+XtXaCEisq9J z&M%ppJ{N~V;C0#Ly=rg3?B-@J>yy!O2RwPKcG8kAK3*ECJZt5zAB_ z%En7O2W>t~>@KT74EcNn`lD>n{7<3;HlQlwgRarxpZO7O9zY0mqHl(bBQ|AWR*S3x z#ASL`@OyWh%zfmmx0I{??8DQndV`6a7CA=ffTZAfWumA;#Un}9n!*-9=2eWig-kzq zd``tdU|=-y3pJSZW-1tye>2xQrOC-=`s=@}F_o{_>H|QdBvBU!S-9WN zi-C*016n`Sg>z-s+3+5pq2G`UWX4T@b5~f(8O?m zfDuwZWDja$Ply=E?=2tjP~O$_k2Qn0Op9bQuy2g>e}v?YYSK=jqGAsZ?RLne*JMC<{XhQ`HJ5t|1**N>PmEik@ar;Z{^)g7LDs# z0R+)_hogo2`OF5o1|cL1CuXf4Zg@N9!^H6GNTLN$#2hiQuyP$T|5BDp!?>8)9$t>9jaT zJSMWi8^K$4#CHVGlhNDSCv~&Vzo~@zwRuawP%CG? z>p8Czm43Qu&W0=#bC-$ss@B?VTUiU!{9-*R+RQUK_&PJarCHSM>?TCx@ZRl^rXi8_ zXyIW?>t_73V%>~}NM2cEt$F2B8-2aA6Yp%~d)lMs%m|`UOY|%2zffEggj#6P_BM98@7C0r!Icdu9b365 zQ(>@HoKH=5v=+I4GI)IS!RX_|wsq49=cYS%ICDO8gb? zH*bxO1S7_2p%g!XljIcBV_4rF0=eksB3%X7yZCgSNfe8(ToHiQKfPz>U1r|^Sn-~AwF0Qi&x(jM9FPX33k5gks zTQA)cyW5v;LFNmd$d5j~7_cW&RXK+ySBznl`|RdFhMkQrTUA&4j)m`ZqTd*4s?L;N zafn&}D>o!qFmMP%|F(GlOZ|V-5=HY{&Mn`iqYtez`q5WEsypXL5wv$n?2CTjgTjTJ z@wiJ7Efr5$6xJGcp4;q@uwHyS+VkVnI@Mdf_jsy6zb#wNlZLn~MN(KI4yqUB09E~c zvddltyqr~186Rfi6H7sWfWZk58@?%^S27 z1|dE&D)?@*bvka(Ou1@L>Rp`~4NAFn6}b_2E1T%=-iQNd@oFZ7uj_~ja@M~N5sX}K zP4^NGdxcs&AFqot)f95hLG+RVd6zXcryo7fo~--cl))TzM4eutM^m-K40Ayujv&R1 ze9RpliZ^ru2`}Bac23)%0mG{Jkii!2c3dOqv~jQ)R^?L8BpAz zrtU5=eWYZ@+M|I0eekYBL?pFz?UFPa(Qe4ygEib2pyO@Cljw4wmRDZl(s(8xy(?Wy z7!5MFXL*1Xm44%w)0Mm_=W|&Ym3`yxGQPx`P!-oTCE%nY0hTT&PtNz8dTBgg)3PV7 z!u5rsOTvQy84u#@hokry)xjo*dob9Ujm1`Pc?V7fxRzMzWihw9) zt!cWd?|KCdA5VvJ$!M6KB`W$idCQ}dMK9g!0SdhT4UeAG=jTjNS4QbB<`!r5Q*jgfTnjZ+ zcgcZC@Xk&(Ipao-o(jYUrztXEEGMxN1!_F1j-l+;f|d)Ljc%I@f)&7L{~uDr_>V*| zAPks{0Nn?JUfvJ+daw*ZNr>^>me52EZ8rJ(1YzWXEdrhwqCT&a_itppMRP9j8R{o! z!VJr_)N{1PR9*+c!nfVX<=&GZAI*`@I9TFN{?ECtCeqgk$-IiPNL)qAiKG;`U3H z$T+XpL)e5IW*H(Pb=;_U7_9J=yx=DyMfRWpT3>+ZUy>n3O|;3$XgSW$i8Ff%MKV>i zLk9?lk>RRY^;rl;@Dh5pE-bf?{7!dmYnM5UQa|7FH`lAC{;qjr*=QO#P@8H83bV@q z2u}_a#la)FH$&vC&Ca<<8!-YNjeoV{(STBjJF}*E>UDtTss6L`st?s~SS-^M465YP z4&=qI^{emgZ<3Oi-!Oz!XhRr^N^wl{VLwinDlQZeU{fNL2wtMgs_IsT{@E}hxZ!iV zz?2)#W|s%7Cc?%1^7%yq{ZTkJeCEm03&}TAI<1iW)$51BPF=_BS&UN(l6jWA2+MC+LKx^#g@dAi=Ug2CEdl8 zL(Vdud&&>$elPQ;6{NBFQ(^UIXSYWpYAczQ_aNrWv8$_meSR09GbX9Tfm&Q zXXP7JvZ1?2O8N;sbU!t#*YI+C@^n}SiSRK^KYh$JV&1NOAzSq?na}@;!G7=rF!(TX z0L8N8H+dht-~X^k&yh7~1%0`4Arcjw@b4ML=+hZ(J+bV{jq;HfFeX6fG}<(`i4{mW6>psj;es1xx% z-*)vVuTG-zX~bVb%s&?d?wu#{zPp8RddZiQ^Wy|ec#lLHZ6e6;+Kq8__{YcF zHT=I}O7lD7hhJa<2^MaER{}^U@1Ks-uMDd0gp5Hk zJ@gSba_M7)x*ya%979Cqg5h|1VJN$(DvE`B6JZtb*QPnd2&U_B zv*B|IW3;&{MKE2fAptQENvg-^1uh8{VbY%Q5QezXAb~q}-2N!6xpsI3LRPqJn*KoQ z1I$m_ESij%M8Cua=Eh`0N)&l_n&zVL;tQDD*h@|68DW1ACnG6uFGa3@V<|1G%;#8w zRYkz74`x%6>6fHDy0GNEDurO9PQ67D%BqGKbvfpckUc?FZp{|Wg*_NvecVPgWmv}# z3sK%hbu#KA!z;IjhTrJln2Q2Ljpsp&s}Qyk=LWP+LUaEIFM`w^XhK4jqZdIYY5ZS4 z-b?skR(pXS@BLdSxD<*nT;(C2=5KC0-V+=z&XRAC9xBRLuLb7OU&y?%js|nKEeHbK zZAhXwg4Y8C7Tu4wh$G!RsFV*L-ph;)6tl8ddCP(MM@T?#yjS9(4G513GVkJ`tc4iMZBH&>LvNqIp=bZ$|rc{ zyTj}S(R00jbYnv0VR(wvv3-3(zajt4744_R6e9o?4$-&CX9nXbVs^|~MahmzA;BKg zWsu{^AVlP*0z+GN39_U&ph@^b%Km#{1Te8JVrwyTky%Gl!S!$}Nj5cCP<5q5aXRQT z4kS9DmcizGVT11Dk@(!B7!?L(vg?+<-7bcO@7OqBni;m7_NnOl+YO)UXk;f{+iiFH z-oYGIsYMp#)qU2i2f)}9eg72^UjOD7!-1-lF>C>rl4vnx`ISWg?h(aZ*OnD zzRrcu59h##-mN+=*_~-fFg9Wvks9O`bqL`6i=WI{=fw=wTfXhelt3Ty_v|O4(-f<{ z3%}p~o1GSSf3LO)2}|wo(1~T4_IFNnoW%TuBd$eb?g&KjVZqzge4$P(WK2{h1-xpt zU{$j0f_9jNWeWoYnS=xv;*+fdzkFwy(Bbd>L=}jCB?1!$0TIgO#lC_819IN5ERqCC zi_PG+^&B0gB(RCn8hY5N~v73T%^lXBpH^!z)GfP5iIO2YDV!| zu=|rm4c9=3cBiI?1qjB)^fnGvseQ#>DRK(vCy@kX+|maDW0hZS0y(W{meIbEc;xF= zgWH5IxlSbcW`8|wohg-hdCLCg(AiegyC&w3Gs2@~gDW-M$fHe?H|j3*>q>;-jD#|cQ5D0GR1>Rmi3ijCL!%#^$zx~a;5NCl*zaU&6>#wMq!fNWG1qT_PR;7vt*oF8-3E=8MgIxXxi`k(jHIkN0~Uk zMXwZi6ZMZ>O-?cy9##JDkRxEsdYvOB6CV7tlmW>OrP{ddi|C&J?fYjxjwH+JASR{u zWfL26?ldm9!^m;Wx8F2vxAN)lu?Vi&$n^j4nTlCvbVvw9gMZ3l0iF#X9gHE!QV;;} zYCKdAG2N0j^ATmMfwAJJjJfxI_!RQ4PJA>jvE^J~vqqSx*wP@^BG50Bn*b{Y6RdcH zid^qKVionDb^Gvbbf#W5ciZSr3g|+`HyO>a2hL$ z5~;?L)+PuL_fpb9Se~)J2*ZgG4WOzJe_Kx+p&T!#A^#3GmP~D;+sH~p^)O*kh-Y(w zHpm*dAY?>b))#2n9Yt+{6vp_9cHfG6NRgivCla$kHXv^u2cPV^+?-i9v54EWNcv*Z zWMD0_sKqRcC_(Dx)BT6a*?iifY(eTLD|)#pG9u2*53)b) z`Y;B)_nTt1k4%g3>#|3tv2t#_GidDm%B%QZ9R5I_k-i+?KQ@yC;BG=%&T+uX6lbiZ zjef9C{-pil8EE^Eb7%cJi-qnQde4e?`JZ{xC(-mX$fOpaSn_rTbT<%1`9mZblK%Iy zzyi~t9IpE<{-T^Ypp+*czvMcGHa_vXHVg+sur>3Mh-Y~W9eZ=+gfs1-oK5AcR_Qly zzkqGMNT1iHIahchxQK}{*YO5T& zX^^C2C$T#b3vFe_0_YV_l}1k|i%mfuka#8yVO>SaQJC!9*?l(lf{kd8X&@Q*>!D1Y zdWKkF4%WcHH1kLWxGJ=~ZVwu6x!iWe(STP5GOWT`L|em}wbY#5aS#(z0XN1)?U~%s z)|{_N;%KrQ?RB`1!K{S65}FOPjq$y=t9$yWK+OeLiQZ_g+3LAt&Yrumu_Z0`k4L#( zjIcA`2sz(ufG*fuqm7L+T-8qtGWWjLRPPrzD1|!*v;yHj{iP~>N;$}pApWv6$lreg ztO*~cbpU_;UG{-V5D^>hZoRqm0C*beTSNr$1gmo)kG4GpZHRg69u1u66fsf$@wyD{ zGMht;T<@}vVY;;cRb_#5yfo2`v*PfkL0fH1+)tr$du97Eys4-vb5pfcjg@)Srb(mqboC|C zHdxl-$j}XNRzlE%f#%ipUz#apI1$x=vA|LiGi0N=CEy(b@L63cn#Y*a%*`>kgPn@W z6dKk^lR$c?Vq|jvFf=FQ9*4(WP5*Eic`mFTw|q7Jy-We-YwTG%;&7^Pc4tXPRm$T{ z8!)(OIShSItakEA?Zm}sN4Qb=JqrsOuz8v}$^K8j5 z2fvsZpH(QgG0wv!P71S)X@?~kHE2@QLYW}k@_5^>?{MNC$omp#^K<9)m**G?RI4Bb zkk0^?*%6;zPhzt?@j=B9>M!DHij6R^RZrml;a2E3Kl|)N@>p=_4A6=E8X^XQOn4A; zfk0)5jj`M-^aJUML?JjidIdvzHMZTG$eX^p9kCp`3nj~w$~`@A+ElMPFkV&_`8M-h zZ(4(jUhO*gr%urT=BGfE1|m#xu@JKqYdp?3Rx^zPLA$6{`yo)BzPf5oB1fgc4OMwT5d$>Dsoj#yTqY%7aX6`XpkM7|uO7 zT451&P|NEg=6NPM!mOKF5_c+5ms<(4pTIyRvUrAeq;uQIo<_q;l!=kRd{B}CYZ^RX z_IHPYSs6j6V*ac(r(kA@bRZcrd;%2TChbjI66f$ZOPIh!Y$tqPwR$9!L<0z4;6FD` zBp94Nkuvdvzbpck&tAv>{uPosgoXf|h&Uux{yUk`jw1cTJAA%hrQRu;{D5z1?_sT( z3P6!Dt-19$7PL`>wB#*HbisddEB;*Kr_s-k!u!?#FiVL~hKq^Re>AC26LR-mSaJh- zwLZsD_7cpk{F?5xYUsNIU#KK*U$*uut?Gb1s56%%3Uugj5v@W1EV-G8990>%msJDx z^Iii8x&$U?AwCiN`TE!C*=Qicq8@kT^jYjSvM}JQp3d1gnb5v^wA94_s3T!oQH>Ev z*pQ@Pxbgx%ZQ%7X(Gp5|w^49W)+KqOR1^iOlwT%%!o;P5n8BdKKXXleyg@fP7M1H5 z9bvYy2np0&Z_;hl#`{;@GbII4P21BPrDSk^Hy6ylxD)uYvS~u(V)=Kzn2@^=?VSPh z*yhhy7t*5ViMJ$5q0Z$~RgFtW%NCcc(QNptd!0K?u&Unwk zn!xm}48Cdd2MEXhy*y!jUY-Q~6$n|0L;s<*f6nrtSNxtS!VJV3^l+k*`avDGv?-hN zo8S2F@jRDuc08+U=KSLQb+WA(F;es9kxGH$kNNz_?o*yb5!d}LH(Vt_%#wbsMwzOdA;IU+lZ@>bo;4{z_srC*r+L7%wxQ3H(!6|g) z=SKc8)-r~2oGpqyI2dEZ>?rF=?W667tgcQv zKPs9Od6qs7t`6l0Q%|R6pLC0BE$qP+MPMwCNmd~L&pzoB^QHR{p@;=&ZGd^tBG3V# z;z0btyA7Ae$Rk1tptx&96seD$@>6=7VPvnF{MB3xrqACAjP#n2&t_le4ZW}yPW{#s z_I-CLZk+%EvJa|{8!58;gi$sQ+(I1w5DmzdE4XZE1$XVR)XNC^*t09T2R8S_W13?# zW|g3@ld<^JAzIN+?D*x(Mr2;B@uFI`d}VT=W|H9fNqE6bNia+d806r9YO>(sVYH!# zoekMWh2*seo1k%c1J27CYA@z}sZi!pkwHQ~ekiNmMPlW-sXg zXhiH2HU!(KgEM5Y{A}#ogBV{u<+Su~Wja*fTz^-naZDz9Ie1W6N~O*ln)rST`uQ}` zqB=cq-u|o-+3k;M1RAS$nqc3PrsLOMIu@`=FCgxVxUwS`5Pf}HQKBBE-vdV81mkpN zkdkdcdmNxi4DD ztr#)$-v+1r%VGy0W1jpkBPfarHCYb1rEo1oA@GRkksiG%R3Wj; z*s)n0WVq&8MbTab1!%3IQv=RmY2{SKZOn(eDL9-nl&EAwp5M%uS9ldjWKhuhBxu1H z6OaWp?fZWOtN>qZ#NMplcu7oCd`qo1=d?>9u}@nW8y&{&EDD%v1b2nHHTSK^-)dgl zyo^?ACHK$MJt(5VY}I$|kP5^f=`Pu*V>+B4WpaS3vA|c~7k7 zSZg$z52H%^Kg-!mMi{`2Nvu`|K%fDv@w$IJ>;lRUS5R4aDH+iC`#gH`1Zj^SDAarZ zz4w>j5Yg*N=$p$8e8d1tGD6x?r{9>0WI2(dFwr7UsPK8sFt!kTZ8!8UClf({V{-%OJ zBD1mt0x^)-Rh0#-?&^BQ+Rp+}d?P56a7n%nHE!d@;;z9(df2Ky$&h#uG3rL!Ba%6* zogBbx3|cb&No0nWq`Qc;+IU!>*Rh>_?ymcwkX^WwYBf^66T)I1ehk&)xbUeSPJ~&? z*7lkHVcf*}4iEQxjhfpA*F{mz+J@$~`$UwgK-ed3^HA2Ij9?H>!KgmgrTDiQCM&NfME;djk0;AHLgp^bTq0_xs}c z8TGEEyz*6fyQ?}%JExm-W1cxWdW{DsX;9MUaKfP^bUZ!^&TA~}M?TE5Q99tY7XGb! zF9HRhFI~smIsoFJ3#T^7IOxL|6B?Qqa4$&*wlF}FL_DmxKP$F5PU|p_5q!H*tmNm@iMv|CJe>puDlY4b)+BoTGxqD}S#9+*vTcn-YLUBIC_(3ImZQoD%wDX9a*9!%hZ%04eXqlO@jGWOC&(n6fF$Q z?bXPBwKzE*>Npb>N_4&Jn5TQl39^ZdK}bCR)s~F?kV1UWKp;Ed_3XvD{KOZVp|p6o z&9^pSZ}ZjHlNB7r=lkNZbPJl7v}KP<`;)kiIEUS`<-p}qET&qfM&-u)m~)ltczMjz z59Yvb}f# zLm0S=YMB8hS&afEo+!YW>oHz_P7{WmPF^;1LOPM$mk>Em-d{RRrUUs)9~qN2P$!ez z8ogYiSu4d>SMt5ZJX;$mPt}c{FGiOt_)(GF=_U}mrsbz|29I@9Y1wO9^B2wD{*7 zs;lV(9VU^f2C$-`qArx$ho5QnGv)@j1NZjhIy?E|URd|?x&K{1RRd{gAp`8hJ|UXt zUO>GeJ=`lBsS?!~D`z^V04~6_BK#oP6D03HHw2Mi?PT*je>^uO(SLb#^)JLuY{Z7T zmwOezI5@csd7k9wE#ybnUeG?$S{M3VQ5Pc=((qPN&>q62+(tE;S)j4J3QE?^oX<-b zLZ`HaZcet*InA-VoF9ZJ9$Zg*G!bmtLjs!udp4a3xvqka8sv#g5th#|E-8!31B5c| z@ug78-(IYD+GbFUge);ps?ZXJ{JMtJUxJK-iKkqOjxr`_OC>kDu}ct~UIS~nvE(=b zx6xw{hlTs~QuBFdoIWHayX#`jjc1)5U*2LGG*uZL?nW&<#*1@3j(h5*Pu$`g6Vf^< zgMFSLVXr

GsxqP%6JxT=OMuJ_AKx*C_(mTZ!DyBn=iO9m~#>(z|aEx3|p{lb87J zFP$+u4;|mE@_xqL6(6OUUhJJ{H~)H9wwQzs-hM1(BLp4#hpO?{${nr$U+)3#ztB74 z%$i`Iqy>gN!*iZ$ZK(F+jBszy<~+#@D&T1Kb~uiNs3=zy?aYBcJc?z>_c0HFe$)`& zp?DA_c?a(~+7;ioE1+K+v*A4u@QG<>`FP;xe;OxPp}5B65%v~>g;cPuV=t8s+@iio zyufr)ZnW99Wh7*^pK2_Oa8f6-SFA7&(&w6hwsAm$Mj3Gzh#%o1b;u?y0-|fqGXf!1 z`~j1gYTT}LG-Z-jkVU0-+e@{udYTSAS`n9xa0L#JU4|Q+kA*7L6&kC~2LIDo-xyA% zQWO=-{C5fnxH6H6dfGPiV1T#F&4wbqsn(VKSieG8p>#f_li27Znj*q(=U!DktSfb} zV!5aE%d47Rzt0&yWfe^nbI1u88b3aY0+&jJb`V!TH*nkYZ2YQ8YKqKKrS)$d{fy)U zmStx19Hl{TdUMwf4T&u|C`C?eu9aB-g-OnK(8~i@^{TQ}wzANgVu=t&px=Pj=KQq& z1p?ffK4F@~f4=ctn}9zMPv5<+_`BcEIx%1fjXMxSC4DyEE)zb~dX7bMw!at1ynVSP zQyCR&fNZ*wc5fz>bY{>(d7gX&k^IPfdBQ<`dD;n|OuiPmmN0&;nQI*!M5N|u7h5T} zv-kmb7REBLUq-HOV!1%#?2cybg>a@LloVggauaTLrd0d_~waP}NgYHk||=rG?=2_nO9$m3_RD@^kE#nQAEgKt9Qz3-xde#Vw4j((aD(-gbh!qfvxt zo^BIOuJ0^!EEO08RMEPaZK&e*B>1;<7Z%Om#W($iO9TJhl{gR_aJP=6Wr|OcrUK!o z{s)g~Xu(4Oi!lCgqVxyUo$Y?{C!gCPfLZo-n}hi%BEQ>mzf1OxKY2JKeb|$AY6~l| z8k{ISwUL)G=m@ni+$?scFE)l~`z8?vSo?oY(k>gqivCi?w&;3UeIT;|HlvGw< z{ran4gHtU$huOuV3MxYKEwU2qFQQ|hrl3LLQ;y{oFgo@y8e~w2qhS`#g1M1H*iZz5 zqrxzxIQ5zFZd3Ipty_uTZj@o*(ExI5igZQU-})LycKI%7JBe|aGC})JGQd-C(qKA% z-j1=7$Hd?nPj{{^i?&P+JS)4Aly!LAr+P7MwAl$~@$8`oU!^tmkSlNIA74^Z`NQb& zw7(_a@H@s;e$koake-EHu9t%XJM0%316F18Gx-{+eATq6m}9pHNjq|RqSedK0_Dl; zNRW~Q)}{_!V?ua3jEHD`gX}sk6_)WVlB$B`_Q!Xhp1~ck$^Ntp1ME|gpfdqh1F~^>Rk?@VS{y&E_Fd3>ny!ToIO)27>%49)t>4hgu~c_)*W2}%5naG$K z8q97gDA&=8U6|!O%@*h7JXtR1Wp+1lL!hQ?mArm0EfMxSXuN(nj+YmySQWQ!m0lEE zFIx7+Nm;w!V$Z0)Tv*^Q=)=7cp%bMM^0Vx68t>j*X0cYu-nCHnJo?Sn5?-`RL9dOA7$jV6pgb2tH#}7>h-Q}Sp zE^pJ2{K+b;mZe8!IOeDzCparjnpaq%9P8Z9UR zs@7klNkx#UXi}+u5;-c^B-93@rto4xCnE8B=jrvEs{1@sFxsbBLM10v$AbRJWB-0C z7Q8t~IO3uH9i08}k8U4Wd&F!kQfmc)7G>3(%28p~qXk?W*sA##xOtRzs#zCSbw4>6 zXS!Y*E@uuf>9j;Vm^K{4;Oq2Kk^FNA$-Y~$3LyMW6$N?dusn9?Jaz6wR-D)$(fh2f zr^k6B5yKgl15g?Y4nMoqf|F-U0z2nnf6v-`@frtuXIrI0c0iJ-kl{O|CH3kb?LCVV))Jx3D$|rXb zR|%L`1ZNKA!wc2@Gp{Df$73f_A^o^xXfx%q`XOWTE05KHf}^nlPnDJ_7E)ApnKJSALuhN}#=Jex6M{D* z!jIKScskxaq&e4ihUs0($y=)_OI8nhDcxQ+j$5(0y~oUf-OOQU>-^fe3&insJay#)xXE$OJSX{s7+N7=`*kUs_D*UqBF?m>A!g$l%Rx> z(Gvzf3!phhhY8U7=hi|=r{|Hx5KF_YhiWI6&6+0@eN(549koaIW;#^vPB1V*Ta`N2 zn6vUENgg4C2EXV}L=Yum%46lE${iY!jo*>iBxbZH&n+H4uY_I~gP4k2E ze9a7oh!oCi5ogH(qQwmbkoIBY1^;uP1$nOcV$o@KyPTmHO#u@*OBw01$;07bgsD|3oD++%ig)Busvniy-o8pG$YcWI? zV`KrC zw)QHbuv3h>1S6>zwjZf~+u=a}y_@yH-^c!+ZPVxJ8V4apnZ_>&U|F983jhH|$0I+r zK=5>!RHahW`ZlQh=x)Gm=ukW4d6%Eqm>&z*)cDBnf-YHy4FNxN-^=tn5q`_A62A+7 zhymgq&ik%}FTo41!Bm^BVVy7wpSHV^QDR;>IT;G0%xt^ob zMP3ykW}M!LP46I=r!PKHNJX9J099P|sT-G>@F zm0`nY^&M&wqzH3@=XOD8%~nJ0`9li%@IO3e_}3Ai1CXDz4l_8+RJD$BAR_LcXQtGb zLmfT>jQNcHO#1nEpx=WFee*qL#<$vWG6XC?RGDHshLzH14Wb4C>V zBeXG-Wr(H znLAV2W@z~PANG$EtcSL^es#)J$=0W~O2!FansN<*F<{h(l`7!ljk7aioTe_Ic2Vi@ zPVhOw8`x3Jq*(4Mz{~h?;%6@@rA47@Kzhw5;4(KS?fc-GXrW~VpJUd>@MXt^-et`W zWgRVC7N4K`q?P_^4u97TJPDO593u*BPe3fqk$M+MNQz&ieL}`!f?h05K?yL!8lj>2K>- zT{7a5g?|Ui$D+nC8~XdX`Sc9~A%>7cT(Q@G`~5tQ?hoy(2e=gtzrIR-WF0&{p}s=6 z*O%NwzCV9Ye1T2c0iuSi~YY7_OmDH%88k?rK5?=lny5I4?mQ%n@a}Uz2G?3Wm>u z?qaEpLJyWKzqY)z=>&1^!Ft+wq{Z&C1{X1>OnTBp<(rJ9q+^*lw{GI58;p}W@ zL2iD38exPD0bRIae23?FIvpvx$u={J18pOq1)xQzSL z4U>UrQ34B*=5mMRguZKoSsvAsPn(C2Vms&0^Ra6mBXcPD|21}U$v zR1ygYhdBg-lub&pL7{(Fqs+pGP<$>r@e9fZmz5X0)@g04Z?V1QMyM6%U9P{JuRl)2I9=zBi> zCBDMHa2N&>48m_QMwx#g$n6OiK-qzY`-qX^frf$#gZv)rJ}P6tADzXw#UH|n-fs9y z;t*5#LK@hcz7gXeK}OiV4)8Z@4xRFmAV0)Byb!%9`c+yWhOTG#vO4{d=FT8OSwnbl z7w&9|zJ6U|T`2-}GP0)NzRBV!SY)pQOZvrNM4n*&J^QVLF;dUv$jZz5()x5*#H=7=tHU;fe?h7371X3P zxM>-MDfDpDDtk|Hph>`6M`HTCSz~mkMP9CKxP7|-Uay8t7N!?QQijG)XRVkpFuaxY z9ulW1U_Jx|D%X)VZ=W1Qw0KfOG%Y5?I(-s`oVQEa)E%SxEfT-q>fv0`sTIj{s5hFP zW^}4y9q79E?<@&mV1GjIqNU+)kbsRx&1bU%Ew35%o+-Ha&0>%lBAvcvhHT`_7!cg` z_$olXGq}HY%)Ewc;)Po8S!*Zxn{3CP@zjQFs$V&azxn9-6Z1!A{i&FJh84VpOX^@H z5J(#s_;Suuej&4Vu4QQZvaC;EM?D*SyCJ62LQpel?gdbOn%_It=sFr4_1ZV7+JjdL zbU+o~$8(!8qPO~Ib1*P`(DD09#y zosCRLOq(@Vc1!t5Sio8kTyNcR&~Z6xX_gapx+fBblA#;)=A%A4xgwJ! z#G9$gYp}J3I?IbqsBH-70#M}7X%fI(`kdp%C?j7!b=y~_8i3jpOuum)Ndmn(vv85b zP?By${c44L=;{D>9kk}9W&uxnB#_Qj zthW$MgpGot39{QHii;{mbu*DHd#{nJ2`_X5ok}(eE(h`Lrw|+TZtNGc0pygaergwr z{Z?>n7TK8u&_u8w>u843*~>1Cs9(V5*6Ec54Pgfsg3{}E7Q^jOQJ{-rB9IudVHASC#~bt(Tau@Pf&Xn8v_nM&YFP^&RlY=dSz8l02JFlsPZ$a6k+f@ACAATr$^AFGcD35tZ zs~wVrLur3hb6a#T@B9j#Kk&8PjW>|yhbjoum6m3g)FTKIHD4Q<1RYr9TySmaD1g1- z1~$qQU4@>MdWERx8LjTvXM(U5zbh#zFBa0(m89gKRKQs-M$~LyluO!uAxGJ3bGq{U znEA<5miA!@y+5Y-u>T%l zco~6>pL58}Z|;`_jCgD9heS*eL;yXOd1BC4i{kUr=AxcF+PloXN;$7Go-}@>-Do0; zFn4-bI8n9}w#7bUP@a8ap4=2)757D^*YuAGv8GKWr{Xv8lpPhSP%!xWnn)9I!ugBY zZe@QNPO9LqDu*NbGLgl7fxl}$VBB(#=ka$W4)=(q5Or&TT#r^LU=XS{jIx_#-*`6H>4_=uX_^2_ zs1X66e=f3s!IBmvkMS>3`Eq#mN$r4tRW5wpO!YqtQt2fc?-~(4{J7&FdH>e44<>%L zm_Vy5SQ?fZ1VZrjI!#3_;8Y)J-*=nn_WN@#@(1gkgL zU(OPC43B~HK1Lt*AmUEQ7`}E}{HnJJhQf4&tDyhO)iTa)Aywks7tWt`W0aj>8r7XqCw@rF~EqyU(o|!L3*iVXTOq1U_|r}#tNCmhq)u*AlFK_yQ^x8$`-fISPYmx zk;^!3ON#~}F+17?W6!D9#~5R%12ZXDPtCNLN}(nj+AkU)BLRGvEl*~FMJQ2WQsdcX z^+kV#ij#H3!#Sl}F2H7atz@27lUzrniqYZ68hCk@~1$Attn z;E7FMsvCpi5fh0em1DJ+y5wFC!@d==2fV;F$cQng9H#g{@48%_s=h&+Svx%4{JDU{ zrry7aO@Q+iIt5fL^)F85yYuM*1VoI40yGo}=8rHhp^lMu zy(?Uoiy^)#*3E2aGq1;7tgmZ;MnQ?p#gu~FN^r5$Dh(!`3{pp~r~N->dSv65Le!Ih>vFl5 zjqtu(@cDvh(1K2Zcs_#3aLA@nE!X;WI_din8d!yGet8QhPfO~Yy?c@xKOe+Gzb(CE7xv60y||M-2A(FW3(qnRXQ~@eGCCf3 zyi7*yM(|^;-;DW?`BBEistc?UHKW*ojk)TW7*U^#MLKKX@OqO&iEaC|Iee zHg3go!dS2~u*M3=PXGJ@VxOOwBnrCSn{Lq0DR#4E{18`wrmSmipbstCXqXhu9(saR zN>}ez*tFxDK}nu~JlAKOUl-FY*LSBe-Z-$1QqGSmz`n=$A>0%C3XRW1-b{k=J~(1p5~ zS#Cj%!|CdtgixS&>pDB$wgX^A#dJzx-}RfqFUmj>rXu_8p;>n*x@!A&u%y|NyLDjg zyyv_0ev^>5Ij%X7o5cS#W@4m7L4ea%?jHh~KcMH~g5b*GDscmSXR5@b&aXM%S&=VG z9$AKb*L!NA5y?EWPpdE5(*k282^JELJFTPTxBCb<`~prxt_I(GNO`r6$3Rgb!?a3X zT1t=4UUnBJ<`y~F{7ATr&1HGPgIMCaZLZ*IbmL=?JJ3^%lR6hKkh)idMj|srk&}Y+ zcR4$B{Hf}&t0WVOrFRG&lf5)M214Nz#}3IP*lpeSP7v*y%nJ>F%yO!cNxOTx?1 z^G39g)*l|}08=DQ+=8@&6V87|9=sPgkLxzKtMuB&_sQ|KLA*J(RccQvinM3urQV@DT;I`=pZQZ zHrEmKmShe2)qxJ~@_)H9mn+zHtlw9D;A=<@6>c^N1*976iob=d7n3|LmI&Q35XT$_ zj_hs$Hy#Gu`T=dX7jVs|bSk~K(<|KxD=`ZffeK2)X5}?EJP)BQnDEPB`yL+AjrsEwqF8r)Eg!`DrC*7urQgV zGDjOO!T<}8iPy_1At@%DbL*o?uftb`$qr{kk;2IQwLJVWhppXp{s&%p2p#b4HC&Q` z($wW4`0Zj(t?=sRgl#He)J=|_q-s%+B@}vh%TdYc8H~ z9SgEwwu)#kmJJ&7Zo20$`A|-=z~_Lr{tL(hMBGe4KY#%K*MFnQ&f#hSg?E)_(IiX^ zu_zd33!W3g=8q)hS$v1i=l?DKe1d!zQ(FB;_%T)Z4_anAEXbdb$zRm3hD0m;eg5LN zMsHD2kHQNa)`!YD)a(U3r_tPU#tD5>X$Ou6t|Q#=e6aG?a@eLciNdqVY*6K3Itz^#8xx6abWm!RZ%fR0#h^0#d2^d3}ZR zq&@x8NE(Ym0$yo#hs3O7Hv-nZ?T0escZ~Wrlkm!a@5zvRAw-34YzdZ~MWQ2`1|?nh z2$iytvoy_rm+~GE_D=0F4ms0OL}El61}q4pib?fq6`^p*xgTF>bl+dkoMthg!hzau za{LGKf)8SFuUHdaaz`o;N(HMT2KTUQ@tcE6Awwznglrz%>1Ly;$`DvaDD=9LYO=l* z&4nfmxlA7Q^izQbtzEeaJxookq%!VYWA$uw0Xre<-HM( zia`Ad-6e)Hmqh9Vu=TBErw_5VxjY6~9tgX69PLEKlad4iALO)24n3e8_yQ zhUd(Ce6D%fv)))wjh_6n4_epgeF~OUHl3+-(*NW#Cfw}Ab~qQJ{r`)Jp+E$I6s%F9 zUbg@<{ojB%u&Bg4vzhm&eTNBNY(D8r(wuR>Q*-H87rjZ9lm&SfjXwYpEupoeQTh_{ z*!ko{!j>LP*sJCH0clSk^c%ON_uQ>&R(rh>Ty`e1NPne2&L4jLKQA{rOK7h`;?X3v z84Ku36imJuQ2mbrL8(UuLLhJg0`2i=D0*>Syb)EX)3ab;39wgSOZM?;X2_C>lB9UI za*d3UZPbxBC;<1^Tv9R&90{!R++a;*m<&HrSQf2rwxuMcgFj!(?`CeCeoj=-L3WgV zt-0O0*sv44+$h&(cbh%A9wl;pweA^R&3|lUrVV+(Bu$mz(_!`0WlI1 zpv2C7lzU&Ht(^g|Xu7&J1qxZ;VKOKWQ|PXmc;kn|ZJcKtzhm zQI03jG#%`V@VouZxa^6blBtFy(Vxjd^aEg!4?((En)~d+Baya(;Fg$ka6Ntov=31* zA*q9Q2Rf*!V{K^eaPa^c0IM5V*KZHXNwOc?N=WtO(R~fuWM*@i_#!S#C}?m+8r&N| zD-m7CjAIw--@L+VBt6AGBm=gBBruqfW0T8#%oVc*_FR?RFJi`OyVq#Q=W}lEzMhtm zpl;Q}bb^^|woBm4)8$@JAyIr)1jh9yDAlPI*ujEl8&j1cZbxELMysTvaTPz%qny0T zqg$Fpv)^V?+zmn8Be|xsh^M1LbT@&$qa1q%s`Q`1(kD2{i}X_w@sru{c=#!G0?unM zxxC)wO+SK*1E_+(-9RHYe+(wZJlpyCOA`DHamw|05JzSF5bk;}7@F2QK|0K?C%P+n zdPM%8J{VzS0J@o2@dxaj^W!le(OOd+uj8 zGSCvkvrDwskeP_*vNai^qC?jD?(lv?zsjw;%=_de%y(XYr|4j={S!JdWR}9Du+5`^ zdUr>~uDakTY?k|0UL;j%=LMp>%&J?hjEW1@f4SQuFYm(ZBrG+6J|CEC&szZoyJU3) zme%0@d0)kyuoZ62?noK_w{^+#B)EmHb?0Yu?K?)ZehoB(C3YB@Zo+@DeOL~)9Y zY0V2@Gy~`XAcI6MKVqW+ktReUKfL-!*2cD8^2=|an$)K%NYVxVFKw7mf`$yMkFo6O zuldhEc}*XF!S9(^TwrP1tb45>-+Cf~y71m1xBW&*o;OhM4@%Boy>6)MXW)wA6;lp| z_%q4+`7q)i+ur8FKKv`5{ZOAG4p;e|2wCH?Lqj44ac7;^rYHvdJa)L;(6JOAvG3K~WVGgJP8hu)fMu0Hy6Q|0JiSI}pg z(4A&lSuoJZclH&M#%%rm09(DjjL&6*wyRst{-nVXkH_iE>y66R`N?$l$9q?BbY!Rb zE6_e1UB&!leYJ;cV{1zky7U%?jym%F!{)t34b-B`D9?>XS1_OVQ%GDX4~6TsA+tP$ z$EZhwM5WjdPnCs~KLx)6sxm?6zPQ%38bidMx%EIHCN82vg9ygErr<3bogC6!9S<7C zYz+^no`bb%KD}^8{csR@1%iYs(2rGjBZ=45!Bnbfm98{+m zi3$aNJ^HbcJv{s_{ZC7TN+y{J`Z4uxx&0cS;z z#TdM5uEd}h3J;2hm%DoDHw(SYZN2byBSGixfLL0`vG2(Pk1=NZQ5@sQIXI%n;kJx6 zs~FW04@;G@9VD&wwfW=GAa;qO9T?y!LJT0!cJv~t#=$hMx}qUw70^3%XyP6XDlAb&I1 zWOws-OVK<0u+U^UcR8^p>&K=ya>cG_LYBbxd0(fU`o6B}Wfe!CT1B}V>Jl69Wr(+W z6li8z4Pj)x0Vkt7q!Kw^)h9l3Ucr50s60C;&xI*pqxGSDPbGMlJrJeOd?rjL?!w>MKBI%7_u?^QGD zH0kfgN8{7-&A9QIjTnJU4tkeD8HK)P4yTs8VNW^QQN>lmb2vpg9z%|e$0UJlvH|67 z(?32NMMCeV*lWa5A!?f-)C-GYRt9?aJCl;t<^MAdWmaqboq?7pc184bmtXqu3 z_lsuG>vsr=={)jusg!LKiVH(=-oD*aj!(rFDM42;fMtitVs3zi4OlIPWpWT29Komq-kxSgwGumuX|L9S-1NGAa3~k z#)tY+J>3pZKl7c7fgrwi*-eu%P3@AbG8g)R5ugwG)gA!cm~b9!0GBKIGKDvB%kKk4 zy)FxBZ6c^Gj@y_Tf3br^HlbX%s^b_!xynNE;+b$y`V zqhvI)9d&G{ zW7{2fY}>YNbZq;s-g}>O?(=$^HCO#rqsIKkE$&+g@)zv%eK_eo?6fN*@Th2yz=HPS zBYtOc{PqCL7y5|D+dF)rU;OgJ73n#m>PpKnhDtBwp|k5zF@Ma#r%&nC!S@`M#Y0ug zvTLo}T5!u%rGU50pk2x2)XrS8@(l&hSAeG$3*xE3|E=Na+pudjN!RkUoi z-Mz@diZ_oE`%i+dx?_F$8hVDylSSq$O2RMZdmyHLQCD?Wq&5~SxmGdey#aX**nhNz zi^B4-Vcwov9m!_g;dV`UWkoY0HO5y*^i$=za6O={y8Xy_^fMus#^kjui{88?`a>J*74U&wP(DeF zqzI7BgNbi1o6a0%p{4j22_>BAOOZdxJO?IzDKTEAi6B~ zZJPFW3qKCCC9yXo{f@YiRmjApFv1V-FUI=_znz9ImufkGg>R;pw_6le5fD#>J~U0y zQ`&er^KyzpiHyg=5Z%+kfzc*MJnnj`KJty8E2y4-E4L8a!(U1t`> z*RiaorDi}FBI6uzsQEnJo(*0-f0Cts;;CRghQWf23}ikqokzT95RVU;pbj}gl%MsA z)QZ`)(ZSz3`CUUkW4>mmLOv?4Mf@4@vy=%!E}H$e(-&(??ugC8_!okv(2PoBL=-By z+evUfw8xDBY=LUm_|kz`(=!ZEiffQDWlmg@5;0#>Gn`5M6I76~PQcsgGu{bj>5y^p~C0}RD%>oUxV1TD=z(s+Q zfEdRmql(HJR&-4=EwZ%F9rd+{2C_)W2at*5#xK}mrK5?GWt6p0muptg$HnP`5@U^q z1H%`?rU%`7BhEM74tr8iuh37pqIiLy-{3u+-BmH^#uz=#37hQpa%Rd#e2@(>M=-k7 z$gcS`a=zI0rHI{~J;lIwyRbWKSqLo0b6#a}Alk#*K*4A4ct!D-fd#onmVS!j@9^9I zXX5~<0th4j*a-ke`2B)^GhKc z^9;^ksJf~4K8&JMzu&b8OfCZj#JbCw5pSdY*!u`AI+~RoHbW6cbKfzMXZ6apAeqjs zA=c`QCt2qo|Bwj@*(IfjM$36~k=C#vQ3N=SL?~0q*F=z5GOa@N^_ejtZ}q$^%wOw6 zkLRjBv=giz1Pl=Kpd1tZ;xiz?BpLupD{ple>%_@kcd8%lCvNWQKo|kd; zIB1~fM(o^+dVRC%wemf!E^@&WV5`oT+osFteCpJ`&K{E(E&OI3>2Q5nQ&xH}N1{L! zL+iZ`N0y+I1>WP9#H$=(e{rC<238^qyH0G?sEvvPb{#$7x+k<;d3wRE)yUYRPktI> z6HKs81D(IcE>#TaaRpYMDS}iN#&_+BZjrZ0p-3aIxsB022f|VQGN{b^`F;{oh3xIs zhe-swO!O}S_^%BfJ_V2)^zUT}(BeQo<8$W5(uL{a9Vm(e#A`2>h&>W}6S1$aT@Uf^ z-)#@Kes%saSqmd$nJ$=O&pjiGJLUH#ek^$5ME;T#xLziGmbrG3nQ~zp5(N6(wzBzp zC*M^B+V=xqJj3G+S&o}Jpq$`q=W z&dFG`b%(@TAkB0g}LXL#aLbV8Y}v!)LXr{x|}lEW}WS|hB>(3{LH6fwBMXD zeRxQ$+-Bs23naXl> zJ>pO{dmG+d)VTdcFXN!hZDezKUQX=fl9L_z+yx!OIf!jPp;kX1x71Fd1)mM|IeX;~ zaKCn(M)zW-3l+XA`r{e!>tgVgYPy+TXAImb!NlqVaZ%RN?|9IqYYs{&*xh!-g;JRC zgR@T>kn~3pXUOLhkZeEz3=q)2Z%qKA9@vDie_R>y5$E`iJ_imQv&+9|f{J3tJPB3zOS!tCWb|(EQro(FAQWCB{j>mI35^E^6|^HZ8=PAF0KGQTCfj46)UL51i+lyP47VvD99!MAkk zkLpZLU)lDIcK;0K1S_-`HtOs(0OEs=4M1#iL}S@6Hdnv9xv%l@y}s9*txI&9OzmfH z<}?bdsljV$xQ$P*_jkT#N-~>28&4|Fy0r6LO~n^MNamNhaQ2HwS%&5)zfvs5%iCIjVt!}p>Y5i&v)hfWNdWy zGFyS^6eS+U3=g9!GZ~bVSSfv`%Dn^Y{_GrjhoJ;|S2Snfa=&SCMAx<$ea7`b64dj* zro6u{0{W0}<+5&40E)Ibep?VwU#K7qdm2T({&u0nAkz^Rz2Z`ocOYNe(XXodn=pl& z$o|96^w}U~-YnV%RLMj4U{5Nh$m}o#$Luvh7uPQ3Q zXh7a5@JgCwGOX-O5>6-M+4o0fg+tY}g_OT_@H-!#Pky=lX}V2pXtvqnxMlxhGif8S zlw<39`IF#(n*T?O?(_rRTYXkzv_JqqHRsn{4sjpKC+ci*EERCo*o>~IvNrw$L|dYu z>ha(pFVX^Uia2C2cN_Uj3|LgOr0LUQhtUVn;4h!%#nNc6)pR3fF1H`8OZB)LqF#GS zQGbe1hu9;;KgB8Cn=U1J&q7-|iP_?r-V2p`w+-hxWw8hqy;`o_HmkTsV@{?7?eGiDZEY^Ov zup|nZB79vJ4C7<)4|u+yQN4-odB4fN{Fv=B5W9Nw>v7q8LisTlpBs(u0V%>%hv1&V z60|O-QgB3xc#%t^oV%cj;F$W4%1)9U5F~G>VrKIl@tC?f`WD%}G#9juDxYw?((B{zhdq4pw!wnMxFvhC?6IFN63j4NrnqWf=f__-p2P_`3jvUnaADAO4Yq2+UdlR$-xML+sMmk-5nGbNA4D=#Is}A|=y`X`nI* zqNNSp-EQ3~)MK{2#5Ls$IPqs^bssk35GUv^k6paP0xZM6->EuY9d;T{@x4{nCeEoX z2qBxT2klrfkC)r|kvQ8bzDkI_H>;P|{o`#@@@ekV=~NSLlk0R7JA{YjP}DUq?c%#8 zz8qghfjHW7aH}e{k59K*oD9CoHU6uM^|sl&2zLOCrBl>;Q*ak zxclD-`@2GD?MU8o`;%|cNsc}9T6sZr8Xj6l_Wf22uVXDZJHT`QmHY4XFgr?~&|i}z zpyR7RaslAl#!%*E}YjlEK+Asdp*ppi>3ic`nfv2xihJcY4>rtf43NMx=d2Tc?UI!ssn zPJvWd5D)vHY&Z0sQJEymU3C4D8*LvCbxov06+TgroFy5jc8MDch6Qos5U0qyXlq=_ z&7{ocJBOC3R=r>uLwP{U+rItKn+wmc{9PgE-#{~+AS$E6sv2S7_y#Z0#f$tGPY8U{ zirLjjak9PD%cM-vsr?;M6Ua;VWypDEHwd#x-iSL2<{A~5;18`EFn)zU{FM&tMN@9J zNxrc70k38JF)dY`(W?FPvzGhMhNZ#_s5AHYk&+^rI}w3EYuv_d=lkz2=s4ThYvoH3 zSpLAgfKu=LH)070;}h{$EJOY`>mx)H@DrFQUKU;;-Z={qUDBVg00z|Sh19{E%Nd1t zXu^5ToQW%;)we8%9~E(k0zZ#g+-b$sfdTMCIGOXW+a}7-D8|3=L!Vu1!mJ-7)D)0U zjqz2p&Td#|*5sR}i?fxVG}YWT3;j5^$4{|S*K;KWVwuCguxa zp~DTLJ;}@(d1OcGuv5aCPq-gvm}5$Cjb&hCJXPY%FRKz1{C^?@K@RyS<;$lEN3e}U zLn8~br)hcMQk-oIhixZ0lAUD8v!KnW@C#V;^Hg2F^W9Zm0xvE23@<8V1Z3Jb@t&^D zly0&)#`W4(-#NLh0jYdUz!3)QiAN-nqw7`(VJm#c|0Z~Q!3Yv@L(7alzb035R=e^& z?00>Q;B%8ZdqhFyrAH>|4<#XYfx%FiNWln}?k*Bfe<0Bfdfpz*z+g!k(Md(YxG-xk zx#;aRD@z0+H{1sbant|vbNod$V+@Fu$ddc~!;0w31*`y}>j+VAna_1lMu8 zcMb5N8d&>A6Qy7VTXc7{8nm! zGTI4_eKQcOO~qohTOSIALV`gE_QDa5+HDCpfKAMKLP|@0c9USht~xIKSa~<8Zly(* zs3G~Z&E1eNA!8SletMiI29v1_tQ3qgg0i=AciISJrpHl}h)gqgV{{oqsGkn%UCCPX z=ZfEK;{!9c&mT2cV!fV*-pIKIlHU#8m$zFhwI4<|slcFH5i5t6_+`%`XW=5q zm?qg)VqHFiGeZM2u}bW4X63c&HH6Zs(l+ zY-g5tf-R&2(nCD-Te-6BY)1587NUj;m+A-9nDWXFP$F2R6LM9fg!KLmMuDzY(!9>< zS7r{nW;YPB;?fNAE+)tI@^4=3xLH>c-Usl+f3z{?x*Ml8t+vj=kyH2x`ET`L#oMG= zZtu7!z+h|BlK!j@64arfdZN*Vag~0QcOmRz zh%Hz}kt#wq`0}#<3c1yntBjx&w`)cio1#|}Da$z{Wjx+Pr1;0&0Z_~UIs>p6Rp`H{ z!@rbr_{jdkYkB|2FVF;rytw&yHR5x-Yts17upGpNMK)4u@9e=rwVF2?$N{?2UOT#jtRaPO%O_Bc#*#uG{-y&N;OgcB++l= zkU4vi%O_rDN0=`kTV#w*bDTB(S|P-Lc=)*YGSSkrF~d?Z&#OfBbs&X;TO`=9>jxJSaIE6rU)fV|+F!%B*<^?8HfI zN8MeYFSAs$tn+Z4m*prcd6-9iwaT%L{5F>8I^$eWP1V#s6;uVRh=8;}T0J&C?V7${ z!8!f5HpI5YA=f&jJubUsf#ZWtjmDjm9NVquiD9j_Buw~Lh zL;+~>{_WYm|JDJk>c|jwb+s+?@HJztUMD;?f(d}%)BXSCVH~7?6a+vbLJP~w4xr#5 zBn12jd_p84gX(i0r64k89@nXBj0YVGA7tHqQ8~LhJq-ptYhdCP-Z()n!2AjjvSrFX zbuR+Fm^D&s`^I8@o`~oJ*fEz`#z0f~gk|$D$ zsId;l7FoUzx`52QS$gvoYy{X=pNa8QTS%j?D5r=$Zh_pu7joV#H))e(GNh;~I#Uuo zs8(Lvon%Usk&V0Z)PG+ooB(!osAK&=6Pp)#97il+Z&zwM-;KB}nCw1naYt)XM2BK& zQFDsS`NDuQPLS?+T#a+X>(+W$U{nrxjX*rLR1EaMA`HF(mD*Ewh%i$i0``p`lW}B{ zQl{vhhvDkVR2g#@CHKqOb6;I56ufPnC~`M_?g9H=$Lf5j&Mv=q0EiDD&;KJK0CpSb zv~VRLO^Sa{I8T7`1HqE2eSx-qB4rF3ZRS!=bgn2Ir#@Jdf2v&l(UrMSNUHe3eML7c zBT5DEqLGkTc@val{{_CwI3N4SOuk#Kxqpgk6K$76L{LC4Nb78nbRqyU?)#=t zU7al?o;zP#zbms~yswlODYINSNqm|o?SXq2DPlGE+bD@;*O#2NLG3=)Y|9R%c^IyC zj__D!(|j7FydREtNdb1W&HK9t+tYsA8fn$3UG{|KQ}WZ*>ea95@$l8{KQ&Y;O(#}c z=rT0M3-MaA`yMR{R{2-@slJq@ZN-<`)T!e`R9&9OB?q3otV0#eMw%n3Zfg%pAdF_C zVZ{8_Ga2my9A?yc&wK%DC6v&)pn=*bfnmUCIRTZZ=-NJuy!Tue&_YbdFDs>%D4!|5`R%GMRS@t5fzgaiU{xmlXTiZn+@m1EezjpeexyeAff& z|Ifh^FnRVj3nAv;$jZF46+qeHvJl^;kg&lB=o7`5KR06Wo|z;LLmvx?KdGyaO+`+M zvzQQ2)s9RT$SOfePhcyZxWXOwdr-Z+Rq>lkNl5C6`vDg+CS{icvNBlfE_G{-IXN;xxMb zFgYyO)at9wbn8_;{yI0?FT?52WVVpB093$uE(WWm`5{M;cuf`{*Z4Aam$E=Tx&zaz z?43z^&xVYRq}GA?0xJDrW1^T%A#Bqs!Ft1ybXh@EMsfnVUs0yzEjQ}i#bdvU((CL) zUfZFn={Cn^BE}FHpeg*f1OU|-t4|n6(GU$6tWUt`ubJs@^9zuR1}=k_NZDvY?W!U} zxqRUuy;Z(rLw%KrJUmO>P%j+nhaaf&!Ya8i10MX7gkV3J5wNcdLx&MBbtT@>bLQP+ zC%Z*g+S|%l+Xxf;`C%dPzvE+e^CIdRxEwO>(ylRY@6C@SEL88OZ#+mdQRy--B;h|E8+7!-%Z zUW4^V%bq7}ZR^wtjuhRj?H?oWZ30hI?53Z4X(n76tlJ7dI#lc3ejE=_QeE#3pAUnK ze3c}%N>n9BV@;%MNscQ#O9yt2ea>n%M{;Ehd}wzUkM!ZS{Bh~AR+wsx1o@<1P6xVN z&)o+2XDOg4&#ZIhkf?j#+s^Ea5i|b-*8u21So!MSv;QMm!hsQf`5?gYETGqrpaVf0 z`LZV5*gZ3ae}Pn=ohwWd6!lyRQ9f+K0ZfsY?v+F8K;$z%?lPxe_e_+Z_SFYv3RgL? zbiQf{_+%j`V#>@?|r3Wd3u8PU@EE;$V_yVkEP;LffM#7Uoax$|oekhHY9%N?C$O zYVnR*utqqQ0c(8#c_>mgP(w6Ww}S$O(C4V#e9C?49)i`S4PeNAlwC#$Oy=dee8rNI zKPjx8IA}o$5$)F(MD2qRvY@Nr3TwxX zI?e+p{yBzY!$`~K)~w{_~k2xA8gD9 z8kL%*Heo!7d_6_xns#pVs}?aS@!c*$=M+~s;3q9rm>4y#-!R5^z!b73>g)FVla$}D zqrX$gpzd>*hi~F5!{=lEkKbzi+TSQ&pZFccRBjQ zpMZ_fus{!FihHl`8QEbw=7$cd>!BH87wt}fh0Q&U#7fe#Uad|Vy$p;%h@p_ydzvD? zYf(y;O};u3L=aW^NW0s~9r3eUG16&}ym>JCo zWbZP#)JRnHV_?tU6;iavaSpE4X)7`vK!7+}NwcQV8@J)WxCJM2)zB!yr8dGkVGkkW zE5laq1ltDGXCR={U=T!E4jHJWL)nMJ2WvB?&y>Y?fo% zrd-yORq!K=X^rqvV>)#$ijTCz&tA3k-5lg4BIPOJMK^B_ddXlv&b{UDn1!ii~>*m;FSVBnPPmpK6liQX?8Nm`sOezLwG`}OWIDDSQy3yo$~Qj z5*`v9Od`b~DrIC66f}DmRVxrlRq5aqQxxrpT#Sdj79dT%t~WqZEbyWK|BU0n-~<8X z%hFW;i!%7f+C&T%7GMEN5El3I1MMMgH>`p1^*vF3_=#Y#zrre`TSyYN_p4Ykl6)|felcK&$9p4fmAsEgdX0E^S z&8h@JU0$)oIYAG%KI;gy1K9cCp^-D_IxnMvig;%&zPWCX|BXNWWk+zM6#~+R02qV) ze?)pPqa(kS(LEy2*|cXeZp@xrLKcB~Xu_L3KY=;{)s98u>(iEuK^jsJ${nv9BGyT0 zSpbyj_t#IdoWsaB{KSi{`kn&`1nmHC=LaMU$NQ%xFPS2bGtprcyT#Ina(nP13nM_} zgE%`n2_Xbr8VQs8o6hc-^Ko!E)$;6iTtqZ$8 zqni8({r@P!#0NXbRN+8{g3#WK<4jh8OEgD~Q&G0wWTu<31Fp1&Qvk*o%fq zriBBew3N_^#*LQd>glfa3CFt$F_|%Q8ISa4M}DtGr;e15yuU7r4Q4m7Srsp9{$7#s zE6ZmatXRe*>nfYWP@T?*?QL6U(1Fe5&mDCPi^di)M|y08q>14mmHn!cRUc~}c?x>B z^=}n|3?dRvQ< zV}nulm45e0C+{Gr=4RgnIb!7pkFp7?0-csyrwuF8d;L;}Jnrijmc9SQGXVlwk^XAL zK#KktP#;wPLsmtQdMvefV){Mz-1ZAa*vDWp=yS!7V9wy==O@%aH{9lF;88~4f~{-< zF*XtGS?k6Zi2~z|YEIr)&y36qm!E!u{@8;OojsWrDs68Y@*~d-c1~BvSE#*@1Kue5 zhYv|FZH=6k_zcLpk7Q$FRYQKK5QbzjTitRjkt{_KIg2(OxZR2iqy-oY6jd6?yW8cQ z(v};(=2*`wSc8;UPzg?bT-Z1$5>wXwj53Qx;5~92x3_8@b-QhW_ES{ETE{1jSVIJC zFY-t)RT&vj_)XzZOZjgcK4RqkeD)t=wMtdKoQ&HKscEH~baU;Mq;nNCUO25a+gXca zW$5i5W5^Q1CpoNPFRH;m^+)(}nPK*K#(4O5M&^RCP)>;FKY^2iMc_ohFk{hl#wf*uGM>gs35G?xE+fB__GL5G8zkSqWOp8jU8 z%>0Y$2Y=l-w<-H3X)0Lkw>fgmPZ$5d_>2?g$3b|eDYCWotDPm@<9RN^qagWWK;@2W z2GUm(Jc?3$8|SNYYp z4U)_&_4{n|2;&qPu{S>~FUu)9+ZujEl^s2`Wf0<0^PJ4FUE}Oz3kt!kGYB3EZR6?T zpDFp*b$&Ao7b#aFAX%Cy$n$LUCgCa*k%rv7Y3I}Z&J}*e)l|oV`knhC`=pCkCvjNU z&Qxv^K2t}X|EIm5+r~-iG`DTj+<~5-@U#=wcEy3JKJM#;ZgbX^%iS*kOckIM0RQ7u zz5n|d4sfcXh4@MUwwNdUhrg75IB(d7Q9oSw zNR1NPZ>^%r%HsTL4Cbj%#X?G)D2{$CzWS4znPM1|9&*-ScDACd^acG)?*B!Ml(uEn_$|T_v zLb6oAc{WbO@S2iB(w>6AUC92;hBOES!_a_Cx;&G}o>(xJNDE({^gc~91>4~O{?{#w;=CHlQmQ|}*oJ3_c zy=2NyOiz!M6Vn%Iu^0)6x#xfOd4M#J(+8lzNg|;j2O9mGeQpm}2m#XjQnmzp)0xr1 zf#SWmoST|Zg)c}U!inKW4ARXH71vQu3W$R~CDUY?1*iJY8JRS%!qi#D({qLQ9KXqu zoYhzc&37kZNwPQ)Y6@X$q{xd+>zpQ=&9HfGOpG+`yxNjO?c*jEY+~$jjv_@f0a<&q z<(N_8D`+X{a{8qz2fKczHgYy%>27$Q%pQsz2A!b#@CYPf6$s*z+`2+wvB=b9GAu^P9PF*=K*5D4JBc1K*sLrC|n;>DtuqPgMnQ zIC32wuw@y3imRljilQyLWX%z$QBj1pmYA!x{7McyWU_ZHGa{UmdhEq&*0snEYH4~g zO8`NIy?zzv!WIdGZO%rb6Gslnh*dND1?;nj6!_bh7;2oCpP=}X?zIc(KO=nQf;bX* zETmXT6uIxuvrs@TDEBlGps>hL;c%vkC=z@TH1G;4#L%t^U?^{^Zyc1Lb#^nz0Og!L z_f-N78LGk%8Tm8$dm@0~oOr75PWf2eTCUO@=p?Gu`-@8T+$sWIyGT0E;#pQ)stiql?Xg%vjZj|AC&1JA`J4$TP zKAi;`pC}BZH5MaEmxhKKjIB7B=H5BJ$*2j!5lGaT(y)%4l$659*xd!C51Eo%PF5Lw zQ1m-qoiG=3#S@$z6hTQ!j7XTI8c<9Bdq4qW5DlhqAGs7 zU&JSSa{jdvSJ@NbX?Na+mGZUt=Y%y}zoK_Y!BDk!X&Rx2JtU6_MN?L@Za3=PnC@bp zE3c-zRx4^!F$X7R{-fIdy(-a%0gD1@N+bW{X?h5a{ksTF#6G=)pOtnQB%;QY{ht_# z&levP(0)OPuc|07n@4B3UN~`q#MaZ*iGqQpgULM%jsOm*h2VM9EHFrbSa$wtS}zr(jqz0i_3pP`hG> z6RGYEBRF7|i8T3ToostmAQ`r4iNz`uUDbHQ#q`dF*k#VvOq+iU`;wheLo`3<>AO*L zZ()|Lq+XX(|7gIoYTzPP(S~zaR>8suDnIMM&p<;Ah#*fRsEn;}(T3=Wd1Pol_Tcwe zpY|@>@Q(EAw)={QdBPqh4(;(U(nB2SI!hY3MN<>HRjhHi`9|-}wd-7ciwiuCngW!*2?F78NPuU#9ujA@tb|j1^~h z4^x^*r`yFET%Pp|nq@PdeDjjBvDtLulR&2>J7@6;F}n9O@21)7t%tz@@h0qw4^vwN~GP_M*B5D^p{rO48D zv}Em0=<_%lj@7G;Jxx!jG5%1;+y(;O1XT4ORp#&M5oJ)!U-kdj-|@^A_znJ`aznxW zIKwjKF(@AK@&B|u@SCxF*Ma(k_5P-LN59F~rDe8ZOa@H^v7sPiIvrI={gbSmGX{8pQs;A*Ag{l1&N=$jgl9T1trHiz6ek(Hs>>)Ue2d`$Wqt5Kwy1;My8@ z^Dv6uWbLw~hJ+!dv77&J#Lx?;GkLpr;n6Lw=HRD3862Rh&##}}*-mnn7GL(4{w^QK|^rvK*{23(d(08}PGD(*k?A8?*{=X4py!US>( zd3@dh^xXrF2B3c3S_0>GABv;zPWQpvrD?P&@}U%*W55$HiVJ`bLAI;RHTerA+#SyX!|tBNYnH*(pW#%tQ&stCXdXp>l#$6 zccg)pjkIUoW$I36f=!AJ?Mf*kJ;$uPN^ ze(R7JoH=mp4xQ2bfCI9SH=!0e)vH;%o{4tZNDh(V~Y8u2s1ecFk zleqdWM0t(*c9rniPPn*|I`}1La>0YulddZ$@=LCo^5%oI!M(rr)5)dD{3Qhwno=xa zqVRygG}lVTu8w2UjEM$BnFWylc+w`FUKU(7m)Iho9}Q)YNvt%HlI3J6mxRKyV+R6% zGBA-GmL?s?vQLCWSBMP`5p@m0L12lCte}Zl0JC1o*AQVZWF%WiVFc^0oEDzR9U1{u zX6$c1Xv?M|dD7tcQ!cVl$q>h6$T%TVa7986^?vIL$J<(Wt$nR6gnN!w!KfZ5{PSw5 zfN)jyvgHTW8fa=spOEqjlxctYJKdU8M>=+*AqwQtWL01j45piA4ue^bhtR)&0R-a)Ar}-EdK;-{X~%Rkf#`!tfN@yY?F}u#pq!pu+@%o2 z2*VkeIgx zQ})T3rS()4chSPITH8vFMTSUIiZd-X(|oWW3u?Ld>i%P;%5h}ksyo9~sRsTL=_fHk zBX|qtHXMIqRTIL}=7{4a%giw4dSQqo?xDuAcGD&fD22E%^k>l=@^$SvvnWIjmn)A5 zkbyLNW#hpBR&8n=quyk{up}s&e7aQ*pQErfm0e>UFG7ow_k=+WC4M$5cH3%E16%t^ zkm&cXdP6J&G+rhq6uxO@qO3xeo`8bo%?|o^lcTOUAVQs}7U(`*8rh7u=!0VdUlI*d z$o)Oq)7>;jB1@$lR!43RJ_tou4u-QbF$E@|2SD`4UpD{WQw(-olp*! z7eoXZ84-k^Pl!A&LYS?Jg|9d-{vG&J4a%bLd>`DX+wPea?h;?79MtW=E)qf(Mh|Q5*ENA8`KXOYfJ_+(kOyyhXFkqa5#_ zU%_&T6Juy64I3BqYT0l^o@S`?F+8^@s%7nkP+|S|Z4wMg`*MPY1)blpP8x!vsJU7Y zrP@px?GzNJ7gx45WAVGMzeT$1f5og3lE9VEuE~`7!lcWDTzyrI(l(&GO<_lFx{DYb zzxaWkzF7`kU=ZNF*=LVe-JsSna4#pTb4l4Dw0+aye{x3(20&q_)wruQ%Q}3M8k$By zef+zwtlaoLRd_tVrMTDotAj{x%Ol$1GWx=hpKi#G&^mz;_YM!HtG+7(y3zdAus<~@ zcjHYq3w?I>cT54>Kz~`>nSNR){mAVuttE{8jBR)ML1KXo zp8eBo#nA%(`}7jZz8{#BcgyFj=+_|q2ioWd)7=ncA0rflJU3cO5$21ZT$*j=TS`3w zz0IongpOe5uNo>CGu2@Qa(Q_e0y0Y^9!EyZnBRx++?MdvB@_$vvnpa;W)<|n%A(M& zUI#mxlKE?H#|5$rtFfSIWDEX#Lyl(M*^Robf^l^~)ZCLkK?HEaW@Nc7?l1 zcNguzo?~Y%qiF1YVyfUdHfO#VN!st#hnlnj2SW@o*oj-Q*9?_#0F4b1>4e4IoT-NO zIJU!_o1n-osL_4jp`QpP0VfU(>;f0swS{hRO4_zN>=0*o;Ap?m+|~qg>G`kU@0no1 zKw*qYWdUjDkpLLYPrAPyPl8Mo6)r4AL86=hjYcPga^hP^{&4ovmiQ&;cN0PROmPDq zJJBSBlof{@XP`*1&8VkDO1a!&uW)&FokspyRK2FeI3Qq<-$qF_frWM1`z+AsyIsaB z(8x(BY0leg5$AD_dQyH{G(_v)ogevPd)P4fL$f5BMKiLD22Fhl8tn@1MA3c@kDfyp zN-kWCSOxaHK7ylg9=L3mh6E&pn~D$yDdAQ-0d{yHc>njPRgE29U(!UdG7s<956?N1`GR+4m?Ra;Ip=N4aQULY6F>n2qLLzD%e)AKsWvAon8JVv*;oX{w8U z@w8-|6>@!78R#K~vjV|oPn5~X+sT{pQ16cZ;VOQO80YJnZIN@t^usY5rman!LaFr~ zh++E4cQVx~5?cjF*2Ne{lyZ&HFy|vD<_kEWNN0ZoH{n>67@*$eOU3zcX1sa)t74VU zE&->|gZIniS0}dr9+?;bZI2BGCg`t9L=N?40}vwL{<>9hmvwya4$pg)tcn~?M(?i7 zQD0*}NW%Rx>R-4~-e&j5#uE<`kRut3F3dIn38Le$#UuN^bn&-e&%}~`=-uZKBJ=v? zJxp`_zbvcf8~{o&B+<{!-$<^H^*6K-Ytv1OJP~41S?u@Zhp-N`8G2$Uv+3;R33N6o z@nJ#8+Q!Dzs8X4`B?&>+H~uEmIhh?3a(jB##6P6XD3{OTAgX zZ9!rKgx2mk;n$D%m9~9Wu=?eplj=icEaZQbt{YM9Xst(lhvD1q56(D^0owWQ8@hAl9|NGWU$~H>}D>FpJaFp50dsdYaBK~z=Tke zOvMCyXks3cE8SDN+F^x`hhM#J*Cz% z<<&>j_|rUqZ~p1Mf!}mGY4SyhEsKqD##5@JnLDkW07?WG4obu+omSgYynR0Fwx18v zrO>jyZDF>zs%v<+@V2-$YgnZ01=sjnF|5({Q2qyZS-Zl~Z}|;%jb;?uEFeVZ{{+V> zLn;$!Bw%5ze^GinfOPr-bFY~4il++1`FN#TEQlK`1R=u^-0$nRX@kJ z2@WzC5Su3Yn+z=u`PDr*CkpJ__A`UzXN5qm10l-mFz{~$rCU>IJ{03J7H!c)p;KM2 zUM|~U?`?JX2fXE3Y?|I+!96LRxW+3Gs>vXF zwMZzjG;GQ2Lj*`GYjOyFcTINf^+98%dwQT@reR!xJSK5?+3d%qEK07%qV0m*x%sLv z)~`zjA3JfO4)E! z_IX7n$M0uN&OyN&JnFe`Y0th83m$AfFcT#KSURX8gf23}@J_ygPq$U!*Ao3J2jz3& z(HsE5k`y&Dnxv{^rnn@s3p&0QDt*?OS+vy<;ayY>GljxmQu;F$-Hemh+E|hBwB65&HfzjW;pk z7r`SsYmB0?G&NU@KXtbTd6uRqgnTqdNP3QMKFw*bEtw;VY)#FAV{Q5Rs6#pphHx7k z#&=VSw;Mt$!^6r#HW#@0u`zVF}FU&_Ba7|Hz^z<4e?t`j?}y&S^{D~#d)!kf?jrw&ZwK7 zW_6T0fSC9G{7BIYzMp)64kk~kU@`aS?SWga)lW*^AnTkK;Mthl2y8A?LC2eMMNsNJ zir+X5!9mIoNF$}u{3vp8&g&_nH4JLE&{$v<)Cek=mnbxUux5aiiGPA)5!dC;hm+Z5 zvucyWHL z+{M?eT5#5OD_e4VS!&8o-_7qV(KZ&0`>XT*OUK56Lcse8Ozcnd??mN4Q%AnHDcRgCTHSVEc7~~^?V^OdDEc9{RH`J#_&{8SHLoZVY8E?NCip(B&36J% z>7NUv1|B!r%E}4c&t^0sMD{}!y6W1n^%hRSG5cH{l@_H5m86TjFIC1)LHM|9U3+s{E?v*% z#ESn{z*VHm5Afkl;?&vd58I5bVU7YKHDk6mlP5ODW&YIviaRLye?gf4lbu>1S(j zyUc2c3ii27e#WoWS~vx~?|ZX|7BsAADw%`WCnuNb6Tc?e>P*KOwM9~Ys|{roNXAH? zQer1j5BKe+gTcxVYId7%6xOMSI4oyKf!XOg%Ud_z=B zioGYZ5)J*bI}}ZsnD*@B8As?C%cs{Or=?m5L;hk$jfyyv!DO5UP`%qO9|-W=rrCy+ zlPQgRUY@QKI`Z}S15Y$=SIjiC!nZbXRD${5vTlXYxcFA7v?mN%4T|xbnij+&TMx~sCXij!T64!&aA#Y zZNj+$H@VkEjh1k>(UY_N>5fCP{>uIA0<^!_&Aw8qkDjJ}g# z-@qrv??ra-?lL@D+^hi*Z2vPSC~5jR+_*feQ}g(TNjr^3Rp&SAtOUpBDRAwkoViZe zGH%7z28m}Cmwvuvfxa~xPpzUFk;2)_Zxtjq9!Yb{DeLly3XFN)zWV^L1BRF@ntrkH0`O6Ra_y1_V44|ns51s(%J}f^S^3MkCdi9 zsnd+c@q0)p4zcooyy60d$hMYmL%-f-^M{iTX++TnWuDMHI+b-z+~r7Ycku4?w|tIh zt6*7fxcj}%fg~g0rc@kcY(-$1I;<|Y2a%;A7e-t!B0k#1B`&|eJ#YL4KpL$={Z;jM z327PEDCNC|EO67=d4MH28ly1a_i%y~Vr8Ya{B8=JhtH)dKfu5bo1XZOyUHhxK;~k? zD@FYj0KJQn4lS*8xXg=+Nf8_w{=9u<+u{^VpO|tCRuR}b++(G2B80v>cAAR}Amln; zDmGji*6hV@*o9+-tw^F5$utE3ByII6ToG}@>uZnF@eQ;iMQh$06J=pp_;Cr`z13Ji3lCBxpZ}#SnR#=!LLnW|DR5=T&^SbJPM;m{I zfAC;H9QV*OLGGOTy*zb8Q2zUl(*~RniqL23^&Q-?a@Rx!c}2e?uoCqvKHs6+DK?f;9IL_|b*_eyi4UVjM*YEw6HXH~Z8w*L-a`dVFnft7epn_!ZtarWQNDJ^|6Fd)qZ1()}j1=b&;LA9rrQhY9xQ?uu@IcCeC=dPI zl7ABRy=S_c?6c+$gQGmdOXbGhrALQlZcPTh@h!)aigwwJvdMuY=?;I-9zy{CuZj*- z>I(e3<;Dw~bx)|Hh9Yrt8Thm7Xg)IaqYH;FF!gR$^d@kfET{%Xu9aZ$XmV(C#R@Dx zgw!=nJwbOh#bkc_;;(@yNig^`*CBp#2s~afv=Hi7KAZ#*8pVCV@4vP~=PEQB)p4?3 zf0gr{{eKo(f3uz;EC?Lr8Hb7h<=ynxq_S5D6#iK87}@VcgvICbtKwxgO=dPEkC;Po zA4Z>_hp&3+U6AQ0V2!_)Cd30r$yrfx-j+kmcJojqef`5?t_$8fg1k#kqCOD`Xg)ze zorKS@Jsv=48a*#9x|z}cwiHs^@X+`OIl5e{OqlR+Ipsh*B8M*Hk#E%G9a1UyI^?Qi zuNBQT*A+`|4uA*_w#F9Boq&I-SYf9mcd*S@pc(8$As87-Li^g2jzOc7r%d7cJ1t)_ zK#5jA8P8$7+&LG5G;4-9Iu6j>Q^qHYQlIE8XP1(f7 zBbrL}rN;yUtEu4cHu;&nP*kHsDoXgXfa_&uKKn9jde?Va`EJ$~ zis&+-HKPdpAapqcT6qM4!CxcS)dt$l^@WfJ|bm(hGFF*l2a0+NQu75|rN_F)1Xwuoe7ro6<#=c*Qx%HbO8%)8LxBdzL#Y zi8wDT@flmPY!2q00M|UAJPD3dHmDbdzykn_o2MHgfMIOVaz-BFaZ^)#3#NR#I7>st zdHllvH$!3sk=Yr-g29RXWB^^uKD94GST?yJcmRMH2NVY*|E5(&e%L}U7Js_QL-C;a zD3$P`T5}eavny6Hk;!z|)@VT6lpy}?8_U*90^3ZQ?QWbC#Ser`fL32(Vwz>)t#&+h7oDtOe5aZp zWs>#j7^KojJF}J*J^~qa8WekixzJa{#K5SDc-HD~q(;*Ol9P@w&sehNgAkRGdi3|{ z9-iZnM$mk)cR-aGSpC!uo)9nkDb=el6va`umaOEsZO?KYYS2@;>$h=T_?OjVy$n;I zpBwdRnePm)PkuI8ZWUeVF>y|^5ANA-6)d>9QN!=wF? zJ^%}ak{|NvQxGuKE8B5q9*g(R=$*m95_Q83`Y2Ym>>Ry7=Ldf08*P6SncrWwoiIoh z5@P^ZiwHt=b%o59KJh#CfD#>3!R{mU{5(LWx7Al$F~$p%>iXzK0`P&=d`m=e`!TV| z1s~(EeeAf1RJLT=lQ?nb$ltyZmGtV|!j#qUdhCaV#VQ;CVmW(;*Dqp8(j)!^c)u`&Gh;l2NIA<>#yn z{sEIirM2SJR&v5IJ5YXgSX0ykk`1a8I|sh3fipF`OnVIIn5el_mz*y zSmOq0C3!23A>dHPIR*W(%p8~kOm13H;XC4|A_~eyeSjZRUNu|uFhrO;>FOgYAYNUm zd!uQ3x|iZR3#_t3o=fJ}Jm?C;@K1t+RgExl@DAqD|i#Y6ywamheG zGyk9%mIr^LJl{p)&?nLEbQY8bLiKj;&2e&WE!t~mnS+%XKo1>B?1_&H$$j?|QQU`g z%H>b8N0m!?*~!Q9Z^ZfN+#_IwOE*NJx6v^zJTa&P$D^GylI^4x_`heyY*}n**i{Fw zuta-5i}10Wj1kAf%0uV3j*{-=x?Ls^;-CZDa@Efgq@0{6GJqrxTJZ^`Jj!#o>T~tZp+Um0w(5n%tK)S}F50?bT;n8Fm0;X8Lr)q$ zs=Bg9V5#HJ>8qZJ_yj&>__-ByVw(9Lo|}AZxx`pFRnzQ@9-Q?J4K)U3 zG(f=X8YC}Wk4~=bTc?-o;1~pb%6BGqGz9u$3~pLx-Q;92C5F3B_hROYP>?GMWw1=< zkbUe`L}PQes&M+TjTAD^tj2hhK=kw^$z2wU}3I0$l5w`xj? zI72oU0s9V~Aq+)(&al49@Kk}j!j!m2!^P*+_DKi9fSiKfNJS)MAH6RhA0ZV|geart zxf!dv|5`(Kki#dqQ2~ZbP7x7a_sU(5n>#42!NQ=VAV$2F6&uD}JEABD`Zr`__E?5n&p#?b1FW6n#t{ z6-Mwr`O$oUf^sC^UC-R!zjpZDOQ(sbYN6KF&Bg#oZr8LHY&ZCXOz~Zx$zOGE!lIx3 z&YI#grLj{WT#1wP$a0<)G4 zZs=rkmPMFf880H^ih*0s+Ru=jM!s6#1+(*${7D9k&cz=nwyAa~X=4Mbz;qUPF%toy zAIY0zby{jDcz4{|5_@Z4x;LomDG5b~l|=2FR~iX|bTMK%JOn_ul@VE|0#@_j(vusa zylqi;azcfu`mmr0eeFOh1^6=V6R}f0x=i52uGEYppQ@d!$|GZ^6B!B19)$li0`k&p z^NRYHS33)NUDL+1vbdj_W(!6e&UU%DWzyG;7K3g)~Yq1{Y92u4B)ijIWDKM)lwW?=P zDHz73PaEk~saQ@lsGuH2j+Lcy$BJEG(Pd%OW*Z5?8aIZBwv`(7XUB!@iLltF5%aqr zC3T;l*9la`#R8_!8a`lFQEp4o!>ZdJG#TN;w6&Ih<}okhvVVG`KWWmXL0}u7$A7Vq zc{Xzg-d3AzIIm^sAnUnZpH`nH5uNHSFLA+WTajkPEqBkyy0R@}yr_#PDYdmd#ioCK z<>GcbFuh&FD9pG)o$vJ%R?laH?7fjC31MOtCE=cdGR#Ayy--1=k(5b7wlJYvG1%nl z3l((Wt9GpRHLk$+to|Q}P7x7im^&|aTL2=hU(M)jgF^kO#IrJ&HmGwYY1nX{go5( zCEfd(5Sj1CH9#Y<4D>tzGj%-J9${KEb!D#d;+#`HFWfIa%TjHuClwFATFYa%3-w8| z2{m)#>f>g+vsIA_tCpOWg>4nA9kxyQmyjJ*6D=saN&Xb^9>wH&Ay?9#N!g-3_VCYMe%)6RPPZb6!Rb%ddB5JBi+gvq4p+|&M!(2n@F1qxxT ziL3SUjWPFw3;S5ckRj)cT^-`%?&}=7hZ7se|G(EzAgEgey@p_Y5`D@*3IDH~i5&w5 zk_Vtkg%gu&P^HrkcrtmCJ&fPAiM?6)W;hZ>Dtr-k&Zd+)B`uEUbE>G?*#>)YKx!U< zAcFMmswFyud(#PfHM?DeENv>Ng9ufwbCs8nnbu<(-ng0jjGoq*16_Bh^TT9n`zNy+ zeZ#HBbg*&c6>-H3ZBWZq0lJGDlS~HG^2ASBRh7Ph<(r{v$8+W`y_edi*X2pK+tP+{ zVA2;FG-c$MB|n@fFY&w8j#ENqzH!k ziv}V&^wovvqjO6=Tm|+FnDaP@^Ye?$4l+z@j|Bbz@9ayxt`0f@tRl%{AZ0Kj86(NC|){zgi=jZf06sQtjvY@7~Ezfi;&jT|Ae*g z4gm_X`nsqHZAZSR4*1x3!2j8h7#P9-nZQR8hM@zC!43xJyd?p6VS8;E{m9n=F*9HB zA1xyfSL#;C{hsWWy2(WF)G;%W^-eb)YFzbBb61GS!o9qU~Dq_vfyPGP!5tY^$^{SV}R;0_N2uajP1Bn`~*kEO}QbY1*TJWmGj-pYXlx>Q|HL^^SI=xGg^z zyEXHUR*q+DYA7+gZ;Vli^Dus|c3c{`6=yHjLT>vy7dfhSFwk2DpDMX%0`&rW{y{Wi z;xY;MfYgNfE#ct1acf?%5`tFh)Ee+pEF2tc4#jbmI*KJ(SE;%na-c|9US(cI(=7lE zbH>5Wt#(ELZ}I6Zt5M;>N2~SD$UJ)+l`!>xP}koLU(EOjaAr9QAM@uO(3?mN6n$_& zR2+;rCHVjvM5Q_U5E!ZM#|l(ev9BbA@8Qh5Y#~Ut`Ed9EZ;moGRs|^6i=rJ_U^|0; zM7TzE+x#2?_+VJNL8Z7q8dK9Go^Hrvk0Hj}!ki``PzM)0I>LW{EV>THm(hH(JSzJx zW2ahOA#i9}XErFs3*=d_ zvqW%=&(>B-0%2n5<0FoVkeMR!g(<>fI^N}@9+&O((Gm8^@pg?v5PWv(uuX@#pr{NbH=a@!qIay?f8r>c`H9j*UG`)dy`g`n+rL zgWskS79~?+)>UN9qBkBD}%&Tj|Dqz`Heq?XJ%asn=3ERk~>8wHRxy zzzk`iNMl~8YCNKV7OQhdwqJ>3D&(|-_tL|gn|06snFtGWt75`~gDg>G(Ep=d`~!49 zMZBnuNSiy3jru*>3Wx|MoCNsl`DXev-8_8Yk%IQ?&=z(3Ym|N-yCst2S*x}G82bPfse4E+Z@pYa2gma^bcds$Hyt*W$%F=LS zqR2VVF<Eo1Sc!Pn{P20@0H^(5rFp_5>%4{+1r92* zm-G>ftTJibVp-~VPvSGx;NLAejwd4anYIDW0seqV70x)~CXF8D-{=JGn#Rhk*7D|7 z#ua7N3hz8G?iwY!D+Y|GB?B*Db4yuo-pz&z@;CjJ{RM__J(hGfM|#7N+Tt1$B!-{E zWv7iy96H}!H=ZR7XEcAKwLyewy%IGP2LfD8@SrjqYSs!fIGZ-c8DCV2qxM(9QCK0` z>xUBN4tk;8Z+zPB%!;b{Xcc(xl@rzO-h4s3NrdmY|FaUVCYJC(h4Kfj z!_czagZS!aZ^Jc)psbGt0>#M5(ZH{3pRzqEA<#Cx#KPw(-^( z^Zj+(l)_ya{7|$5B1n)s_f&}7gaWUK?gV@$VBbmGHqd80;4_GC(YeE*K)<}@@k8s6 z&&c4FT(`BVH5%I|VIQ)J3IPHV_O&OZ&Ya=*^ICNATV62?_&e0=XSL?2E|F{o(QsAByQ_870D9b4OP$Je( zlhg~)A#{~sPiYi9dqlM&EMl6^L((dl` z+ReN%t1Qtor@r$IY_i--I;MyBnXLwaRPeSyas0q=Q=0&u`rc8FxvLm8z9JCN1=-#mBSoyT;qn$^&4A=UKX_03QMA zz{azNYH)unqa4u)h>=%rT&WfpjkS9eY+|Dtut1kyur<3)0MwLNwM18hGoxWKO`#Nt zwWF2#CNeNLqOB_{LvALSf`^5!w2$a`7GYF4RfiuykY)o%O4@_>y}F!8SXJg~l+qu% z(~ft|{4(L%>ByGJI}s5nh}&$$Ca3z3eBVp6-r7O>gigZ_7gctBO5sT70e_g~r)?c%8q?D_ z)LHz-hn(?+Y^lHzzrTX0)nFQq#1OqA)JX=GiftceyFH*I5t@q)G>`Wof0hYT9<;}= z*mI}p-hQXrn>tfcXfD6BA7I5BLEImRhs9+h{Dlq6>RlHO5==^g^Q}c005$ih$F@b! zn84~MdvtD3BOOe5z4D)*YPuWJdF#AIQrJ)(~m0sm5aQq;6wg~ufI2~T^78Xr5C(?=T|w@$)DDe zS1UX*&z5WJ-hIPoJt#e~-rf~kHNQM$ZLF4+)R&w#cWdTzV(%n(ZP!Zd4A}I1o<l>>FV+R^-FPaju-5>cHAo8bWM12q7V_)fT2Hxq zc`qyM;_#4!m}5T>PC#YZrFT*b9X(1LlKlC|9kXSy7Kd^d^qvsrL;VajI@_vuXBm^y z!S!8(G}m7%(9?YiDfiaKXkpJvwb2{HnL+O!*Sd}*iq+_#ZncRGHcK98!7?Ary)G9z zsMG`9r2>t-ZagPGTBoZSfTm!n{~QT^L^}c}+>31LT-vSCqdz>ohs`B+ZI8;ws(2t7w`(55% zWGMU;!m&7;GZX$jg`l9m^QZM<26qc?*=vuBeZCa3E8s&@Zvj zR|=A=i_uW2lx0{+8bwLz&VT!j=z=xF9aysW1pqZ)5*Rn^T0o1|hI!KkvQ+2=oNq&7f zYs8B~Pe?+>F&0Bbv`}n5B10-Lhn3vv#AUCkL?&e^OX+V(1Y3X8|F zN1YLUuZFUO5{gd~o9-$(Y`4?67h0vt6K^TBC z)8X|Z_OE~)=!6Qf|C$P9>)CwbK!Q%ml@I|^wm;YyR|D{L=JQ*W*QD`mFzh3ge{xC# zF1pZi!^o)c09Hl}WD*ydmeyC<#%!Yu&`X6t>a-qrrmBw)EoAnWG?6Rx?q5c|HtwA= z7$k)-Up-{z>C_`v)6ugkcfESug5vTFn&*fCddy(KLj0Nz5=fVIseP@Y0-R*fHV~F! zG+MYJtC@-F6!q1NJoERi)C&ghEx=Q8W#1m+?|y+3ia;KNOD=4GQ4#ztoTYrG;5wSkf-M1E~Sm05bvt)N?mIWp z)P0H7anwhc=pHNqEw%(pJBiJarz2$q8f7q^A_Yg#hB%G!B8xc0b~i?0b|A%_p*U2E zC7h0Be0h2Ps7!7XEb@)hb_ODR3hN*l=`G@*8+j*U5F|K@ED0~vMKOp1X2i6u*D#+q zL+d~v(WEZ1+{O8&h)a2|riI$pYMEmy=gG|AbGDt|c-@$e$e3sS?OB)6jOgXk{&VOY zL3lf(Lk|59;lk?9D!g8}YDVmGG8R~bno0Nmv{S`T=yrVI)Am3IpjeJnVqwRz{uQQ@ z`!p(K@x$G|QRmS;k{L2N7a`@>FN&(D;6c-;nhl6PZF}o*ssR+c9Wo()tk}vBybUzn z6{9l1`6+k0=)M{Ewp-xPNG$jU@&782A+;8PERs}fkTpJ?a5u0!WWm=I2KQ&Ik+HkRloae+JF5!n-ur-CUhFVy8MYJ^(}2s15J z<@l~*n`Y}qJ(N!gYR#bkd7N*|Z;EpwwTq_p>{Hh@?=XdX?$uw&t_|1v0YBME z>jyW%RfC6R?ZJ*gM2lh-ihuxRE2icbw23<*C$mbY4!>J;X><4E-Ap?jzYw1(tVx0B)NB6!ps_b2fI^y^?fGC^RYIa_b0O@ zl~E2C@9uzGC4Hf&Gle?kg1=ah{M!M^wWuYMR(Tft#~|Bn%HW%&05Vn=`n>*+M~wHPYbUWyp8m7MnOlpn;#qH#W(tYY7@7*0gL@X4#x2KJzy>JgTih z_$|vFQd?lMmIGhU62b0dFbv|wC+unS=qwSYm1D#yzZ5HDDws3GV~zInrTKglM@;OC z4KXB>14=jHn=ka0gyBID45VRUlA+x~WU0Myh^}kZel7USz9xfLN`nbe(?#5omm6s2 zbZa7|SEUBqh5#GCgcam?YKWSx0zq9#N6FdYrY%oZFv9nf_cK+pYW%$}U5a1cZ$GR){xM zqrZ+L_RhMSt}7kT_l_1u6;ZvRyapt8Z-yAVUVeQ;)U_ZC`tsQjybBcSe{3TVp^@Wn z+oD8_feH;2T>HD&`NtH^NH-*}%Gh6-~;_havWM>ycpOgXQAXTyn!FyT_4* z)buMj9Z_LGl-y7oBt2q^)jZ%8<#mbbz3(EL`~$BgL#Ol!(Y>!Lh<%bq37y9zc5LV9 z^Fs#j$FcSuUN^$%D$RU#1J?|m5sc2o=jNPliOz{E4jo0yh-rEVZdH+5aglrA>xv7x z46v;NaHE5WL&$BM$Y&09{3=X*tp|1R6-ck1OeS7rz|n-*lpN%!lV(@slN-O*6#sfU z)ymsCi0sd-zK;b!GTU#!aS`X%uG(<)?T(;Iw&pmfgt?cQkhh8Z+)7p z0L;P{DU*`jOPdq@;z$57z;?Rxp3_n<1@YUaRg=|=i^=|Pr;EEL^!&Owmi9%B;r;@e z^oFa~@)D}C31tZ+&4TESiC4z(AeD>iIZ9Ju6z2c(yb3fpVhM=^Sz4``EupNu_<`aJ z79J%vM4AUMge}|8#}a7=$W}aMrmQLMWh%b0ZT0-j*{Nw)d%ZCP6)Q`8*H%~%KKSk-W)FC?-@N-`9bH362t z*8t^xGfkDmX>(GZkh^>ek&g7Kr#XV^lgopY=o%|s*|j-a-~IQ_u+wQkrX=yXCiKs;|6 z2M$(`6Ua%Q}pWI`gAp~K4C8V7==s^6=v64!-GF0xkTmEGpwA6;Hv~^j8A1* z7hKbmS{*#RBV?%9xx^N1`Csb)C3_(#qclx{%nVzqPC%kOYvpecQ&2xEobhhMfr`nV zXE{@Lr@6APu|$)wDa$nsE9?4rq9mieJg~V*Tc+IU<03&ZTGwertp;@dJf&LDJwu|iuA|N{b3%a&fJ&o=_2|^jZgigs9wFVvO zaJc;_Bz~o{3g9o6o>BV<^;))k$sBcp2P<*R!qh^H7W6n3_*b|cpVi2_jGpX~fCLJ+ zC}qT76go2@y9OSw!^sPd77gOxFb;lZMrelhMzG{{lN3^RY2}H4ccd=C&~SaP!^;7C z=l*DmF_2$rlrRBF;-gVA{JuC&Ozw0wTC0@~GOboJV`+sq8VHbLQ^p?FApIr;bj&Bi z?i4_c3xf_E6aJ5dTa9{83WBw`?mrt4rQ{g?O=URLLKzhaWk6vp!kq@ld2HVo683fK zeRM!*Xy^5L#9RADe5L=!pxv|bmGP-MDb@FyWK<9w^{M?WEbQZl&eJ>EbMxUTSJ+8e z_YU8OppiQ>M+;Y|#RJSy3!9+FJ`bMsM|Mrs(i`I)7ME^k$jd{;^?~k%)&ZGvh|_$s zs28bC9PNR4cnb@BhC6gu#f4E?`=NC*U2t=ot@B!C5-a%@Foc?1g`ss5I;MOy9oy*Qd)BJ# zK~ldd=BWOu`HefzkE~=>Ek!>}FTO@r_j%lddKcRZE;YQw9<=^FsSDCO$K{3%(g25g z_M%n4B7HGyn@rdaj5>Z2v>PXHkr>wqQIi^uRzbd^Q928Bqrf_QeW{*rC+iJ%Q*4!H zFT0`I_zy+3=t>YxW_-Vz&!f$wZtnt@P^>l zMKY;T;lq6FU&h~ZD*v8};G1bk2>Y1K=TrzoHM9|zDGJn=gjeCPkJA1JD9J0hh`P{oxVZhIPc38h6e|xJfTIyoF~TaOd6ruO~cy92^Qx4gS`a+9ODcCtWxP6 zq2H}N-AQAjui?k8*Iv^aMs$XrP55@9szlh7e))p*SXqoJ>p@lNu!-*NmqAQ+pu=w! zjKptRCGd$zaB*<-SgLOE`qL2+*&@fw7W5o35V#IZ#3RgQ%c6k=LP6u{QK=MPM1r`n z74}8isnXm_E{U_o8W<2CuZf$W!hs?r-7Z}Q)#6^wLh1wpG1yif(>DAmaT~f1I*KS} z85zm^r`dUP(T>|mD)mSNVZ}5GCb=^;Qso4LDwGVlXNJkCArbe5--Z&_*Bm&pV9cj? zb4}={Gq>iNARbh+HXNjSZDwmcU|#*cD~X(f8+A1OjEAxuF0Oio#bC)Iuy95pOz(s-5`;-(0|Ax(eEdE3?kQl<6RDIFp8MR0on1b z?INmIeWB_rA!Uyk$bJ#>g)!;oZNtdP-4W4xVa0Oyk(@Q5eFfP!>a9NelpWQcd2E6G zjM&uihnUW7Xi8KchB)c#lz-_ z`X;IBHAT6On9uH~+v8u|(B=hRYyMOe#o)v(C;WQrZt^Bisp}tf;Xr}>lY(2F(}b|D z`QpiS_kr$Cp@{6XyNF#}?QNr_AVhX!Qe=<25HiUo1bB6GtVOWZrKk^;p)bBbYO{DK zYU5q6uGT$a(szbzbk2^HC{;YsOYnNJ+P}N*)jjjhSsAOF4)UMol?s^;)XZHTh_exB zhkd>fhY|5GUN_vIIqqZ{j4HXhP^L6?7XC^>Sq2Bx%R`342QpJwS|6mf(&E`B9~jJ1 zpZ(tas(ndpX$^KB`)}un>pwO|C^&Hx2w+qMtpog(wF3u0#X*Uav?mG2CLqs=lIPgO zD_+v=#2cK6{sDI5|4Duh#Xt*SRg#c^mu6qGx_TCn`%$v{g~-a+&pz}v?7fj(5dAsi zY*O`V2Cau2XbdpyZw9E88MT@8`B<5Ri)#;?p%z=Ptq>C*l+i5+L)YMIGacCt>MQ86VoC2K2O|f60)( z5-PWN4m#0+h>SxkswMtiC-6;$e4BJg9EK7-iHP;j=Hi@A<85nWa2_EF%77tYSyvutyDRlyx2A9 zK;!R}B5HulM*edBv~2Jnd-Y9y3RQ;G?O-!$dKc&*IO2S5k2tvVU^X(Ak)4MB9$?4~(m4OG96cHB%Xr1)%8?OR6?lk%D7*19J=b zdEw*qg}rLD)w>rd@j7jEsvn^Szz9J=*S{Li2&(a)e}HPdusJF^R83Y+rO(tlYY*t= z#Tk%Mk&(tCi0c7H*le7VE%R$jo=;s0lf0K3EPscAevS!kovI$S$9 zS&+yxz8*s^GTBXBtXU2@DRE))to<2k9ijBAag4Nrq9Xknx}-dThWnTB1l5``eHyPY z&x9LnX@i)J`0P4wF=h&Sav_o0Nz1HNYH3d3m&fWcv%}?+!S>cCL4i@%z3Pi%BINBW4 zCq2Kdi5*RYlMveOVIgG|r_#mFrY7Y!&gIQxFjs8&dU|2z%VswJdDXlvlA7J`IP;sw z2xGg;jR3@5{>9F3l@KW+!VPo(I*AWXjql}x5*>`k7=`b{pBd3#k_-R_>(Ct-B&hab z^E{NS&qH4M!!^Jq|+>WGuau?pYz@(wBseIW~QGA!OSJYT;+gB`b z2TVBnk`XZ$elWemqgavcDW5 zHAs*l9iprV*K3FYI4^_*iX2^Q+-L=7hbE5nXhqbv$S&DI0jc0(hLZCPw`WOqW21V~%Ou^t zN%t$`pURRiWz{*&nvd+`8iNeL-fu+KqX!M<7l~eMY@Tb#RvAzs@6k_LrZ`Ac>_RK^(PGlidnIipZ z#>CTAHcNUr34VT%x(>lxb1aw>SzCRNZ-fL0Io$uP>>5(5qG5rsm!_${53}>XIZxDj zeX8V)Yci)jK43kMexmPBb?^Bk2-0FyyzHQORK66)oMe^%3UB6J0aNU6DM5MtP7ot^ z>rPacGrk}CO!N`qfYO^2N{4Kdc~%Lng~1P%`nU{BNAj!~>7>YEnq}j*qOMRnL z3`S;Xqr8zY*#GNVZ>5qJlKO&H*6#T5a4Mg^rJGnfWD9LqQY;;hdt2)&*q0JQWQ-k9{hT|OM;xt_^OVI_$%Dpn<7u$SU9(A(})tM+MSDzY*& zB0ZyT)P042&^yi-LT-$qkv8=YZ!&m2nN~H14cS)!2k_v7O(9g!Nwp=f;tuqMDjEzL zNn~kgMr`f~O`#@-G70i;3)J}GV0_#Eizo{W9%ew0GLFpKr~a6kyPnUtpm>6lz=rim zZTDz?A&-Cu$qk2E?-&b#Z#jm2{FMAHLNwGBu<-l&M}Ppp8a651_85~yuv9kc6_et+ z`9&q+17LS+MRgl9_1*VV2UWZ<(SvW%-FhZd`GKLE9lmP~mj!{2;O>mhiA_{l*3$cf z-sDU4Ds3~&Ln^?5t{$U(tl=5d7MU+;KPNqOxfCnOD=i~!ky={w*-#9 z6!Du%4C*+`v`%VvYFE~RE-=`mF@kIllW5>5QPU`eGac><33{Ubb$Ju6lX)vjdbMO2 zXX2<(+hnactSF=;4afrTLq){rP@OYaSqm1uwF#d+)S{J=bab*3w;YeAY9du)b_9;} z?Lq{DkC*wT{oGMO&!oRf)KX%&P!J+uvBFqr!Kyc*n}3xt{)!=w<~I8)r8hC;+ZhHc z4w8J@vp}YKM^O>-?yiL+{Cd*iF(PieUK;S#0g5_J8VFyDCfK=0>g(nkNWyd{?lUay zL*}tyGW}RwN@*V}%y{b^e}Ro>`d)h8_Kjl)i$H~m-KiI65o%+|B(wbrO;M@XcCT0F z+ucDTA~nOET8Bz@goqu;;ThxstQE*0N> zU&*RHrG~dVA51$$^{#>x_zky{AQDFfJ zaeE)UPc1_BkANZyB_}G$f{Bv=D_+o(Iaw&eII$M)eg7t$@S$UN@H1!kB5uZ;P7Up= zH1%KIIce->qLitT-7W^S>+g@`QohJjXEi;tRg)v6=*A>ws?LqGjPE<0 zMKvz>-ZPpEcOi4CwhWC=OmKjIHLn-@OIFz!Oa^TuV?$ivuww(%G0}HVdZ-Qa^p!3CT2KMlHNgsEq@vxUrf>j zXYvbuW-A8~oU_GG$FlR8F?SRGT1EUU_?J+bm4c?$lY_o8%@(pb;LsuPn|UXAvf>&n zfUZRivt46okmT2qW z*|j9z)SBRM0AeZ%+Gs9S?>m0NW6|3toUfncvqSt-4Nfe}Xl@}Ygavf!ct_pz zi0df15R?xk+53RPt>jhR@9$-HZto^)lNM@v7p&Ocx1aK^LoPPjZ23>`OnIxarjM=+ zJOs)_ypKj1<*(zpamCU4bb+a1a^*Cr?o7ip13S(Q@o7yd447Y}DFByJKHcHRe{lIu zqgI_06bg-{3W?Xm5t*j~RG>APrOB+cCM!?Al4Br@qG#zrD%K7>7eV(^eqmxZkx{uE z$P^tmXBKzTVupTRHc73ZQSH~|m5t_3RQ*+4*MP-}t@K(hw!l##xI&xV3RR|IiMVTF zZv4s2;l{0)+-v^N-~ntVpA8kY8bOWzb@?^LgPfda*fb~fnDiwcM_J&S+g`RN$I0%= z=pPS8= z*(hlkR8UNxHyT1W_J|wPE0(dZiNSJi%#XLPF;?OgD>8^DV>=x1v|#gOiGkz%;l0l4 z{gpLo?Uu8%3=@W<$SZB=xYUtD8uE*@P(_mJ8ve}sKRXsM$?z$mY=dc!{vS`@7#L^Q zbsf8HGO=yj*2G4WG?Y}@*#y`T5{J^#)*v#-6^Ucj=kb_KS> zeP=?|jNXGnXpof8r&{x#$0{lgP+JCKo@ZE>lZ!G$R;mysV;w`v1S?f^<2u_THtjR! zpBIl(Z&8-l@?G^#iq%r{G_a;cvEsxeh(WW-DJymAN^&iJ>})MQqsq(oDMW_u(i@>rO`vJrQm+bD8j=2_OF)A({YLk8})DR z?k^(wgI}vT(^6g#8=!>HSnAdkija7oLa5rt`pu9;Ra!={@j(X>X_VlO`p)Y^hj;oyKdkTvQH9!ut zeWV8UdMj4Vi$8w4+<1Nc;-VB{J;xF3Ps4Vg#8rW8896~bnkkE3kwQ;#wFVtccT{)6twg3LnXY*94xN}bp zBzbqzJU=2jaGJoXER@Y`%9ga#4M`4(r7lx1Xx0Q3K0>I)gLJ&c=O`EeBhWWyNn!J- zjVasI;f7DigibBygWy=zPTWjcFNl1e4js$_Tm((1i2aq}Hv_$Axe>j1X#+F;Fao)ibj*O*<(79P-W;)DtJ#wlwh@9qypKNEhWbS_$AlOAi5xI ziHm#~c@cinvT9?Y1ZZ_5&%xBwP8$y;CEY~1L?I1W*NvOLut3tWXV&$m?J`Jy2+UBK zCZouZS?m54cztS!^`Iifwp3I|pu&u7>8LOV*ZCI1O*2dK^i}N7f;Tw~%ze%&-GO7F zez6X$f4gC+2&-TRQz{&HRbRBJbTyf1p(O2!?-L!`b+^rpvVDAO-^Uvkb;w}NK%p&h z7F4gjH@&Z$SCGH|^B(=H{SS@!YkzrDa87;jYWUCcg7x5nY_+&7)4aM)@0uc2^jvsgWOxo$W#h6&t6F8sTGN#2BMc2r#jBbtqRORY&&vvktj0u`0-J0 zI3>V7Ed`OoGjP|8N0fG;*nY(9!=Y0DZ>evP%9!M&p|H~R+OgbQr93EEl0;*}g2jor zV{A$ZMojHpUd!5?(<<1WZm+E^6V>eS(=}%GYk^R1ajVO-x2LBH>-W}dE1M(ySBvZ( zjfV4aB2GReq^)Yx&U!D%AA#~kQIt+00&J^!q-Ts~y;#omR_x~iw=dj-c!}XQNdU8k z9QqyQ5YNOv8yICR0fl=i6T7Nizt}Oo>&d%XT3b3FAc$K2!CiZQ@pJ;p$ViCNIlm%e zEx(`tm;vy|b(V~zn)y{R1=_nT3BESX6w{f*tn z{s0JxXLp3!-}#_~_xs=YCH$a#naVsuM{e$~%3$Ky*c5@M> z65gI(=Q<&gXH2*uS8BRYZKh=o!i}naM1)Kb?EIwvNXgM{R)EY*2`#p-Oe)qytF@Ua zA)~67#aL;hTZbM~FXL_9`ZF%DaB`NFA7I2RX>$qORI&Pzp$l0+IP?EIhs33XCLW-&Fxz^Iggj4UNxDEPCB=J{YKL?;MMO`QDHEb>qE^R5)2Eq5CcY$r+?CF+w43Is-rzYJ0I8e_Y5q zK4V;*Jp+}vVbKdJ)Zen>@*7TO5%KMrpv%dsq#PZQw9xLcYIO3Y(acE(_AA*IC31Ox z9!SRU2lnBjE2P{ZlrF=x0Q9nd44XjTpnqo-+mCYPt@(`)`*{}Ke`z*6RK3N~N={EU zGeRfgD!Y0A^5K)elAeo9>!_N?Q7R17(}Sw>=Kk5m4K<;vFfP=b%*&~$=X95b!b6s- ztIeYsFiT*tB-X*c)|C;vckX0Lg>2Z%Cs6w7c^xX(9cZtZI0E(*J(+%+ClK#${s$(C zpzvb#vBWLfZ?7(N|1H&m+&Sf}vyjy?bO6|!e0<`FLc!fPNH*cQnWs9mhoVDycxkLX zVV-TFgV!!YWisj|FHzxc>cimFr`0Y-m@ce#kmg~Axxm$ns(6}!kUv(*Qg&D|v-f7- zHj`cLDWhG&IcS+VM2oI^zM9f8)uMT&n5nx2q+Rme^1sIOlYhi+V4%4P zw`9zDA{rRH*nSa$sZhRRy*Gb}Ir;QMpVw&c$};4Q8b@u8j9Xm2u`m>2o&@VPEwg@~ zw1~x!mqyjthx{t!OD<_K%J%6%mh!g5cw#L`tPsLhy+;<`m<%u~OF+^L-N*ic)X+0J z7PbtFnXK~-;>!OYPF{@xURvPvu*5Y^bWAKg2mFhNg`#F9Cy4}rt5|Et6E(D_hJI#KmB)Y($r3dVk2vW-$67hva+eZDQot!y zg?VPlF4c|NM2>zPHgwQiG;(*KsA>FY;3?^6xum+{?-`q9g-xY$i6O{X$q@L7(ZR*4 zvI2*y1SO-1mYGiJq|;FV%HV^f7+U;kbBg#h3B}J2=CJ@WN{RX+9xL&@nBx5Mr*y*h zQ5{(`{05!SdW*hKSJ?7YToxxjcoNSiK1gNN`PITGc87jApL3zR^|2?ALNl?f_kQqv z63;|J|C4{@B@4^;jAG0?=lWXJE4?`B;OTNhui%v5a2n(r2^T);^;U4E;dKko^B=AY z0k#Lk>4TvWvHoj20yfnF|1-GGG3O!jdpZ#-ZCC1p9|xNdUzy%=CAxk?zSJZ7j9v-G z43&e>loe?&cZ}`(ta}D`8hWK0j)8CRo&KorMixW8#4+;DKW2s3xPBTdkE9qdGJRpF z|3r9hdpy5vabtzoMzPZtQ$C~V<7=FhHfvZ4RFahGEL5KB(nTyND-|h}nkU4NXx>OQ zQ$nJZ^JOY0D@&QRgrE(V60zg<*K83t*K0al0c(zA{bC!7QfEjk$YfU@jzNv;Ua%t#rN`RNYJJh8|b& zS@EAo*;w&H$CxOX<5D6?g+gXgxipoyvGQWv0Fru0h(!M9rig|GU==gcsvc;6ZoaGi zTSNe?LurNKj-&?oK1J`ZYyxVVw8RPPD<@$O)?EclpYn3tyLm91x|+^BHybb9o38rV zyD-8%-CmFi{nDgwm&nfy)V{PZ$ad{0&?j-c7kcK(+3LUiX-Skls6m!BH=rM0(tG(s ztm{|R^Obt9p*$*3MvGCUe4x6eu|amQOD%d#EA~inM8>>U7e&-0kk)i2II7|uq7*;CQB!6>{D-?`*qYL6 zI`OURTL$or))S**s>!*r9CQ9>X&Se4AUwgyZ&3->)=?ssch zX($)KXYIwCMiz6Gm!^>ix7vp$w9O9c8jDOS$!k5&;z7Xh-1Fyjm zezDt+_kgE?JuoCfphjS5PJ-#?E6=R-?&S0kW_FGoCuHtX)EM;1roiTx=?5$ z{u-d!PXDM8(tOqaBt!q4AYV9M2bB@0gsgb5944pyur&cwIKQ$W!LQ(Q!)*Wp9AZd{ zF-^(~*H;|X93CdP=fLhv1wlYlabf^FY9#vgf-~5OU;X;)P^cYlqFufp^QoaiE7A$x zDN#n>rK<`3@qEY3b>IE%sNGb8akj zR@63iNNr*{M-M~8!X{e_GZa@Q8Jz~6N^OkkPfAFrdjgCf->k%vq_;fVGA!?nQ6e2J zRdF+f^DrPSm#gLKas;n7qWeQiVT_tnWFiioCaWO_oTW%cvd=@l#Lfg8sxc)VHf6qS zUGe=wriCoy^nFK)+F_YP1l4w$8MW59=ONDlvQOq|r>4i$O-D(^%z>(EROoIq)LlTV z#JPO9S}}_|Ck%(`{2ztxC}qcjjPgxqpr@a<<5F>(7tn^%25L}eF zQJ-yJf`RWvUmqmUUKrc~5&JD8%yDROW3RF(8YI5VoBW?@`6r?{^_NTvD~zc(i8e_# z3TEMk$*q*maS&!uzQK~zWo_Z1u;~=W$3{^INqR!t2Btt2BbHi;G@txbuKP9nsgaFm zUrVb)RMMY?R172o4iFSkl!Jj}<*&f~kbc!DjJ`CBX?|T$>cGL8^mP1M&&Xur({xJ93z&tpd zd)4lIGS~fL2*+=<8_}1dP>@;Z4Ox^b6>HmIHXk?9r?Zj(R1cPa>j@@98b=to0O(pU zOLt3Y={X1-!wX4H5-~0#O_l^w1%jYnd_jU1qrtwb?>J(_hjXv@s4sl3Pl>tKb;?PS z#}p+A!B;ZU%Ic7mMGq-*OY0ky)UQ~=RSv>IYx&*RDj|Wu$)RLzw(tP=2X5f2+=91U zy8DX#8DpQVIhchqvoI!TGc<+MNERUp)B;3`p5{joHZsy#{j^y{=J1!$DPvJVV~a0R zZPz`Bi~_D`jGM*b#S>#x04kuh#C=on&Zw);f5^n_kA^23E6gz=SOeES;9Af@D3Y=S zd9!_cY?o&+~WX zw=PZDLh9w_1mW)W4U*JnsLlq!i-mR3Szd?3Ano4og)G63da%_*%6C31pu151Iz0$1A@@;&2od1}Y;$VH;yA| zk-xrPmrlxmE^bPWqTL^lJlddnAOboCwi{J)_^XN=;?XUjVGQBeM6g-*q8+RmaiJrk zrKxh6NhX!_5Qy@MJiFD^m`wyCb58ZQm--_bW(AP*8rWdPPSpL$F5bbQuJSni811bCXv!*uhU(bM*l*&*dLf`-?hCf4hUr&OvBH^FJy#ts8w z4f%3nDo(xmg+sMjgowHg}mK;VIQ z{P6$b0vMFh$SRn3|6=T+!7@4cI~yZqnoT9Ka8UQ@4|%035B1zX#9QCxUWIep0R~~6 zbD~bKvSK_%9&DBENQ~iIwi1m1!Q8@x7x*`0pkMX=rMdWJP~Wa%_Ku4WI^0=+HCH5K zxeiUP*)>zuk}kZ8MW;|f)y2=L77Yu`qz*^zUAkx{AG-caQL5+05oWHwh{CgU=17oJ z=}1UX;qT+Q`=dnLXwJIQwPv+qZvCNt&PbRYNrg$0-r`^C-EgdGGnqu2qz74PNUF+I zaBOHP_;-bqIsGYlEpj!w$ zolg<#WzyM40_;^iEFg=E#S3Y8WY%O&*zk)<7ieNE+78B3tK7=9P5|-{pTIAf0OEwF znw7#hqEo1+MwtG368wfCgasehK!-%ruQYFK(zKV}aoJrxRN{{)etK6#9k+-m+RW`^ z=eH@Jt3zU|NKpTH3zA-#JZ~nvjdsmB#|`?)CCo!J8+hgO2TS|#wm5S7&EOko>Upa5 z?#XJ=0x!atGEL?S%6}FmW~<2(a;{Q*OP?V(QvI`B=nw&os;elCgFmV49)hOYYySPu z5+(;tkks2}d7g$Zav&OWLs?Qaxmq&vZ}=7zi?ShbcEx2Ooz9k=(R<6;)Zs`nBq@M& zCf9lRIS*8iTQLSekD*DjS*gvJAjO2nPh|p1-PZHMqaO9>-lL?s`QGvod(Bi&e1AB& z=po$mytO%g)bZdPXFBrHAmj0e;{or@{;lpYM8G7uf8MRGUH`G0A%Nn8#T1~$k{=n8 zI}!G!q$~C?!cd)Udss;dQ}5Nn9pHaEea!iKyOC65hDHx;>KZHUFaL@W_xF4^UDhs`T|_kp})2&x#*))R9`=% zG5u+%k2%CoJV%N{igAEm{`JAj$2#GTwI;@ zgO2vQB5p4r1ssbN#e+5IG*Wr0L4T~*thCL$hB929u;kg2t*WbvZ*c7v^t5jfxTv$x znx}XqrxBa^`41apu@A1c_TxNd++my{Rw*H1u;_AF7b<2WzIKrXWtE&?kOUG$wUS!Y zk7JQOWbqP8uSsL);NGV(y9^x8UhM|S%4$7piK&Omt;B2$9^v;}aAn;}^|3(+vivP3 zdti@IE(y$kn`J``a+u!A`+{S&JD-0Pm%yOYnFEW>5#uMC;4Ct1Ar90Ftp`qow~_Z+ zR$?O0tQZlAy4I@xWxMfMDrkz=G9kNDV?TxW5>e)>r*~#D0|$Z3&yIpY1ctkV0plC* zVJq(e18?a|9@`FkNct=#<0fB!f$eW(#vfFSwQ;F(hpy;!zp-BA;to!zZ0CtjR716{H}~x$UrFL12fM z%gn`9D;G=}Uf!;r;FqHWP=BZ>>6ppRxRct&)SVoV)%45`fuTu<~GZ#$Ai z==sc`3AA+WIqi8Ih^-nwh(OT{a-$OI1vUh(B0~BpBLq%Pl)MT*4fSOc`P|^H zP!1Cb)-e72C-{{AE|MY05&^8h28lFdN?y{k8DoZ2qf0dAcH+)yzoV&Fg~S(SXU~cJ z6DWFIDvW3okiQsL;9yn^ml1vVrlOO@gQiz6$$06Q;djkARHN~ zUnVSkm9JYvb5m3!GppswSC$p+7_ekfMY~=TR$%G#9wSL>;q+K`WQc+qYknbJH2+YM z0gsFNx5A{+8=XX+Ncr(LW~vv~kUCE>_5{oOtwmBA%mh_fnuysP+IT*Nfr@1Rhy$c& z$5B}NgrubAZVp!Nc6B33%3n47#dxOF_ySarqvovYZ+xv^x^U?h^kXMMxr|XE&$ePE zsmYF~$agZ7n%|B_wQL^TVxDS7c|p2_`Xh!}*jP>Th98JSfaxmzTk_wMKYZ~QL}(_N ze0fTtd^9q3d2iZY!;w|A{v8aXGp_jeb9fOz|fFZhK|0`SccD*cs!oe72D^aUpDQ7xyxR!TVN>__1San%F})P=@$c z{;g-|je@VkWXin)IUVxS%gJaq_{Sg&-5|< zLt-7-zF@S1Y08>00roWE?idPVBs_wua%Rwn?(D2T(@$Y=b7ooil5*=)cKz^DCVH-l zspdkl8{2ItE~_cAY#SGlee5NYfIm;(t8Dex85Bz=8JudjRTW_=hl0q;gT}*}u_j&L zbjfJTYN{Hp9)fEGu3U(k!`_~)8kA<_wQEGo1hV4{r$v?hxTy#}BIYb4>)fDNrxGvu zyPspz_Ew!bewuET{CMKS)4G;uenf#tW*wgKrxY8;;ps7j%4!o0=!k@{ZTCY5T~NFp zR|3==tAPNDhV`c%!#PmFRfZrXK=>v_qxBIBHkAAUF*%vwuDMIz9SuJ5-@cOxT++e0 z^pLgPbbn`ku*bhP=6XE1-jf%CrHRL6E&y?0F^r9T0r^-*uk<1FEeYwHp78FP_}M4W zw{g)ZkXU$faOf8GXVTv9kU|hC z0Mn7rrGuoaz1$*kE<>h37hk6gGP*+o01QQ)C?RCHvLOb|pOoht!%u8Cps8^`b(1Yh z1{PZysJcot^c!Q1pdB5_dZ_lo>CK{1Od~>}u^o(6HFnvwN}zGH(x#}7K(k-8mMrZu zeev#q@M@o*!5Q^_+q*C`so{zKu{BwzA;mZQ{uyD%d3erh>SoPmmIq7xm}2;)a$G{^ z<^1C?#|zlbsu0_OS_fXzT+~#o9GOp-E~4NZP4|~MK!Q_+1;MrlXL~^Y9T?d#Msh$w9eU^(0P=Z|>@`NU>^DQLturhcD{g@f(j z)!Ub^sJc;up&TMyMauF}KbMN3pVlJ2Av_x~-;v!o8pI^augYE4Nk%`BOwS0o>JyY( zX$x(>@r9{b6TB-=K30v&RH)9C8Kw0t8kITR8W3^y&Z{LlOxe(?$l%di58UT!GZ>lX zn~IfHa$R3c0HHkPSVIyTa#}DG5fP(uwBf{TOpLr?$@FWkuX-Qbq^Ci>=yApw*X0?s&xbmQxqIDU4e;M)p;bWMEapJPCY2W%3XBD&|+y7 zx>-ZLvp3VNQr0R|X@P4j8W3^lH|-yfr4##wUhM<`qoHD#m!AnI6m-;)=aaxNg6P>m z^FAWE7VcKNOmI(Rk?-mo!@aPa?esu8M$ffD4%aVGtwuuHsfB8cLDrZxw{B&{FZ-k; z5%{$K4{SN2iGgL#+9H^!k2`-ga?RkAhll_It;je*_`sMYo-p=&#IKz+fP3l(M45LL z=A#3ptxrX@f^ejXhNjeCSk8)9+4$e`Ao<_1BOcUVF#Ee~x)zgC69i2Hv`QSQ#KWjW zs&)ARd4%@39A7RcU+@`@D}Sbx>GAQ_mT5UhK{@p#=`FV4XCHBD(Eb@R?|n2XjBqw& zEXv|Sk~zBNd(%{Sgrb7ZAGVFZ-X>7(jpRX#rnyiQwa=)p_=#z_AF7W&9)`?4(w;P{ z9nfZ}B^U)&{6j?2K|6#?OXFN?^EFBFa>|y-%mlXHq_GyNXcA92 zZZxxX<*l+jWf&_Z*479fm3;C3YA^2y&$@_=EZ04>N2+0DXgQ~fW28!Fuy-u;pAb!z zk4P_%bY#1IANKIP@su%cm`-W7np8Xo=CH|og87HQ@#b*15QPyix3LNG*HyoA70tJ} zk`#S|sTJ)Z$ib!LuXPI(3kg`B82DGZ!h-ad(+~FkzYUlGpFIqaz}uMagS=O9FE8b2=rqbxyA6q+SB&y~_)>x;(R0 zhV#3|ok$gFtd7kk`pu>tWtPbemhHtyS{S8cXWasTo6A+l5vF6J>{4d zAO-FFR+n3jzZ}(N-kn(~HC9KyCN_cw85w>|Yt}LS-h;3zT688UZq^V&fr9%$$J^|P zg04AWE+5)S0{5cyL@Fxpa96W4xSX{ZWc>Z6!s3Dtw(I4Fz@_t&2X-m}uAuEku7WS~ zuWSNLmq3AmMlm8+r}I9!>jaz5|9#pJKqxZEFXF9g$;teYlBKc=e)th5T|GB)2Yz>y zls&JSzzvXv(eO?c8ehtHgL+dE)Gc9zaPP3U^+N3GDz`VKG_17b#3ZL@fDAN^XZa~7{wNbDvAIAaY?;alDGJ(wVtgyt zu}@T969mD_sY24Bzu7DC1eI zFZ8n{E2al~HE~&0SgaZRSQN4nl%T%K0I~|aYPxKQ|8j?9q~*GGaw~a@oo}%qoXkf= zIJ$!K36xLUT5c;8#V5HD=K}KNzxKR)}PyGGyzg`F8+d`dIaO) z56OVGWf5QPqqN`|&76=-tZR*pjv@yHEBLto-o(FV@gMJyd_VD$8XO)1W;erxJJPUH z;6wpsLXx#PW48ozYtf$XpD0M*nkR-1KmCHo`_89dL5U(^9G>Kq5YDC?A_Dyo+;aXS z!bj;XuJC<~_oZm~b>oUqY4{{sJrPUNYOt?OIxfOE>fFHWS*!N0De~m3Y-)ZiSzt!( z1E=FaWg5oS9A*$rUz&ybS;BS!KR1LdXdX8JIEl_UlbDR*U?o#cB@Dx`3Q4Ap=b$QP zI*Vk@uruwgQTfsFtFy5NLQ&V`ci#RKgp@*ZoyL0T;E%>og(Fk51?5yO9Qg}>Mtw?W zEb>_M0k*w2bAFYS4);C|pu8>=`e=$a17VnQCB%kd>B||Pfb4up`ppf=k>RDYitvng zg2wElu+8s>vjQ3F^OlRgOOL2~Qgg73d<>#Bw)r36jQ&oV|5k1|`RrGmf?do)hJPRO z`I`BjA4P7DC4lrwf(N^=ju=(AeVF*1{#7{qrS?Wja7j(y?H3_67-FJPUd*GOdX-W- zKEIBneDhh~j+eV&-fC-0bGGGOe%&OU0W|wTmOra4Uc;FTa0y5W-VIhOO$+`GaQ6cU zLDtP#$>fu{{Z6zM*rC$cAMld6eM|6M!+?a#RoTb0?afE#y85xwJai>p znXg+~kf!{_VqG3z=r#S zb4NLhuvkFyQh;I3e*Macv_#=$afWB-x0GW5Xgd`v@sG0{eu5Ah zPAjdb5Wl=0uzM%}2Q^JIjbahe7f8juX5U3cv)1*WS18msSQ!fR<@VrTnA?Jh2$JEv<}w$fqP2b4-$} zo1#pP?u_I{)qHGLa`Ko1#s2Up&ZHB@?nOeXh-8qZEh?6F+`oE8R3*0Lpo0|l&!?Ce zNyjJ!SYS&NaSKGVI{5M@Eb?T)F$5x`QBMwi`g-CC#>{f2b63+R&C*Ti~QGHY3o<7v^@1w`?T}{Zh@~9iKE&lVbM4ncnQvD2h(Y zm!dpHv}$NzmlUG~J1|Qvzmb};`;I5N9ob9LRyusqEWJHKO2tyt`)ou4gXQW_K_n=u}N)rH?D-gsiyc{AzkgU54Y~(hhx6X{SN3P!;4~pbbxb(;z0p; zkxwSip+zq*Akp zpG3*unwf`x5D&Z)C3|Wa%(@lZ%?g>3$Olm_-&XUvN$62qS$*X zzVmh_How$#0MmLpTC~Kivs@Os)>rqhJ(daGGKpJ8#s(*!)R*Tk=NI~g6c@2?TPss? ziX4#^hY$0o&6Bu?57{1OB#RAnC4N3=$i&O34q6YqlogbTSKK+^E;x`em~C>tK{yBY(XQJK)E8Y97{Vip87Bd^c6%*9&67f3^F6q_12hkVdyg!( zk)02Q^jO&-@?71muGv)A zx50=Y;W)THK#-vUAb|lmY!_NG5#h4%6caK1O}QJ7)VC(j0Q4>2aw>ZR0_W??BTRMp z@hbw&1H}DKn#5b`2lQb-_KD+Sxz)~+wmJuGDRGYP5ihF#l&Dvft{=7c-}jcEwvwl3 zZB~!oY?Gm}wlna50fee@|WZy-<+h_eoDa~O=%9N~L(*PS>5z1G^tE(5tIoT%x zGGN>*dKsX5A-PxyU{ zXRr{=HM?~#go-`R01*o=Ape@dziMWz9`<#MW8$Cpe=oujq&!pxlY!!CEG=Oo!jk%R ztG^2}Hvr^!0{b+rbXifH{N&nA&JtV*6TJzDkA`DqkNk;9RI^(r?BEwo`srrZ4-!Be zI({nQbSBsP<0t}CLYpoFHp6e*S9-TotGo@qs-ecnQm2jS|VI3eBcaR(n-vWrgO@Y@`y>E;L^cTCw7@Q48Z`zn{0O^B`LN zkW)R%FOnQN>{ih|D8K_gjZ*IKGglqrNw=S6^0mEdnE9*Yy4+o3bu{J+otRP-)TbYHjN2v(ce%=8**CQ!jJKmsr4Q6tm+CdLahu+A- ziU-m6`D{4?Faxh-1expoLQe07Y{J_sZ{8BVKOgH(w^C;Br*RdNf*$w3-oY{-XU0ib zJO@f=;_FKrlvx;NVtb7)%Me5kI~UbMh>M}1Y>y^x*Od`bjV6&H6=^cYeIB5w%#9!SX$OqJknQ3M2?Z4mMygPD)@=X&fvoin9c*ydBTk~$j+L*hcSx(2%{w3c|> zWj4P#ezfgSn6I`*ubo*1gAhT6?B8u)pc;#&=1Iz|vDQayG^p_{eRJ?lR(C)DIrX54 zt?INLC>BuZLVBSJvOepvy$V03&l_4CW0fqVkwCe%MKr2+!!KmpH2t(VEP}6RJ@zZ1 zv&pS5s)0K_$r-?k_<^Tsebv2S4Lao?0fmW!1&j&C5N8Qd6N3}_s{b0;LXzV|D$B@{ z6@g@d-^X82`aR?FhF;U&Gzx{M+Fp2pzJPXIRVVLxU@w^wKx8P%G>O-!tDbVCV^41X z{kEC+6^$-dsjGWjs|txm5X1$pj8lRz#((8{NOn zH^N^J)l{h*;={q|pRE9kk##G(5}&lmq(3Ea44kzJf;;gN%h8N~M*1oW=omOHGU%s{ zt%;ewTc;w6`qL@8D7N0t>DnlWlG7!|>NV7>Wj{|`ng2%mT!Pd1 zToZ5QsSxMo$A$`hhS`km%)peP(>b>i5cPv&<3;&kpIyfXITCc2Q%PtQtqGM@gbSheM6X(7yevCWG=gV3waw=Li zfJEtUF$ag3;UEP-()cr=gG0<-nE!1bpxC!2bYH^muD_Ek*aMyH9O9qEU*!?rQ*s}D z6P`VGM!Dm*^Q}L_5@bQ6en#x23@`7ec&f>Wcx`?YhUuDKeIZ2j#d?8Nb6O1w);=Yf zN#p_IV7WgP47`z7KmKH1!_iX}FC_QaGVv8_%`Z%+f(ic|o%Wq4XNYAz=|Q!WlV_+< zrfAgMdcwRXZo5ZKwXColw4ms8Ur1xxEotjy!%l{%%-Av>K_u$5-7WtSU8O7tQQLO0 zp*MKxRD5Oiaj0wcAtcpATqz_-vJ=C*6weAn2&ZkcwTg6mMT6d;=ME)DV$!F+0W_?) z%s;MwOb`$)B{RO~&*~pWsa>afPb@yYqoXD;(z;GG{Ck~9F2wT^(_=+FEg(K~(;655 z`-;IMeAxe%H$2rGKu54w8T%EJ!{L6#-?9RUk(Qt;DNhlKMpNWe?^BzK^B^R*bALBW z{YE-*P_J|dpUonK9B%h_$-MTigns47rxX$F69=w6#FHgYE2s~QM{Ky=-4@j$`=?In zoziFeUOzh9F7!;mdCf9527o>X25w=yMjIvP!I>?sYDCX3U%qa!7e>gVO zS2}B2L?yF?DrY2R($u4(;m^u(6XNBcrj?s3o4|q-H_NSSlrjv&l0+LnPwL1OBH_rD zW#Iz0tqq)LNH`UY7(O9f=rh7ZloCWn7b>e_ThXn%@8vmWA8ck*CGPq37b@U*tK-Ef z$Vyc?FN&29MTx*0tB6|bnkeF8Bn&lDBr7P_24DDx2z1Mc^#4-PaMR7*A+9bj`H^!hVYpL~2Kv9L|j z{E2}JcCF(KC8J~h(Q;eL%AAyGL91>BGnjxp1rLaUpxnmmiOwLp>;IPs0cPl9pe2Pu zf`gszhym}+)qgLAxFIFT8l=OS;mJ^*NHcs#jE?@?@d6p)SAl+OEO*I2UHmPo)G|>0 zC>c|-fQTA0Uw%v}9eamT1>zfZBgGIKTBbEB6&t(@a>gmehJWFYpctd|IfLptIJ!Qi7 zL_=WBWrgScbp|^%nIK&6QVUIR4N-Z6U>+1k#P4Qzd_Eu=E@4oYpXdSsy$U|u|Ft4= zI50twU>PCW`M+I{J@^+24m5zwhja~zz+Q&9)SbJ8w#)Bv!Qi}j@w>rSf1n5PY(G78 z@3Qi@)QQO|9Md4|U7>nWVO*fKUr(L%-ol*>dE0Fc6!18HS=Dq-X=KPM30*lXfzQIj z`K)uUEmQNL8sA99!BT$pLuT-zicKD6eHpc4f9@%P&smygOTsp@l|1u?oL+XDvpSk1My4TYY7&yHFu{Ex#Dtzm z(~Sz$gZeEuZu&p$GPY0q66f~XFYLDae>yS4i`R)<>(=>EF)A0X(|$J7En6QbA`%zb z=buIxT_gO>UJTKXuoGYpeU>cflYTa2vE48k`JxJwuJ*}%az#<;04e$gMPnQu-H&joa=T(LsR zV(37{@Qx$hkJ%a{Ldos-=lFCBj+Ni|k#!avG__BP_sPqoCN$0BJj?6HW`9?GGIPq# zi%E}GWM|PEPZ`MiE=_Peb)Ak$DqM7K$_ zk0q1&5!^93^f4Hu8Vln7*cpLkCu}@AVjj7xxXeY^@D%&HvFejKNdr7)s%-Z|>%0)= zyd7igcwJ*tThaJwHaI-$GW3un3ov5uUUCg8^soa(Wsx9Dymti{1V+bQQ+VX<#t6EW zNL%(2+ru~#*oYH!>kEIi6(8kx-KsNWiqVz4v1`HMA>MAUVu=r@qDEu9qNS{_Jhdug z${xwkuIqS1l8dNz)JHoSCDYl5Hdtt*Nsre1;#LG#-2H#Ps?~pG-hbsj<3uwx|FQES zpA1GdOX{P9N-kyx+Qo2%qhSPA-w;8_K7QSP$KldY4 zmP{LJ!E!SRD0}Ql>4z;soSh_PatMty3P9Hu(MfGf0+am%=hucV)M(1m788qz9Y`l< z7uu)qQ&Mwq8GLA&%6ba7!2sgU?;{Gdc}y-l-!i<@zx(h^A-7+Wg7|1AM4IH5V$!KZ zTq%jv_hJxs<%_7LpcKPxb4plF=!#OcSM5T0jutNsfu8oIDf`mLUN!}Cw)}B^Pat$F z$lt#ruzbUEdm;1(OI)ly4xn?*4{Gu>1!A5?FV%#du)bqg9w1I*(riI`T^QXhbm07e z`-Wu4bt_#nND!_fsGVoWJ8^@MGb>0sfs1y&p6{<35y1I89Eleto;m%Vlu z81Obh!RjFv_~%GQZ~*~@acb}hIyaGey*l|@IAQ| z7M_DKElqMvh`dr$l%i$Y@ophXJyaDoc~1vVIW&IQWW<3rsOniW;!6@n1c?BNUJ8fp z84wA>w9j#mZ&8y6z4;zvqM&heqftw>c9LdJv?N&Pscx%CfYTZ^w&>`d9;@ z6j`{Ozb7*i*wX06%hMQidJpQ!su`)b*&*2r2dyZ7A!JPwI_k0q*QU`>lWbWnnmQCB z4xoxo6>-uzbC*HkwI)$YjX@D6vE9LKh^cikMfq9|?^I919Z8X@sHD%Buc;x)X<2Rgr#gvc5A{~Oa&RKWrt z_ABO!N0+^Ba1nvzmJuSg3ZsJ`TzRhoD-~eS9~)kyz)FRc#{G*Vh@~byv%*}|gcP=-PmNH`g%X{r31=q7Vx>)e z6I5c-dASf0X}H?8l76;(WZUjs^!2uC$YJA5a#$>Y*061P&VA*3EY>p@h2*m>^wRU2 zJ!}Q%(C21wyO<# zr^~jBbGGtINA_<0%;XFm)7Zmk zF3!WHv(A?t9v`k7YmK1$Rf8yDm?FSKrz-Wry(FeUpRjgmI+E{fmcK%&kVQt%xZe5d zK{J?&_Fyl;NGEg9ZI*4!f0W+QDe=6zfm9gYG*gIyD|t$$h}_tXeMdB*=FCvF4bXE&cx%OWNkLvsne1F8pZC zeKkc&V)*|@)mufywMARINI`IS2vQUd!QI{6-62?z;0f*)9D)RQcXxMp0t9#0pns8l z&OPluS8MgK)?8x_>Er9^E#cBIb?7-HQ>{>{5*RzFdnOC$$RaQb@H?Bv>;V-`E|4Gi zij5{!?>nqqHzmojaZewXU{!hgNLpvV6U!_`(jRD3nyNs-m^#*Azif%QCH`0alj6Z4 z;PgR*&B#Q+Me@7Lregqs-*5Oif`@&t5Fj5E{gJT! z<)(Dym|(&VhGndDkWOTokYeRSUgo;}A062jb-_v~!>~`q;?DZVYM74I+hI-Yq(@(s zd{cTyGc1SGIzOAX#irF$P?2^Y&FrnNZ%Y!`=0JKnw;=)v%|~feR&xhv60>``nx&TI zE6lfaLCU$uW^DcwH<&UA$mnfRB4)Mr)I#F^FribM5;L}V@MVZcrQ9-ck8%Yc<-dN_ znF^E_P(hE2kZ2PBfWij1sZNwP0ED9~sTg5dkO#1PF@8S_YlF`JI?FZ^H#yHaxu|G< zKdjSX?38|I>qpIEAz1-Y3InRg0Yd{*xTi)P zLA8Ro(w?l(XrQ;m%-b%i(@aIvXBWROsWX%ao=ZEz(5Y?DKS;KgNl1MBKZjg+z4n;# z9UlGm@Xl;5$222-m=skkEDsR~sO$I;-69#;BI#(-&OYy{act1Hw>*|4A=ApA-pPso(0M&C8ZMvYWGjL|8+$sMURZ-iO$g8D zQ5h_Ycph_(STr*Lx~x2OZa*Ro0x6@25~*-#knPY=-iUdBFpxoptbWs9c8*%LP4vO) zGR`{dnJ_A2_38Zl=%?dhk&ApG(|OXW=gMD_4cP+OVBK&z@_-d?sX?|orS=E07Y@Fn zXs=MhDtX+|->h7drmGtLW zB@+5xzL;ntt81GCwe_a$8x6=Ens6PnCdr;jujWia1RYX}8#mW`R%$MHs-k%qLB995 z-sd+Ik%3^aAwB6kqxo4H%<;fC0>^!HlnF|Hc|eNVE$NfMq3F-HKfwIh6_Z3<))F2} zo_fS`^yF(@fz7qr2Xmx*!VMuZ*>{`)+yEqX$RyQ5;lGt*2&5DGBR#xj7?)a>lAW}CvEyfJs@2Z!4%wUimw+&L zw|PJjlz2QYZ_Do}!LB;8q4*0aBLPOVi~9%K(ANW>YIG7R&$g&63H8%|+8O?nqb<(WS!9QG!zjm znR=B#f5c&gs~}TyQ+Uh868g=}v=Hp!MtYrCCJu~TXr+u%$Z>%9HbrWX40EQ%UWO`K zZI|$O!@6#y5by3k|0zDq#2=6c0^w@NYEGy)sWB!_dexaRk&-1Re(XQ#V(Yk88oS}vSA5OjJB;yBW z&q6)Lw(=MAz<3k;PcO@^g~27Z2ccslN&d9RfikKz^0r~|=(RC5Icr7?*uaFj@RrOR ze^v@PA?fBYsIs}PD=RFBDd2Zf`NgxFd(>`^@F{ugXzz%{t7?XBX7rgfEN8H(G=cIt zHvN?-*_fnr+=k5QUkuoBoRvIUUv*1TrC<13z@7k_kDTm$;xzqmWNC%lB7d_#8o1Tm zxF#iolSLk6ryHUDKx70)pA&*;9-jTC(9DT==I~!*0y@4&F_3%U-Q)kVM{slx4>x`n z&yIU#>b=h*o*qFPI;2ltwl$Q8;_q3}Sw4 z;jSp?qZq_5u2-*Uk2yn0Uyue>h^Ns-W2fO3Hxd`eroqD{3p=0`AIbG6E5T`^$DC*|C%qJ2^ypGOZZBo-O%=%R&w6nOl()@-osL@0pIla)4er+{`L~u8 zztsjYcG-;xk(*eIO^xra#*S;^d~&J=dsG}!2>EGa`G$6J8|5siVN{{Yi+#bJHKKW` zu!Ls9Yxa!?#7@s>Y_;W=p{XDrta?}GE z0)}{_PHVJZ2sr$&)W;MV>U}erlno9ZO29+l{(rFV;W5%QVD+plTytN}ry|n0xP60V ziks0#=lD0K#j75wd(oj}AK+-9T8*qWZc_p%HnNpFbC7)EB~J3l@Fs@r^)u?Og6P?P zEFFq+>O~HPF@Yc z=MTciJW}W4P`N-Y16I4s)Rl)hh#ptpf}as#TZvyCWG@lp;UmRgZioYC;zmJ(eOfO0 zTK(EOSMYi09ZxZJ>$FvxYUYi?uv-&hk|gBqnpQ}R?yCG)^b_(e>W@m8!L4{zrmtEFLA-_)Wx|`G8ql{@eBQ!{u}ms{gq#X+pzHsKEr! zx^Epk?+^3@iDOtTW7mv3oJd`io7r&&BI9MDQ^R z1TD1&Q>b&aQp^+d^V-l`=HkO|^k)?J0`)z%q|)0_A_1C(9Visp8A6g~j@Q7I@05mF z)-JOA47}UCUGlh?s5w4)$rcL*1lE%7qmI#L1VrL}!X=W30hY|$0xDFE4mmwZxLIt; z14T6;QTimtL0)YoX`nMOrwRc_0sl{E8v{rLhCsa>=L|SQg`Qh?nmXoW#-}uz?E(>3 z(Tj0_++V96qw~1?E$`=@LZWJivVHkRO1p6*-6f{>gU`TO10ur8=4HLhhMey_0Cc^( zV|C8iA+%l3%CADA??^mG-^Do+cJv8aJQ>O2JBP zG8?j=^uu>>X7Zl5yzANUV7sA6!F|Ky?suEuF7PkF^spGIA3*Uqa<~`|qMuo@%irb5 z&!aD3VqR-E=AA?OGo@o@pn5@4d9q0H`LSaDvMvajRTvxJIVdjnpGGiWci-mbGKVXr z|9nFNLP=NI&Bn391qKF^9IOdFR{n02%xiV~S$pPrNYwxHi`8CFq=|B56na`t3(Ylb z=#snv!$O0DT?SEt$w;zha7%fqM$wvH4UaH1ys8Yo1YhtV4MB;offUDR&!(*#EqV-NWB)*ybG$gloFB{|!(<`I=oR_;7BmT+#*07dxBispMPn?ES4q@2Q5Z7o>M3PQumx7#=+4`6 z(6N%ck)6~^Y_nj@$}2jBt}EWGHQ5*PfFvr{ zJ*s_ZNjY?07Q0;Yx?i-HcB}AY%Ch)dcPX#G9zkUY}E(Du^$H&CzT(r z;FOP#YpO86T&o#=>a?o;_=zZU!YIx%_vAV#t*Apm?H7HiGEP`;xV9d${CxG~M#J)) z0@5su)y}wE=4Vv%f zPFgJCxMyxClE!H&HC2I@mb7+DtPOq8-|sU9`Am3T-y9cb1jAkg?FuWsEY{plP{B-H zJ4;?Ky0cX>fqSr?dzcP4Xu!TZb*ER*zQhu?aDV3Gh78}YSeuJxPujl@AAqo{sTk%c z0X3bNq_&8p56a}*PWV9biN!sYy#WwAjd#}f+yHAPLpDU|-iB z+1zF6#rz&DE40WJD#q5Z?lR~MaI#w&-N}aaAWF#lWk2avZ;hfQ) zzcpvQ_Lq(RX0i3HF59Vf&2oMkgPOExhS}USAs3BWJ(f1(aK~_s>{fbEKf#~a@P~Zy zD2X@sim1;C1Kw~_zw6iU7Y1hZCqpr}4h6_iRbmm`=r*C)7Y9fCYEPE_c;vW<|-%5uB z|1I2a6stKckW=y3k8<-svMTAqDGFf(_&FT=@fS99V4K zB};RtEb>XhCT(`iBBO-0jMk=2%rH~$31)Q|!GQ2fRYPM{4e91O@`e9m!iFfWPT#}4 z4fZsDtA$%`*Si8^R?3%WHF=*YVN!57cM5Ly+ntoznr~ze!vd{10EWXCG^6u0vYCGXDythY>#%HzqDZ{+_-4U`Flg3_DTK4~h%FV%Cx2l^QjsiqkWfg3>n}?d#Zl z!|(AqY~&MeHy4!c*Y&8KK1ux@HVeLJ8J-OI~a^o-<-E{9aA1#*e`6&m)@0 z>;A<$w;EM{Q{k%4W0OXgx@V^BMNnLGtW^D(Laz?Hn1}5zTjLVaVX>-ALoptH9zQ-X zG%P+B(uyUL$L{a6P#di4d@&x_$7j;hI+b6W41G{6lY=6i^f{{Sr;!KC-;q>)6%G5 z8c*O~w2%mA@HbZX79RXhI|vm(!sbe{E-=|gw7>-C%xH}ESp0$k_m)ob_hanS^|mf& z;%A#LOAUwPnRu^&Ov?LB!C#q+Q^^l+J{j4J*Xeq-6v1h^pgt{4dZ_Jb79d%pOsY?^kSWk?;1-G{rSN*{U>N4uZjexN zpg*ERW?QFxS zZHY@kU`JS;nplZjUsjRyP^wWbU&ZMX~KjaA_PW}w$)h+hfwe)mA_*2?2IO3 zKxn8OOtVHXbf?d? zBy!Col-sAT5dTK;zId|OIFMjh!bz%sazStyfB>lYOG#tU$Nm=o1bSeFgd|P=@L(;w zHF$4F^$P8CSf{vWna#xCPAz5$!i+_L2kc1W)KcvaU3GfK{=I#1g7b;B`MZSn__SwJ z8?ohliBT34;TrI2X!oXh;bV8dq4S$h|EHtv?3fNmMxg~wa8H4WmYj@ugcf7tDL=BC zkco0c%WgT!j|h!MZH{RIGqag^b@7s!?(f3}(em&dBhuTL0F~wJj$YiQ669JYz_kp2 z{C+a&o*9T6?o7(Xd8=(j%U*>*j4vWZI6hA5guc?^4{2X~kDZ;mZ-cV2Hap6qYwDNX zY2h}y;-z@*b!ubVK|{F3aoZmq(?aEiMJ3l0eDAW z>*P$Uj=spQ+v;e47YTJ};TxK8nq{T^wt^$ma2)!{+@d_SfvZSar|pqb+=BGxffwy7 ztDawNofVeW4@@-TI;q9k6W_viaatqnYnsh+2b5OT7em067_rvBda~lZ363@hN$N|D z28{IXJzE5GJ4WBL#56oMX^L94{J|aNLF4Szm^^k9Y-TiG7YPE&Lw^^MJk9uYQzfSfQVyl+i29g%*g}fEXny z@#>jq<_0qU5FJ_4>en0Qh4Kl{GxX~Saq_-#zbyzG$IWSmGoDfP>T$?-H+C!cK?AHg zr%EKpA)Y_JB0}CK!F}uK35knEBNr zvctw?U-^;k5n<^r^o94Ry?v~`Kf}uIDWguFpJQeLbv}R9##NhB*O0E=IKSJZe`|W# z?65|s-I&rvj$pOeO&70`Lxk{aa_~h#SWdZ2>LYo5Ow>a$JMFh?1O=Bq%}hIIS*-Rh z63qT2Q62Mc7A8MWRWxm-Zob(tG%+-JA&hAS0zBJIYoN`qPjDh$b)+oStVJdGG@)H_5kB(jI43E5w~$_q*sS}Zr* z79X)`gPdDfyh;P(pHaNpC|jZOWVlJ4SLb7gDDk_UiwGmrIT&B*Md)H9 za|y6JrECd2YO?ZVIN}k#LsyMzlydYTv(n$xt4e+?BK25VMCFTn`UJj&Fp5>Lvc1wp z@swAZor9R<3GL-cJ7V~>j{@pI>~eL3+o!wGzWYTWz#wyHfa_t*qh=>mR&WunftCrc9MAkjD3wTT;PiCA(4w6_g|@-Rw5xw{my|(9&Zy9Z#kgyE&9S1 z<5Bx14&I?tdu^b}%<3g^y720zzC-80;OpQ<#9>;-_}rbHCPi!|MNY+XmI|}6!6So# zsSU4>w7;mE2eu z%z&)DReG}OZwM_!oazk0xk#0@2V)wTAO_E_q>LJIYTAwO)0(-qO4rHrZS(*x*Z0R zQOsBbJ>n-3w3JI|k#2_=kvWrPhJ2nl=!9{g!)hi9QYx9+n1z{oZ(C3}XF*wcjjLmh z=(C^&ypE*-UA>HgB>fz$6k=`vuxPWSA$o@zD8mV(U(|Et!~|)~yeTLEx;u<6F+`jD z!X&hH^S*eeX?!)`h!Y;v+mOSbtT-Ga1F!OP+t8B;(`LQ@qq_*J~8Hw+i=5OM!^GqLxbscw>dl5gPEY3EKkj3?aZiQ%`b?S~%BXUvE2!M$(56cCy|SVhsdAoju_huXB%^%Py{iZZda z{$-7;AvU8t)doEyPy4XYu6fqvtiH9}j=HuX|C1^$Wo+w7!moyf1Z`nDN0$g5%4;+P zQ5wE^GeAt#_qLoBc`=+(>7)xn)Gg=(v`L~DJFlvphnwyTTa!U$?)o7!k!Fv2?HLQT%RI44$s*$M zZ!z^Fj{ZgN(4*b!vz9@(1^F-YT%mX7Zt}Jeo<{!{f55XL<=RZqNrL|w{KHP92%qX@ zzrG?8DvOdWK3j;M4PNMwzZUx}7L)Wfs$_W*rCg(_Lp&y=?J1#& zxQi5wY!ipQ38A==&{W%a>e8Fo7_4n$Y}Gz`Ct|Awwq(`d;*=$;AcoEI2B~ z&g!yBp1dpQ+G7$s-CW#d#KIq%bi|#)n2VyIY7Z(k#o3>kMj6`O+_PW2MT{EqqNK6 z$3gJ^cRRSGl2Z{C#-O6&rOoBVfz$ydp>8uXV!s8TWnN&tcI-Tl9_jgfX#;Wt!id6l z<3J+hU}v|R9rw!a96>6<1}oHyGasoQ5;4BFXfFCePINUDArA~XSPpgE%g%uN^M<>7 zrc2k@rR9*H)kHEWg}HcarglQ=arHtPem2Mq);fA-CwcY#>KM-Kx^dUy*6V~wsij?H zHxFa;@-^&gZ9)Yjn>`#!aB==cVjjme6cMwSzNscOu8H}|h}2~I?kzb*4TWeNA?{6I zLE1F6T_Wd!4oM}>&Ip9(Rk%w86e6NIq;~jKSV|(f5=c@D)l{qsY1cVgMbiE`G@r1R zbnGxF8^+ivf&<9;fW}afn_^MTb0^j^388QT@d0#my1rn_TltNv6Sx7O_7{CI@Zk^m zx`ZHZ{rjnVm$Bfkh5*g1)fURo&tO3DB~|RS5x*^ z{s~U#PKRh$% zR#KXkbwzPLX7n$gdV)N2Yc`CK1x7scCb!+wvolA&9u~E_S#l&)dN;d`e_s&Skj2{3 z>=3oWgFlZb_JzS;SK>Bq`xe|wD5seJ+>`FsiTmY`soiX`Cxf@Po-do9b8AD}_K?3> zSA#!J{m^6%F;Y)?7hF4+v!NV)k1~%r)OC31jGydg3$V}9@2ef44?1R}d1)wh2-wj?W&kX(Ss3p7nsIDS>S^ukcqs-Iz~EnAe)6RlKo?dpRySVGl{6 zd0QFX3?gkfpIK z61oYQf*9vxiO|P0QPA;B#lgV*sF`Fa7C60VKuB0K zR+-vX?!F@28I8CTvTd}gtBS=}i%sgQ?bPn!MJKKj?5DcCQJA?g#t8%)Ad2kOQ{`W8 zd~bKs;e~G$?GM3&_mr^7*7L$N5cdv86a4hi07;jqLY2SO7kRpytpU20^WH|4#+_+x zr48kJ=Q3D@VAs8SdcD2=)$}WF+k^%4b~70U1Sj~Rzw0L$-t|I15R?+H{~2)vfOQp+ zz=UulX%S#toEBVOC8wvJlAFz_++ax&~t!3vz08wi*gR z6CY}Q{4@GNm`^n!jhi=zopS|p<)`Yywd7=bZr1?$ zidh{yt)yjX8ChKo)kF?f=H*IpHt|%3nAs+nT5P!yu{%2=VN;QKZVU-Ru#%RjC2P)3 zjeCy_7deL>dr)qKevGU1IJ7Rf*@2gX4xKoHvSAFXnfAs9G&lH%*^X>bmANQ5N z#}!&Nlyiv~%j|a>PgjV{hfb0f{;s}ZFjo9M8m`yW;$L&IyS!o(7vnd$vzwiX;d3s^ zAE{g9r;MaN`0Ny>2SD0!CVtqh=oQH2so!|dO~5P2;A)@u{=;C)k}~dF|QHaR(kdB^Dglygxp+eAW2|sQJuXM*HDg{o7m@`!d<$^-O<*9CJKydl@rp>?P@vX+C;+4}%y&v8!l5vx zPAEvwy=DKL%ZWIGZ(f6e0*s=9V(H4-$ilbH?X+R777B&W8WH)V{8bl1nm-UWuJv>$ z9y+zZ3~IyV(l&DbhI%}mm>+UFVN;Bnes|U#AjEf{>OHimwy;2OV_+5^E1qF@p0Yi^ z*E3wb+aToFdc?=;l$yes)B+~7Ub7o^=`(5on*|9hwavngRBSr8HBu`#xeYw9S`YXT9;%3yl0o|=#h-uooR(X=D6vQ~uFy|{^cqZo zAXE}g92@SfQ|>33k4rlD3~)ax$_`Gk%HXxnaRF4$%d@X)=3D{CzW)UfrT5)qaI*zm zbZ!Xk`+{EpTrnUrhen$sE1Xzrcme>&qSv?q=V3c>R+1I_P0>zIC&S35X3A z>lqv*Cv~biW=21LB__YGz3&ly(J?FmLw+)n#hkqglS|Z%WV%F!5&A)61fa~0XGVRy z4U~tB`UOXQi~gQdZF!p?y?T}Al%uII)SRc{NV(i8vlPU77%@iGMbd`tT&+6*<*7h_ zd<$7(I{FwObzxTwjya2P&Hchn3~m7S*OK$(pU!E(or1o)aibU7(9niayfK6CGJXm9 zq8MYU62I=!{Zqeg*(G4&&j8^Nk*+w)mp=6vI-cES_sU@9O~%>&73BOS`)ji=%TtKZ z6+IZDWp#j!##y_>lE5?2=l_zUpm@RCk z%_uYsvbI!6t*Rega=HN1K1v4@MUJU{FfjWc|Z-)|r)@4qp-{7^R<69K;nkzugel6!;>6CqDl=k>DYa z4HDM>?n!{Z45>mej+ss>DlirzEN$LRM`|DJy?KlKKq%$IHhF9yy6*j}i$%=?GzbjM zOT!D#kNYldL^4M)@scEa#CelT^=cuwYpZzH7+dx#pG-5Ok{}KP!bC@Vn@^N{X5@Za zw7%CgIXjew~B)dweG;Yv0lD$uN6LWGGW4v)PCm~5h^xollf zQXn-LEINCQ*vgXsvNAGRK66aJDL?;|a$S=)9Wex*BR zQ|7(q-kU6{9G}pyh1ZdkXrf|4TLi|G04c*h<0GC(chJ@$amK0bOQJDr$;u-K9m5@{ zV33o!e-;VbU#en(QZIL2R&2YabjugI{^|=aEEiWHZ89O}FQr)4(^8Y+pYgYe;`B@S z%0?<9t;c59Gi`jW)7r)KeO*-I5(-J-+0CotpzxmDg`nSVtOj;WuRB6vw=i^ zic>T0uKynrh&eD6q8bbA3-&(Fzd`T?zX~832IO^djZ>*c9NbQX)DHD`8?w2#?g`0j zo0lM|`%bm(Wo;RG{kfJP5A9}H;`PQZNw16F(Y=i4e4iz+d;&~|%10rr4iHR? z{dKCy{Pr>J>LjqpR)@_OV`j~fw$8#R3zCBZ4*ILPqM56T(pC!u@%&P@YFqr_C4%zU z@NPF`w5(8ksPpYnt{ARo*ICaY7c7k(sk$<)gXliZeH%icGZS5wNnjjV3R0qvEY7cF ziO`>T4jh!a*_!c|zf+*wD9b*5@pTi`ETfKr1R^y2@BpC7pI@64Vd-Ub`Ql+Y(;`x7 zq{wkK%k$!A*J%vw+{5!hHg1OX{ZaW+deGJ{>j3zm=KC+6v%C-R$zlXn1;5^TZ?}S* zSz`Sl$5?4;B7okvW?By5u&W>p#?$hXFx6`f$&|KWzdc9FBmW->hoa(J`cWP#c&vDM z2DlK5<3K@3k}MX%25xM7P_?@AB?yjUwomP+>WdNIwpYkz>v;%vsec%!L{kzER#+2IFUlQHBrjK zn3;3v)~jw?W63lCdJtz^ExXR)iA%MTM4AWBjSDO zt7ogYPl`pu$)fj`n-%e{U$wq_j) zl1u>ry$B*Vk)3ObBBD6_VVuGi6PgryHh##MMTSAhSPh|PL>V>;fbgB1ehbSqPo-d= zPhKas^_cVfz@mP-2(r1KF~3PJ|IEFPYLoIfPe6|GYm7EIdWBxk&5kh8Ne%Qw zuLw;w6O1FP4P+1>K>vyr@J_5JbDw3$qz#xkPW#pg3gL{F4+B$&7De z(X;1=CZf+1f+KPvYi}e==}BpWeO(gL&_JWLsn6=8&=f0;d7aRm2JmlH-}Yk#2a-%Q zj|h@&v*Ja9EvR_LddkGp&vy*quP{MgpP^@f(pgNl6 zmrqnTLg7N>FcKOO!31xi_JHw|t0|dAw*?27Fiu@t*`JBu+-+x@%4IBWQc4wBA6oaB zKA%Mnenr*97Q1dhEl|4J^MZa^0TQErv?6aTiy|7!Lk)-nA}*qgT}^sGH-;Xi;iq?c zL0}1+o;mR&BubjiFl%rv!5pe?SrCyEXX{Q41i;CdupKE7w?>&^3Z`R8?l83ITiiwb z=2dm3Q$FH9h9G2UodMd9?SQBaghsq3ygAQpfv%|{nLi!%u11L{y-Jd>@pFFQ5e`8( zgTwQm_lgJt2M#$F($rU(9uwX$>#_AEvyAs0wT$vaF8!uT5`rqN3J8(AIYg}z94Cr? zMDvNDA?fxBJ~FVZ3JWzuVEO!o50bgNOL`~oMi6uOaq<}s)S3QvJ4A7QuByMRFYt#F zMjt>)_Q$WT|0eqUGPrXw<&9diE)O{eY{;z=-_cgbA6=Pg$2!V5nDN)4w1H(dwnKk# z!e06@q;XIoto>Wlo%t%ETxv$^BuW`E!LjpUDZ0N0#h0?1KThnjuEGE| zNI$xckBD!sO;-ifh56l_)36^RJLwxvWZURlKWz{^gIs3$m!5h&X+~u3ZzH~_tg)1g z93}j|-0&Qs$WGMCwz3c-mDSRr{HlcR0CS5G^@H+T1d?el;v6L`j33b%6Gz!YIKSi- z*~2PnPMS>58L;Xh_2=;_U%Bm%5q_L7HTY9eP27l1NafQk(7B){suqma0RI%paur3R5-FNzps!3yj=$f;{pHW zY52`V@#u%*c66TOYepn+s4p2W63#439C*M|b>fJA^lC(Y_x7-Y_9oAH!AtgFaqdDT zu;Nj2YDI}Lt^pY7(_j`XeX{N&S{rX$EB}gvnOUpA@YAY{5}9dCSvSo|Qxf)zvYWD9 zjm}a%747F=#AJ5){pwngYRjt9%8BeTi!BHvg_)i!#H@;`(E8e=Ap4Q5DaKl(M~6CT z6+rtX&W|E1Krv|yjXBHnPjhX@QXCWU9KVH4#m5odN~iDD>?77ZU}Rw&Bn?WD7Kq0j1BExI1^fM)Addc3 z>>vFDm07*~FCN~{C5)1zJ%#PZHTSdoFlVzTO>U|O*yrH5H-pYUOUc`uK6K7&tZLfs z;>l(b8yRH7FIS@b$+tV?ufiMqKO~PcQ%=_DzR6!ojZZDsGIMV+X0!1#2d!c~Uj|)s zKaTxP1={VYn`xezehXc}?kfz(ERdZ!WDhB(pjraW*5sx-;5*k2=Gu|nMI>Q-26Z_| zU`)x?E4i;O$cF~{W1W2-l!`>F(`JyjI^DAxRFd^ifKwl#{@J6$DCtGethB5YrZMNB zDqL&icKKJ8_TbtzLw2t2q$VTaq1%u!vHWP7UU_%1(QN6YBSn1cg@d6`(-v=-Yb8Bw zq7)70IE6?ze^@%P@X6L|jVw5vA%LS-DhQv@(A$$MEIo#u`b^OGyK|qv>R35Jcnqr> z3yI?~MjI)D$@3pA1AO_NxRt_m%qYU4MK=LDc)@yw)^S;A_S8eOTM^Gx#d%=!|p zAp~~hD)2f{^>TQP!X>G$_CI0^#T&o|TN6E$Z2juu)Ya%Rk>-dXDSO|-5}U4j`Xw;Ej5C!06+Nmc+GIbgb08} zHin9kt4tL{e9818Tm&!oK#9OY*~0yVD=<8`hCx}j-6H}A+wbY-4h=w zySb0zw3fEvYX5yU_9A8`dqKS}e7>)tytn%XE^j*=QTKBg&Yj$yuNYcFwMY~i-!wL94q)c!4$Bb#0-7oBqmj~R$qMou6SmjU_ts-meXRDn-e&t9?Mm$32{=-V#GwRP$* zBCdsnO32NLXnVcY%HO05(ko3+K=P#H`6Qula!6NJ{f-Tg&p(tysJGS~BA>nA?>eRh z+V@z3{fNT2dgpQXewd+OIF-uLrH2zHDY291L%+NN(H=NoZbRR!1dq%lcSv%p&uW<+ z-(w`(TDX?mvGp6U-32?IpXwopZA=Nz3oFY;FAE*(iErSo~nmJ+oTK zZ@GpYJfLbfj-6juuS*2pGvi}~EzxiMKr7ExFIAjI=`6RbGolFixh*}DLK(&I=vWcT z%DlyrZcFFS2P~In3$w-n=pTZ9$QotVM`IWvAb;p4xWjTUz$8F!9;WVo_Vz03^MiuL9!fY%~ z!o||t$)j$~XVE~6XV3O40S2QBogMTQ-9XU+l!YH|9ew49SetECX05lS@)$+bS7F*q zQd$z7F8}-$?nNv8=s_Ww{g1z{FXTMwe;w=!Fc<3(!w7-jh&UDa zPo;-xKynxokUt^eoikP99L?|L%;BvU=~0?mQfK$mZXT7HJs`6!ropR)&FEkX?HcvYF@lkmwkOYB4*RbAODET z`jg@dgJz;Veg=8BI2R~;6{TZ<8Yu$$WFSP+U5HRveDOhBr`XzNMWl}j0~FG8FibZ8 z*QV=A9;q&YWSBUNb(h2>sDfIsuoK%cGnX#%C{_H~mPqLTY^S zVUL?nf{seyK+@B6AB;kBU>VF?XZH84ChSG(HO8!S-0nFI-s|XcK5F<{8ZwPHFCfd@ z%~FajITOu(K!gbKnV`?R{msmT&vc>bmd)8sK;T&Zv-R&=2s($0Gs!*u^J424QT86H zd0GdNyntd|gN)B58S4{e1!=PMv))&lJK9OM&FqmK4W}3Q4g61$X^xhRBV6)^gzbKV zNo@tE_gD$+*nd7Yxu^G^#y&Sd94PAjTAPJM;>Pvrv`ym)!A=|Z)q?Hx5N+29oSk0` z_MxTRU^G9UZ_QR5^^5_TUubUgKdd zi*+*m{F&k#%kw6`qgLx5_=38_(r=zFtAz$tR4B+!r!n6r*yV#G(X%x^txQ4rLU;WO zDR)4M;B1n7Vi%v}!S~|}fdPa|0fqWgYEy^~=Hy-B!Cg!S1H9RNoO4M2RJ|tW2{?E{ zL#!!!ED0`yRJ3xzuIbZb{uIkC&5T`|`(>Gr`#z+BZ@B3e0f((n{AE!r!Tzv+Q^LR+ z9+IjBq2mmSdERdp2X+xb>fD~gXRU^a%Oz*-x~Vx{N6QQKb53+Wq-!qdAFYax;wzHdar)|$5xly%x_ZP`)gqhb`alieo=+WC20&nS(UlE;(lA znt;Wf^7r3>U0n1<%xHbH7OKpkLfNi44hSW{{M5ukV=LBGK8ZzhH}_|o3eS3B)5dLr z&fjy%KSY&BUCs7P_4)x0zlHTr5WC>~l3?jd;+ff>2np#l_FPW-#?rg~bB6`Td{QYw zX2AOnrnSod+YVfePTCu(%5#=*tYG)Mq@R$ZQHil*k)ricJiF|{{Egn&KuZ! zK67TzthrW?;QfzRzL$T`8!nn^uW=3Z@A`NSQUgS*p3jKg1T_KtX990W$ZwjCFHnUi zmGFh|C>B&;p_wSF#Se3y(;=iE7@r14s&NR!ndWb-xtqJSDC9|OzKuKf zGnZ%S=3F@1Uogn;=k=&@>RO{qWHGjo-6ClH{&kSyYkg;bvpZ0uB1y+^fDGO&<7t{5 zR{NV@rQ7G!%WlHA_b^8t5j=UdA^Cn4)q44;IhyzR*}-fz8-4rRv%`rvRB=;!wJX(; zC=0T})ZnAg&@jM~oG#B@&n&=WhLO?T8RDGySVgGjalbWvG)>t%TV8=77I4|s&KUBQ z4}73@8D+4GIvK4+J&^{5~ijIOud z6V4Wi8~cil-2ciOjlxSaZ{joX$p%VrO!UEgzz^<2TE~^F|4T&0QGyB$FIL<{5<<~k{g%)gmylqD%}GPn{_CQ@w?A^@lCm}`6u1Sh) z%mg&ME_d<9V<$+FCtAg&wi<9+kk9xeq`t{VTHAj$4b@2BEy;ue3{q7dp>p6}YX4>r zk1KaqL|vDf<1~8s6zjG4H|TdJijRKzB+^e@O*s9M%gy@73I;xfN&Rr5Dep|-Q-ZrX zk`I&V-=X4{Iq{Zy((a-s;kMX_dtV7s*!9tzeeCC5MUMy#eW2_rvxcSzXL)Tj^=GP& zld%`^eiVd=RkJH4NX9AFj75FV>fFj`mlhn*5)XFz_6m=({Cl4A5E$x5UcJY3vV=po?p@SuKhc7Fee5odW-ZE!W(eDU$d(AF9D zc8QWf&yV@B=FH%wMxmQM6y-s$}igdyt576e)_ z;5@1OW8~NrXf@fohfzpIhm%S~x0EV&c->)l9*Dcj9LVN# z##$?78?-8c5=l+=lA=Nf1Ndd6tKBW+B_+JgS{A*O?uEK8zR8}Ku>N++T&?&3Cjw)w ziSpex=XifE5Bw(d>^i7ldV-WAjka&knYy#34O-pd8FUn-*EE$|!tYvqRSZwj5#4sg zxvVZ#TsynUYZ(pr!}IScBY=h<{dc*N5(x#|yV?5=*n9fh8w3@>Mnj4V6*VQjo}g47 zIU!l}IuyUqzX7^SCDrU~3IFNYKKtV0zByf|Z7i8bjFJdDr~(ah|A~zHy8QY`_EBlK zgv>D5BbU<7cUMm9<_2o%EEUu=OB=3@*N$-C^$juC{cFKwH9oFnn~re}2U&|l?P9u{ z)++A2qo2;)G+q+8oH2qYYH=dA6jgGyFuS;iBcr5lg+zNQV0MuWk_uZq&J&{2o(N9L z9xuI0Yf;eAvw4(HB7V)|z3G>D>0!;6>MPpqb{nFz7nPgYfTWB3*wFE_B5dDJ0@93* z4jHNzlH#g+$>>REbI%gnryGRs>b!WXe#!a-#&%7Mg2{3L1NB$mJ==(HGc6(9DM6>y zxjzcFPFeUbtrJHzwFbR&E$6+&315=o_Bj|lt|v57*Fn)K1il)OnCx>XoO*^30GMC1BPz+3V92CT7x@eP3VuJ+${0kN(37L>XK`C@oQOd#l6*++2W`pL%3 z|NX7X^}5R@cETCsj5SrJ2JTzT@#xM6ecZL(6XmEcpyr%i_!nO_ku%elNVT#v;-b$5 zdBOtsRb$6vcEjY5*tUx0GSuG6oBFqEnG5Q|ri`STEV&Kl(%j!Divoc>4|3TfbBoG747Q(c!L#>+8X!qX;-51EEZ-`UXY;>I|w>ecRVA~x*i zQ=!C^`@hgaoJ|A}AtbG`7OFd(uoA_toF*vpmVWsb-sK)n=!5%cn7?;Z2MU+)_TI)Mci+Obl9?J}$$eEuc+a5+gRV&`}o=-k23 z6l74W6oMym>EDkJeiR=JF~^khrwkdiADA-8hA@$VH8c{(n~8*0kYC{>fH zvcpw6>#uml#?|Eug}Nco;Y|>7E;{~+CP~|s3tEMtSwxtZTH%rDrA0{jD&y$jDl3hz zASJ$Ce#gT~vO8R5>95nfapnPoF0;{^hP-k zpsT&rHA9SelP?x3IT#@nN-2 zSX5kZFeiafQ>%zU++EA#5_N{XpbO#Xq7q=k?gu*`I&KPiK}JrG^J3O*R>MJI0vrDO zUN3>50Qz^;!MqRi_~smdcjiA?cNK8CflCOOSW>?uzan<%H(VP4gC6{6%M%173VSWLQT$0I{!>}}d?#g(qe@X1&LmbKEU^$$$%qT2HBMd;JQ#8B1QE+OAT;I= z#>aq&*T@+>lDz8W369+-CY8=c;Dv}sRaBhaI zXTMpfJ&?$hlpLqDY+ORf-LNzm`PM{LBjaH-2rZd&i*^ow_djYBs%VM|0DN@}cEYG% zu+3hK-=wCA53p31VJy&#&}_maIx|c{cWh0GSNvS;tVUs+(V_+wRk3<%yRjxo2GUDT4u1jfKoT4UVRXqUj^F{g&jkZ6vi=jL5k-cB5K28B{2e8D|Fa(< zWdT}l!hp-n39Erpc-zKv=0pa1g?+#o5%k}_JRsd+mNET)CXGcBp&)fZRt)1^vY-J$ zIsZyb_E_@xCjYMCTWRDQW}EdF>0`1mkuNWZ5eEKCYOqnkhvRo!Nk)4~@EH%s!G+co zw@q~GR$WzzStYYVMf#r_^Z3GFWsOPwspNFXs*wXl;l|KuGD%7^CmP3T$iHP17u*(TIHiwjKj`*f_FnheBc1>Sn zIL*h3c4K;JzPinGU`CbQp{$_JPn>NbVZYXNtNivp@u419$ttu^aOo74MDn{`j}SvWe$^2_6V3fQbV$%ulrhhI?7f z>Ps&$+(FDhFAz3xhR~UzQ^B=QBs?>{H;wrQzrFwwb(qJv+yNrS0jAhs1W<@GzS%QS7tbVcvx!?e*(YH!jQrzG zYgTr3FEsIyj?wKO(20S!r_X<8T~v*UT(7frRr#x^l1yZ9o?8fc*&x@weq8ao?&fL_ zh*vJBn6Dn=Sfn?1^p!6TjKJ4j#QUIiyxDM_Q|)$!QrqPbVg3>{vcabH3s;VcUw4R2 zFt<*QkQN}8P-@&<+hVJE^Z{FhzO%C-p$%;5grBdsEIP_A1DzNpD>QqXRHz|dVfYx# zc9vGYw)EEl9pYP^A%!Q#55l$3Gs<$pl>hl`mwst7Zr>&MJ_IF0#SqQG1KL|>)yi=J zb|-9wW|o(yIt4z!8CGP~2EH1GHfW{+TMeNo1s+`h8ym4;A04U??zR@#3;6Zlteuk{ zNUQ$0&Ld!rd^`36q=*>+-#h?hWR|+BxdU?TlJSvNW)uk{ALiKW&nJ55PngfM4e{yh z!jV>3Pl%|5JY+N_fdCN&PtTVF_vMDrccG8uLjGTIZ*`&Gvp2G7V~v}QqG6Qb<=>lDIF*?N%?c-%G|*F|SlgZeC^MAKzJWwnW^%h6TB$*8~J{UAl{ z*TX^2Paq3Wm#WjqbCf~C9i7(l&_X01FhrRW$QKG+vIOYyHm03SmyxEs^w-ALF+$T7 zM_WBgYCTH3YzR9=hjw@H1*-36j)CuSS1tw8|BQe-g#Sa&$%Z>*Jd1j;MCh9%KBZv} z2Rd`C(mPy#q-*+X-w*Xy8;!&#s*YTyycdPKdbeM##w7#`eSjEB(LV%a)q(;Vc5I|V zs?^);-)>oz6c*-%O)Uh)g@q_<97%!yfibAq zTPX;hxL-TWFmb%3{8fo9;DH%%q9INZPQ@%UT>I1!3#pz7Q*?~6emd+oX@EAM& zw~HL4&3yKG#zV5j#rq4`7LlhNhkXWw>&3Kf-N*TEu$30+*6_jkCTnW*!oo|N_-;)j zqR!F&v}#M}FtR6(g-$$>YY-V;foZc2B{`H2$5K#U@SoT$Df0g{+=rZ`B&>t z6(e8bcN+gQ2UwL@{UjxcAYvx(^d?MY7lsnMjiMT-`w3|YuJELitcLl`yI^n@xaRM; zIP#ce&oyxYmNJ53P+9JMmPYB$YWd-k_Q{~cQqE#YU*{Yv!hC$FZ} zz2o(#zA9~FIeT~3FGurdwix=OtceM&-nOPnFk{J!a$ zhz%dA4V(-5pya&le6p}sIM^sCK_Gn7i(wz2Jv=5o%YsjzWKW&@O3Sjxd9nVS-ex*kG>zFQ&OPBz-rlOq`?%(KLziZ-=p{`&CPtmRrf z(DV$jCQeF@;+c{A@--pC4isQgrFYdL`1;qD69t!_+37=T4cbL?*vO ze?_RWtT7zVN8*RoQXz|9XtYfgv4`#S>*D&uXx1MnWZ*}gDD83Uu&QKAQ#=dms(1ke z35)l2^gzf+LuYX0l#kVPS$kY2>p5k6YDrpkWim9~)qeQ~1|PY(`0ZmCo@)1DY*5yc zy?j@!qNF+KE9`{@Gr`bFRxbl%4g(FFpqP0YH619{^QG(SiF?e1ZyUdkke)$C+o9)( zYqSWQE?5kMCoM_++DBX1rpzf5?hA<{698=7=^^hX(M4- zJD1l1zSts?Ot~FeXrhlGv$}T<+qu!yUo4HFF)yYHaZ^5a-UoqDmpZnm?(hpdaI(pXUwx|-{59>s zJVXra@!8x^>jC`>;h5)VrYJ`1=pq2=Qr-%kfL`%DFGlE=={WDgM~TyOVa%3E-!nIX zx@7|(W9Vwk#!k;f&&0?G02y`A$@|4>6zX;3rPl-WCRG8Z6F~SEi9=$cc zP_n|OYU!oV9Sw@NC*R5we*f-_-kaHBSt=sv&?$*r;)Gvgxx?a3e~9`0ryCCVSJ>_( zLiWYEsa+6C)HO?!eo_akA%v$xJ45qj9rLOVx>H}Tu9=~fTUU|`H5<)rB=OW)dlp;W zQrKHHwXP@Thu43|Hd&u*=WAiC4OOI^ijB_h=X_(pm&Rs@`h6wAx1gxdh9Hp56@+$o zmaHRnQp<-0rjf^3usThah}L$zDXWv-yvQ*)?q(q?qtaNl-YoekO=km&Y$%x9E||x5 z0~xC;h0FI0)aFV^I*J~u9tIF68q;iEF(@gvXOr>GW3~~%&_Yh(F5(# zpZbnlGJ15@)jsR8%of2OpKV=^V5N@bl2Ja$04yWUNJ`lYBMw=gi%lVA+N&e;pOJou zz>voS7W03fQFMV(iZpx!|5V}Be=p@NX1>SiQdjVew0;ccVX}|EH)g>||D5>QRr5s!LFIh}D2ORcX)4GRW- z!|HFAdg|qH;sjstVXD_RmwT5V>(ia`N+AANZdo^DlHO*lF%VcwDBD&!q;DF#y+wWh zVS{>{h#>KQXjGuBAR`)EATQ8u9cV$#+zaeS77A&Xwk?7OZnqzH6czsVvZk2y?c0*R zUVIOMafwkiK5m8~0+OQ-{GlU^QH-5REd^0dyG_X+9-j^qK4nt=C{R2ZT`ltS9!Z%b zM2hCug^Cz9{Cw5MBCvSnFu4A~dR~gQh$|j{eh1ZoVK-A+F8VbZ z7J?0DmfaG@yp>kUV4@LMbzyuKQ;3Bc3x{x6Rs-MYEdn&H9Ry54GV&*To_XRf)0Ddp zW8&(`T-eEMULKKNrVLd6R79-63Iz6_UbWuo(Y%v9tOTB+#kKPoS#)=ib4#sX9i(|f+p~Z*U zMUtnY&0!tYanS6l(6y;qy2n==e%9eJxIPyz& z7+HWpS7#`%r9pYuJ+|l8rE_aqeTr~~pSZLNsD=9#_wl|O5I@iLg zMofDFvQ^Yq#LL>nM!{eIr3v678dGqn5vBOL+5dIW!Cu+_yd^i2<-2pr1_)z+!TmEB zGHiF5FaCz=I}aT*IW4fm-KkYzodsDCWFbJTW{dbadXwWF_q_Y=0qq-W{Cp7m^7}BO zA^eFH<@Pfzn@O`z^!Ij9nT2$g7$YdmaTAI%YIMfxwFvWQch*yiDq?D(z%ccZ2{*#4l%_qD z-m77j(Q!-fTPvvNX_QTk7^UO_|rJFQMZw~LWfou_Gb^obJMJS86>&L4K; zJE%(Ql`d{)W`1(_LbJ~BCX=}69dVMb<|gU2JH6y$Vgivppgq7CmkaAvr4h8~+-=8(vNcyl zI?Lcoz;DdDWm-6FRR35!Xx4B@S@$Njs6y2jYuI6A!5=lywl;%+Z~p_k1CfQ$UvTR zXc80&p*<1Lkq}PR5J6=gK&FbuNSzGxc-9gJMwKq0Dr+#Xm*(iIMQUKF_pLvn&J|1{ z)Fmuvr_P$(41Vv+G#in1CdWu4`B-pAUX*()@Gq_!nH(1AvcdI?;Bx8d{x^l>K5m$u z;hEhdG({J|@FkrZ`RaaVaxvV#ePY+^2QVP5Y>b%fCVsd@zc_8Z&m@QJ`7>zv`a|d3 zCVjM#zCWL8_b|waGqZ|;|6X2vbHRIY4Xdrz`PMff%bP-BdN3DqH&h$HgnK@dK&aUM zs7S$*MA}vea$oohJ`ypA94LB^zJu2p-Q&{|D^@f~K*V1&ZMzQ58kjc*N(Aiknxs_?|0}5Rmw=g}`b2}f*iJUty0vfda86&QtfBdQ#X(g4a zR_jQ*00INOcOPHTFYB`EE`YUYHdDs5#H3DnQ1_MC1xx#@JQ>z?FJ3IjKkg%1UQSOT4EM4oZU6mk zSSEk#xFd=;mGx<5X`b5^bUxZrmf@F_E|t_}GDe=w?lMG5X6{6~kWMmO zh&^VY0yvC;%%G*%8q$|NkJ=RGZ=OVlorLn>~>r^I`Hh+h9|Y5gty*aCt<7-PSq=EOZEJoxMa zl|cY*xGb;f2`;TdhcDKhiTj8lY#fmJNQX#aRFP;#w>(EDATm}41tS&Bk`J(SRo~JEKxI@r18dNCO?&_ zmA{`VY6!xn)f)(Vl4T-Azc^Tv_TagRMOuKRNm=Y_x>3)VDcvY&OJ!!Rkmt7D2CD=K z%%I9Nh9czPG7a$Es1yf#W3s<#mGw;|_!A-f*^^F8Ft2lKAnB;l0cT(f_^Q!bePi^lwpkTZa}x-J9W7!^>Q zIh&BuqoQKobv-8)iFu(_7Z;e1>PJgDE5a*kZn@Qxrm{Y&zSQA7)!u;5XV;S;!(cn{ zHdpm%XNxbs&Z*82OPyoN-7eX(93Hbvld;xPG7kFF)$fVXcw4SZ@dV%2y@y>rb?67G+#3#??(_B^iJ7YX6Dgq$jeNEYV30P5v>Cnj^~s*(z`v z`bn4H{q?q}?n>)otWVx%-F3&qar@`%+g9jq6zf^;O}smYZ0OYLvh*l!`#}x!%!l4s z{_CA6`|58!)Ww&zP!%8MX{Sxz>Cs_kt}x8X38a*8(3_f&tsLy%InBQ^xaJd@zaNl= z>-o@G!k^uSMnl^Pz=3b&D4LoMx0>fVr#Q${RV~_Jd zSERqDUI2&%ymW21q;eaQgBAN55<%XU!2n-S{+=|3bNqLuFfZ%7XububhWZM1wOQz( z!oW-8I#B7t7q65R`NL89cM$&J!kiu7!Fzs|g=9%GRPy+#uY!#9VMr_33cry z751&wn6{$_)$Fu#Qo-D)_@?_aO{R@$loqnXd-NsrMa0q>#Uv0S;Vv*ev^0s$Y5SA@ zhvnKNTyP%4In>3-0(C5cp-0MKW>ll2E>i44C|fU~3g;IB^M^?e*oR^M#QU;j@hIf9 z-MZHIgqS0X=F+;!n1Qt%mg}@P%Q@rKT?uDQk7ERV|BQFeO7N^X`tbZDX| z76l#M!WYJitfqG)PaNtStQ|J5%1>!BgsS@pdB3Ds)eA0S zd+PR%%*56GDo1fHbh1?oHU&KS9pKkMk3*bOHW&5jMCR z<+$-oJj_a<-+z9Xnefk~y{SX{zvX1o0bgM*AH>mvMRg!Y?g=Q6pfL+s}*eRR9{VDCauECqL?blS8 zswO^kuW2l_Q5szEY;wODp}|n8&$yO&e>z-mghcGMj@o5);vGqDW=4J(xo9S-3y?Jz*$yJLPiD*%?r1=^7)uVo&Z51KK&}!axQfX25-NwHW zcI>z2@#?ynWV_-v@r*G~9@IKT>ed7ehyRo$(|2ahQj|vy7WSktqgkxZUuZ$`gV|!> zE!r@!5TgGl+C&fvF-Vp35yz8H$zcSZApl3buH$0{aqi=I_T32Yam|D;+gG}t|Ip|| z*~I749dz=by`uOzUKuJ!@?hLIhh*>r5&4_5#7uDbV6|U|F#>s^K z$;l<}p`GSFt+=GsdU{rBW-ZMjMA5a%ql*6ay3ujf5rs5+Vyt17(u`FG5-GOGwmY$_ z%yJae7Oy6m?1Hc%AQtTRJVF(8S?tgG!6j;c?{4nUQFbKiG;{FO*uAPj^0^rw4_3)+ zSv?C#+nGB!KiX){WoE%WBXYA8DP2_iIj z%Tm4?xyxj1(?2EyV zyzl;H6Hsz4BDAo02`OS@6hZ2K02MvapURYxo=X||H!^}bubgQ+zSY;0ovWQ z^DzCf@JT%*sCA8g#x_ru+pXbHmo)cf!^?w<<3x6xanfXPLZyj&A`hqUlBilZ7ZU9D zguP<(<2w77&K3VFV!};J#r1pX0Q1;%jxrz3 zw3&emgPhYgi-qS||LmNMeGBCa;%d%Ujb-v@mv>6Utb)?}CT2-*?~dSSw@5EOQMmZM zPH)c9xbF34`L1hfwH%FMx$GKG+$ATZTo1vr_6QIt6PC?ZLLZ7ySh%s53n&U=UQm`F8#jYtrmh*y1X53dLI zIng}@jXFz)Y=~0XRNP`AnK*~zieM-1`VB#vQbdK6!scWV?hPsRE1h+)V9^*C56C7+aR#89BI> zXY+Rd|NG}}U|M=f-f3Y+$&?aI=qFhLfv1lR^6ZdPQGfzw6mA*ey zLynYRVUhk|mOC7j!Vn1&r^K^J6a~gR63Mg@>(+wzfNKl+o{Pwy z$D|n*hs)sPO9JkK)uqAjpF0z-8bg*BFovKp;C*eEQ~hd`_qpO`}Owch*p zLG7tR5onmI0BM&j+P;$0=tOl8Ab%zjkRQ6(Sv+QuQCfRles8HzKvxvnsP+vnvf z%gVg)0?DcS)#c=3q|9x%OW+slx-xOL7pkIX13u*KPXmqvPUQYjy{?La6}2OT zS^&1nOIJXC#OI|Tyc<-DYjWAk;&^TC>A3lQO@b?MI>*va%h=F)uH-t2-gG`<%2`NX zpeL`kFWKfL8DpI?OC>q^+c8#9v~a;z^uVTlV$dG?Yby9s{?C%+au)G>2^35dPx0U#I=zWj=sVY9=vLPi$#`Yw(4a+eRccv z_*ec$Q5oODqaxPm^QC24*3j9x4IajVNl1`gt6+?FELBwMYzS%+b!03PVIeVNDGE%$3H_PnpCs;r z7+n}EoX%V?VfI0Xbocm#2mQ&ye`X@S*`3|%=+-n(!z~K*CQQ$#HoejejnThmBIx4p zH3)gB;@2MJN6C_^J${G!GEysvWg>whgr81L*(kwhb?DOEba+V;Bnn)BCE~RGp6P<% z!n(aH71g-)E#o-pdt$l^D^cc{CHRU_idz9@lZO7x99jG7R083TN@H~hBtef5x26jU z>O(`iA;FN0te@`3LR#>x`THY25q+S-0(VB@m4!j*Q%*MqqPfh>H?i*dD;Ah1y%KZ5 zI?MT+!;-Q|gPbw4Ig;sE@EI7l1Ct??M|iLvMHl;PTkM;v>SS6OeZ$&X2`lDKj%e8i zLM63ZZFt~*r6Wsy5?j@wrr>u(<4TW|X1g!FZ+yuvUV2~!n8iW|gfi*K;mcSj(fxiZ901;tQ!Tkjk z#?jouSn?qQVqN`7((YAX{CMA!QEgDS z8wP(IS6^H9E#`pgN!j=5u|p5%rbW%yW$~u%x;-K~O`A@Qx1H17svn9+Uv+G#p~W;L zNNxLD*QDMw1)sY7>%j7n`RroLx=T4O`T1GbNvCj#Y04iNTEAFUD*hN1*yo1b2FBFb z>W^Clkqil9(wr8FNOcT$SuzrfQ3DLisaGun;O542fFZUcu7$bq9Bxr#4879@ioSeLu^|5~`X`;Euaj4b@{O#&bv&v>o=JQ@G1&&e)C`fBUELRpcC@1~-bggR8jFi%M!3}QKv z5x-KD$ox}wtx#QJEsSFD(xeoSOnN~IlDD+S_%l0}86GET|knu@7O!7@Y z$asQvYHU*{!Ga5+=27O!S*mE6#E87!;6|~lbS-Q)unnJ-tbHSBGSzLvmZ!M9m8hrJ z)z%jp3d~idCy8$nnG_+=>({v6yQ+(M$5wq728T)sR3wqo#mIDXM5tx|#Nl zQx@)GNyf)kj=uwP;`d($|2qJp3N|Fj{M$a<7I5kXhBbINGZ__0YS=GHqVejm1OW66 zgMXpV_fJ!1f_=jeA8@`(_x$tSMNMKT5IXP3&LV|h8yBZl(AZT_{(l__@sW<)& z%`LqC(4E|%w@(Qp0#nOV9|+qKJN9OZ@4>x#&CV+6Nz1BQ_H0|N>%_T0@S)&a+A6A! z#^D%KEJ5|pc2o@1<~VE=*+YP@nBUu3BQ~ggf%;OGI=CUEJ&UnuX}Tj=&iY-y``DOL zMVTgA6e})`_ChijL>4fVBqiB$Ll%v(SLaRDC|y4JQH)pEY#Cm=$bhVNaeMFL@@JFO z6*FTqqHjX?9wyVUr$NQ*jr+{*tX*&-`LSj-P1xv=ecW-sUG7)F8Cs-RP&I1OTkcY0 zOo2T`#DRM3=|b0}rO$?0X^PBzJLc*&8a9>MhO!G!govnuoIaB`syQ=p8*M)1DC`jW z*BHeJi;=5-4_KJF zd3u0`Jxl{4D@u&4;HR=Ck)xh+5IE_F1Fp0&mSr` zAaaLaC`(P4h>C~npo!qF(lbxe`Q$3T%kfx8^j_gx?v5DSo!r3Iu!$!URf--^zzufQ zP}|+{TgP~ycB6=WbT&L`bYCt!VJx#|u+5CVj=iH(_(H`Jxss5nGP=-ZNn zn>bmR2p)utEsj-iU^zQl1~{Tf1}jxLH9X-4L!By-hS;4%J%tw9A~Rq^k(iib%urMy zKC_k=3YHn4@slFWz5)JB)o7`=^uanP5Tqyspj}R>mr_k z)=5^c9$lN@j)%^+!zxR8T}%8|^-N==m0AT3Lc?rf4tx3G7bK_Y~SG(s?@`c!hhnmh{P%YULB;7DNwI)qVri)zm?7ll|-x5Z;T0h zN)st08_?#02(bzof}O-d1!6!kOG{w%}3n|vOP^)DQFQD`9|t_^PRzK z1gVW>{_$r_st3BVg>YSQM$1A)2UO+S+fnHdPXt)Ic3JDBoqyt9J6bSI>x>4p%i7y7 z(PHR&;ow`^<>QP_MWTzgyfna9VD0xWUH|QU|8o@-Kj*E{FF5o2dmTGH!IgpxkV8D3 zCSF7^566L@pj)71AA|I@(%0Q-R;6}Yu_{>5-~_jXf(QHVf~0SzWDTE?ef$MEtAsgF z_r?4?C_U8?+7a3H46c&7`(J`ib?H|t4wr9mJ^S0CIss*Ay*#U=#u6s44**gBLC348 z{J{w(MZ-YT8E2P9W)h*{;#1n<93wB;T_u@9ir7g#2c~&JP?;eC$}}DTl}P>xzEX>qU7Vu$ge%+E?2q0XGThp%$o7&xE5@rb zx`a%vn@SyZ1}ZUb>QfOgg{hthE2Ag3+|;7EgnJVkYwrnVJ-{E^uL*dWogSnT5lUxp z4hS$rx`Y);h&7X~NR{B|I)o|7n9+u7yVUr_hBWK-G-He4E@_0wIDM%?9RV!idJq;u z323ld0#1zS2tFi2SO^6kCH}&heh9t)dv^+OT4KTXE5u~WZvYh!z!S$_Ar+9x&RrN6IGD9)mNJ3`d7I;(qbEJl4KTTSujvB!YleL zM4CfnF;o;K+lv$C;1WTRu?Vz8npx9m#Z?>5Y|ZsU7v_!l*-O=afu`+f?v&AnT=hoP zV%+N_V)q~k$Rh|BDzAoy1fJau!LIoc!8#YWOF!L}xY#@>YcRan) z*D}vi*>IKna`TQR}1ppp^LWYou%i7{V_A^89Zw>8F(81qF8S+x{sSD1J@-F6g zoYAfg--?~7Wcux+I7CM`??F=@5fA|CHh_9rM z-KTHqA?zN5k0fDOeoE~$Qm(*$l5w2+s0R!ChKI({jF>Q5OUV3&h&Y-n&hNaA7PBRT z4nv_lKJe@wiN^~Z=Y?(_AL}Aa1JEqp&1$N4FqfCq zocO`GHSaP@nl$);^hAut5i+Qgg;c-`H^9xMwDC42fPB_H-6#~3`Q4fi|7gFP_v!d+ z#f0%bF%25k9>_D7S%5I%C>?=z&b%pVkcxhwVYN5^<2Ot?VTfMGZELoMKoG04NtQfBbDa{s&JE1*HsGH@oziUIIor!29uLDb$o$ z%OuNR28D9eVfv>CpX#R-3jdzlM@XSh%JcBWE;sdd3Z+P&u7%)KX|xq%+-bKN$-~*x zP3))i=N}WIC*bZe1@AFu(^|g_G7?g(|3}V{7b2U-G@G@qP2!ctR#f7V8n$a@`S@6P z*}9sO8hu)vz-*q&jP{Vf`;JsNbdqragkpv}AbFHpbSo=5P^}2zp@4nJgS{`y`d;P_ z8)xnq=&ma<%rtQ*E`B``PF!w=Asu%CyKj2gUsqv8gLBtCTZciE7?mw|0+Z3QzAC#WL6ox3;^cru% z7o9#K5BKP-pA2ZCcm6-jv>HhOjtdDy5THSh{ICintiNd-{hKX=l2DN(0!u?EVigqF z-9Ts*o#{=H?g(Gie&65PA0{Fn%g;h1`z?7GU+RNsSqSRG;Q^d?2D$f0=(h`~AK8Mbax#N=v>) zt<7XHc*5hX0KR2Oo74-_H^eyy5^TFtuJX@WqV2N+`-nq`3jV-a7Wdt~bIZY)mt1v_lQ85`*AWdtjvjMcRCW(F z*MBw>WmgB!MhMvn`xxvA=?pE;{=9sdQ0GC$tNmY`GvXn~fQfO+0EMzZzSWQ)FwmjI zK3_s=_Io|JH>HTZ<);rmravi|^-oYfFa~>IeIU!)ruqnjO&*cJrJ_(`-G$CU=w+|! z^}l`AwDDiReDXT_YQ4=XUu)07N{rpjiO{vfSL&0!Vq!fng$>rOo*}f=KxhuIu@RSx zNml0uvG*g1QQxY^8aXhi&3VL1L^#(caIU7$aW0}Yx2)TxQg>93z3d)Fp)*<~XGp!W zU$t^`!}&RIq}WzroDHb9;`y*W%3#8dr9k7&cIFwZWWQRN-Zy~mO-I{> z25_S3W|1(6JbSB<`LNC4Q97cz7^-D>2;!>fy(V+{7zAb!;4FYWh9CX3D?!{VWzoF& zlE|HhUe&_o2loZYjP5+Qd(F&22>j{EN0&<4 zEVhm^4p=m#`>WLrnIgiuLh=>Q&N&$|_*Q5?o@Q6whCE-Nn>1Y=ES;$YM zq@=K6-yB4ygF98F16~}==mHl;d)K4~umZ`F$f8I$6yhhIWD*@$<+zzr>Dph4`zzE& zRT4@$oED_aAX3g+4sa?v!$eZnS!3F%<2IqSlZo!PdDYJyd_L+vZ7#c##xLzD z9hgw+g!;nOTF}MU;Mr8~gBa3nE|Jj>Oq5-QuX!^X_k3$C)nbMc0>OH8Kll7*yAPEa z+5=8G1A(t=XOwD$BzBox7dI&*Dn0Oxipc=V+3$a;d;y5D4vbJp6+h==^KCfun*u5) z03hj|>}pWZ(s*0nY~Ypf8vUYG`|Hsrf2G$iY+s3x- z6E$vZ+qR7+jjhJEZ8TPs##$e&4^ZY%)~vP09hBlh&yYO# zsX$^uU|WjxZAGBk1KHyx6=dZ1@yUJ1L5^QW6G9%}+*}Sc`?$m6I+hK|#u<4@S*`j= zy3`<(a>F=;VBOd^Wuv0_K&3#Uf@?Gvf2ur|kj!s5g&QNgpd7>EKjacI<#a}^%6mG3 z@5GhodceO+XOnVPL4;1XVN#<)rr4#CkS2>YLYRQ<()`4eE`tUf-w%^eQErrm!?HbI z8G#RzS+Prn@KS4p1Ri1>%Z^(G8HoZ&VM3%Piu84j$syBZSh|uE`ZgEp7RwQtTW2kK zqpZHL5DB2X2HMQQk=aJzOYM2E_hJtR95c0t(>UAW2fx*dWfL^rb3l)d@Bos9)(fkU z^TvZZ>haOCxSlphx|VO24#d_>KG9c0{mb-AyWDbG2-T;OC)V_qygyIv`5!3*qKXbf*VvOg(Xq zNIe{qI)(P}4og)NlgF?kuR+R!&E6Y0%bOOjoPq_k-0$Ah7>o=RKuM}26~!=p9d zZX#gUvOBSWjSqb?TVg=e`A9& zC*gziEGeMb4zvMvZxoa!3#L#v?_R+0Qr1WF7A>eUt^Br2wGgFD!_a7Dw@L&`+ zTKZfvvmB#l&(ZY1>*lxGNlS_C5vrD}73mkAI(j6yHp$oY|?^C--1FH zl>BR+1ssg<$zXcLnC5AxYF{f4o@#*s2Q?3zL^S@wNkG!IuRAQPm-#E<_2MOl>Z93j ztWIEH36S!levvE}kQ1a1McZu}Xmt%i;%!Dc*ws@aaSeEnhq+VG-l)Z)(GqkGx7+>D zSETr0ggE4Psk^}O4)){RI!p1UNQ>UYULrc}TH{b#b=1if84)MOSKCWc$dTBgOq_Yh z__i2v3%lWmrqD9CLXcgQnD}a|vNcrK;uyjEu6}V(SwFXp&(M{4Fo{-xfe#1CAXTSfp*Y1W+PoTN5O%_8WK?}0lu0t8;HBC?H&}F!J=xuK-V$HHgK^0 z=eRm!H5H#mnmUt@i!Odq6uV0(m~3T>(ul3Fr>T~beH-C~M@;D^Vng}&Pxz$@z_FI; z(ao+t83&)nA@(4zj414oxXPKmj=X~c3xU(6$4y2|n8ube97#%=VEE`ZgroAwJS}8XU0l8dTonS+4IbU-GNk9O21GO&s@tg87gZyQm1t_Jg<3y zP1m}wW>%jl>zCceWyQ$_Q#UXALML~B19pLw6;=Hnej-Mtl!!*R=LdKF0?|auqf(D@ zgm>os^Bd30@2a(JbmgcKYRcGnl^C+`tUnWVD0dvc^SK6)nF)M}f<{_#MF?RZfW@N) z@zh>`Ps7=NimQ+v_s~P{aA2umHXPlE8fqF0k9dZ)du1T4g6*0s*Jhm3s7z0zrcQPv zn1Cb0N`HV_$^$+8{~!QBM7S}ITCUMUA8;Yfkp~PlEP|;xix4PkBo8rJuLXpLuD5^R~dW@N!!w%Wt2*HuXW2ce$_n!)=LgPcue ze5*CoJ-=lKD%s#_H(;j7I1Aov(fd4N<7Eo!jI|Wi?Bidr1^*cd%~gvmbkWEjb6x!- z{NVl*Ed!}ane~nLyO3WgYu`L@wrXfX-Nm#7Z11xbZ z@x*cH^P6Av3Uc>W1g*ST?NI@fsN^6SijVdH@_?;A%ZJ@q_?}X$8ltw#Nre^OqIhF7 z_ix8ldz%$v|Ci+5UtqTBAch{&vBNQsOr0?S3gwF);VJ8>11xF zNB4H-aX6SxQ*JTyFg3E4)QHsXD!C5+XDX(up`OoC=5R|@$GAaRoCyN?*G&$@ZiOL( zYVx;{EbgfHFj&QL94Cu9>(ok12AX$bD$}RF;*WdfnREd|T-)i|{3SgjjhhYd(G4FVK?*`cT^GEhb zPgvf`7Nn4#)!W$bFdKgrD-sB2JF8<3rV%iWSWz6ZS*S`YS*Oj55gtD}rl`jC(W4#i zL`=XCTb&{lP+cK}ge!{k6i5YN$k=%XG0qdYl1mK3Q|MtU$AXcWG2x18ih3%qv4;MD zdSCwqF4>R==33Ol+X|TA42E|q68usEz`b}9RznOtq7ajRQ5>+s7%s2I{dyKWxXnfJ zjowbYPGlAYPTr<#CXPCcG~L4x5siZn>?N}q3Rx!+fDOJk`L*k}mw|kZSIn`mDEZvv zlb;ITI_nP`zs{xo$H>^-{V!%^n`a)u4QequQ^m{cSFtr-;%#1+yF`^xwIDp(|Ff=R zqTqr64z6*Az;zu^R}0(}jS2#b(zkt}+q?xWIu&XQZCb9?Y0<~0FHp5oop|$0<|e%V z@LT$55svYmZx3CheAsqYZKQhUJ+$Ua-r1+E)ErDy#F_@Gs;d3#_fN>Fhf>#GwC z`;PTD7x@=zFBRzMns<*gW(0@nAQV_sl?h#f2B{jHpOHWeG>2g!$+E2+E!uSh5$-W= z)q|;1XS0RyWsJxn)(T5;6%(S-Mxuo{#^12Y`fOHDj#+Y3>Z$ z`9P>~h>I&Oq?O&ET8bkiw-MN3?o>FKIqLCQsHYA%!c~29@xb*`vybWgY(GBm8E6f2 zeHMF&i+64ta!I%8Yay$)xT+xeGP4~?5|nv7J&DvGTZ5$gg8xp?%mb;5QDGo?%alO5 z{$IY41BO2^$th4#QKdjy)gb(>?&G<{AjEEd#VEWH7x5GM89U-Js_k`)I@X8zi^ds$ zVT&3Kax5G9gN%s&y7Af#_^1|`k7w*Zl1gq9&yNK-N(Y|uFaI2|MjwAF{e8Q(6u++x z)zm%LyII4!n7PnDx8vRzJ{kO!EVL$LzR1-y(qpOKiq-(_rRPKjPT9 zPa0^IqD!yB6Q^GI7#7y%dLMAa&gmYvS}!1qMC;+B5{)vp(J-5{W@)0)E^P&~B2{My zJg%T(m4IV`sdw}!Jx(dZZ z@IUYX@Z=_G6wp%m1$psrY2N}SIEc^71tbLR0J}#RgqbwZy!Kg$xLadaHuUAq_c-p& z@`74EyQIv_5GA~k6(l^)R8YX4-2bUvqAz#5lJqtCqhFLa^Ypd8*L`=xG;cHh9nc2L z#AOlBHcn2~&F-Pk8|e*unyX4f*0}DeY%(?7z$q%>T^^Yqlvo_dthk9lT`M+wNOmi_ zUyM^=o`diOUYcNGK#eui75k}jyi|2O(!F#U^u(Ikw5Xp26eW2572i4I&%>uI#s+s zC&<>Kgrgd-^&W0Qjb%9(ENs$f^c};3y34>Xk6z$ zrYjHz6{(o|;eGg{4IOjqw@7i}+4H zKyC#Vv0v&?pUO{Uy^1E(&t>tDT!4}cvO}Gq9chVXg4pfI?6^;34kFU06~gj)XFh-+ zzkUF1wK>J8HZ^7#ug-w0`o^*rmC6EFM0|Y_);zngYm=CHb2EGELft0j-VDt7I4_gN z0u8%~3>c(v_OY$ubd^+ZI+YMCO}_8`lmy)F6^kQM1}4U>%{h|@w=s1~FvY1@4Q8^= zSU0Lt(RhithS1|Om*qT$%DuYZAxbg(~m(xJR5s0%#D5Yt>#fA@^ z|GBl-G6ZHdOc+EE^6B1=BI(cwmoh}E{CzxQUDw!g?hA!qIE_HhRqz*nn@mc@)xC6Z zi-<5Nc@71XK5t#pz0Ye|)DM);W!c#5cRl|t!{Uo>PhZl|XK@W^Yn!(bvo>A#b2dw#i)nLHzZF>oO`{>j%-=tktS#1WRWU{l>v@O=Yu4!!(peCn`YehzQ$ zL)qt=zwtkubl~C+5D^5DKQD4$2<#gFX#x-)(%%w(Q>qdnQ9Qwn+~wLOz8Jjoj`*d0 zeKtdT&D^))i9gRuJNbY(fjgm}AC5*5*7F;n=)C;|XYo5NdG|zrlX8)sDmaI!hS;S- z7$;7z`=J?Fg5Cdy4S%%Gz+*R{`+cfL9UtSuI=Ox|Uu|(JF4Nl*?(~uJtHkU}qmF#Q_VT!J6ZAml= zDljI#732E`hMfub#(KLVGj&m4TF{Q38xnih*Ja1?`*^h68}3o+jr!9T<7DUecP+gO zdiJ}tkb*2{{8F-JQxA$ni9qSWV3`gKgbR7VFVASb{6(fl6=hhIoO<4V@K#@CQ_-Bz zn=$Jnu@UQls1JVq`l2e-;WMBoQ$hi;>;rywDs=S?uPXA2p9akKXb!sV1;(7-n~6@S z+e}*c*?21YMSo;J$^TAF)gU-1aCp5!Ak&5zXhFRiYsi21cvaWHKLHxfa8ytP8Gwc~ zFI@GA5+r^%7dGrI>SJqPppWaFc-RN_ZX&(gltmFioaEemwBmQ1Xsz$ALgX6`)otvn ze!?e;!o45h-ehXg4Jyqd&{7E^mo%f6`09gv;0{t?sfef7Btp$W<{RyEn(%XJJON%9 zbBL@o38I}#;oYDE8mD%ca>%fLwLQoY!o?axHfUBIP1U~bW|U`vQAEJF?ky(MUS}9h zia$&gg%uf^O2)8ic@qX)RB1mQ+qD|dCjAhHQi^IM8kIB2#z3o|a_HO3*A!S(VIDpe zFf@zOVzcn?dnzGGD8*(t9WN@=v`PgMcnE#Qpl-7+In!x>7NfwqBS)!^KuElMc?wCx z0o@n{RHH;b2CtwQ-xIHx0>jg_Bi=6Zdbi5o-Asdyl+!nxcv_>?(kO@;dff zO8!wJFb7IJz$lpKDZqm+9frusT`K&nA70lhk@YGYJC}RXDDu|RHI@{*?nk+jm8dqT zc~222>&Y90P=UmLp8bB>{Dq+-<46~tn?Tiudg-uqU2cN!OX?qkID7W}i#yT$T0kHB zH&rvhM>SWc!!nWgU=Tu!q#7-!8s6Gmwv=^2RdT- zukFHS4!B{B;4S5-zBr92;I^2}K3tzRy`-iVUh-h-SNm)lXFoW#Ue20TDmW;9~?)+o!nj26O?z=NK> z38b7Xtb}_kZzT{X2vz>Y*v=3qz?&|J8ElNEtPocMHJZi33KKOlSqedctw~Z)$ShqT zp)?+(8eRYfl8%@?JJp>5Ng~#9=3O$!-pyP4-vRY+r3nFE4VPxV8v%YtAAZmQ-vAkD z0xpby8q09nS_C0Kb+Uc5=;?2L0dSD}jEOR>-kE+p0tW6&4vcclk8ZYjEVgG*tr(N+Z6GWi$4fYYSWLwRQu%g_1GPR^@}5ZZ0gIY)>6jp$RFF!(79fhNl5>Yklexs>Tnuz|jUg_Pu;)<_~4}ELE9hBn6Uu&Ehvp3Dbr;8JmOzbv6 zvRP~wjYH!+SJHuJ#%ZYH>YQCB3$t@}O19Q`&4dXn5#Ggr6)FruSa#%jc@%DxVv#j3 z9shAT|7dANRl}jpzEekb7X9gQGZdx5?)r&ZDL3D* z*ITl8ayw?ZSWivpeSWaQo1<}_67KX3+omh#R7XQMKTVtBESC|%@{5ynZDupm((fq> zE8CaN30YJ%RuswHyLiF|)I9T5-b58>O z;`H1)vXOVxjkfgCQ@<~-AJu+$o+0IfrC20si$qj!9!x_hD`LGvsuGY5K6g%jl83Wk zoI=vyf7=B`oKP?-GM4VF!OI%!$NFS;`|4&(;%Y~n);zk?D7$nSoAB66B**OHcje7j zbSyWJgrdtmS)oGQxEPV+`}8d>Yj5%9Y2%tV?0<8dJ_I->&Hr7Z0(1Od^ufP?bH{!` z+u$qmDw^TVQ1-};EP6-&fDtwrwRzb|_>)_D^M^mZF%K2&t_~(?KZ!2G!_@RuLZ0dg z;cXV}Q!jRpBz5;QY)p^irf`%HEx)+#k6pFG^&>;;#YXDMyG=^G_1&6Uu?m6unsv6R zTmyQ1l$0oc+DwHTZvO~^f)_#PmT7q$aL?yu+|wDLNLU&ys*&&89vE03k*=92Y$U<2 zH5$wUrFt`_?SrE=Zhnl)c@-Co5fZZ%@zdJ22oFh=MTHYZQ`m@oKsbz0CPTjCPLyH| zhwO=NWvKg?<pSA4;eUeG4Ez;DzWn?G`kNS*U#g71=ELqT~+Si;+}^ zrTctqxCnYi$HELkg;$xSd^Ieq;cMF1z{ia)e29~1#}-wWdh59m&6Td#rEa|#)Jl2X z_%Qvq@YQSTwB_5v@maA|*l!(W^=?d$AH8FuM2i1`c|mAl;J_xtYNKdFAp}1^oV!Ou zDDcEC={@=}BFf?0*bPrOqHMk2b2IHhCHzcTcR4-=QMey=C|$BI92qkw(b%aWS2h-5 z)g--Q_guZf(wYoUh(7cCG5+JvQ7IyUqX)BYKaV5%?aq!~a;dAx z})^Vu{L{-n(}bwWYd`UImo+Ufdhvd15)NVg>~HhP-zP_!v;v%6gq z4W~KH;ar*~U;3ZJ9^ppCer2&)9tGkOnPB)=R?@HWSyk*rc#6tD!a^O?OvVy!k*qg_ zbRScR9Xj~UF_KmfZIq66DXN9N$9-fUxnb%5KB#fm0;Yu2h#M35xZS&>m1P?sl{C&)O-g3&plIF?s z@yd-FF1?40TvA!;Rt@CBK5D7u1W%C)g+{TGoTTu$tL<_b0F^&Ot1mkui9VG=&4z&l z2K}BKCBU9;S~RJi7EZJs*YV4P6z{qvicvs;XptD2<8PsKTTp)}T}Lb|4gPBWfWzKw zBS^LCAXaob2U22KnUj-332kDNv&Av0#w2ZuiuSEohFyK=`CymXxgNjzI^k^b z5Tstc>VQ#qP!l9EZg)h^Am_%`-)lX$_coy_EU(_2LFl8zRt4Lq`=*t9={6l(1m*7Q z#x~(1i@~{Ik$adNW66$bdmblat`Dyd(w*w^PRC&d#{WlB2i~6L!pUbUe?jidZ8-Ze zl=Tu!-%{JYUSuQHemm(V-~pC_v0<_)Ak-Qxxo_*@6p0CQU13@EzvSAHigxQbGMnSQ@Qwi;8} zXISJ#{}ZTPf-O`IIF>JnCUYc+oD?iRM~1qIpd=e?wome$7yMU7dw8iiTDkD@NUk8_kLYcGezvb% zRFrzhN|mElZ0!3aDO9KmqF)P2SBusgkI7Iw#69RS7AO?`K{9Si`T9w|e}k3`#C?R9Df|Kz;Jm(I-ldY<*!wyAE`pWoEBG?~ zZ+iWY+W&8<7x_;=`HxAB9Ch?3M&?;T#Ak+_YVoa1;S&2s4DgXBurb2ecVzXm4VpVn z7&h3YptI&^9>oSU;WWn<@MrJoBk@y4q17$MDaVORD^wPbY6@UA&dQp>TU6Yh=|+PT*Bh7%yKNVs6!Kk76lMB^b8i{Knp#Q9F~+gyyXH&>9gvY* zq)#m&-c`d*yU9gXK3kNDqFBv}0s4W9oS4Mr{nAn|*+B-Y>`ijBgWu*`OytOq3t3KN$kpDd;LYPul#s139K!GwC3`UH^lBM~?&5LwbM z=w_xhO$UiUR$-~5hjIk?ZA#sCd7?jmzS~LpB|twl(e!-7CPcve}34n|pFa~e? zPzk>?4{f*dIP8jQS|GiE--1tS_aBySM&F`+57sFxbOV=SV{zOsbOtTJ_!%ThD>ptI z(k7#3tp-;3Fhst{mTXGB&Tg zd;3qQ7kIxJMOW{uarCQ=?YJvvCqpW(g*kynjLs+}#l4HHyVkpTaPEOkc9ud00Mxca zO9-%;=&Nk9@as4H70>;$X)|-ozz@Tk;SIO|K_(~M>L;OHJ$$&#KC9BveP_1(bxEop zmQ~%h4Pq_`@~^w82GKNuMFADYk$sB=POAT$L@(@1K0DxO39u?8n!-d{bV;8u9+-*$ z5PSH??s>{K%ya30`|?ULlT(skyB>d5OC)+d#ZKCvBZ$0t`c1`!F50tcWht0VX{k{J z(qU*Q3dksdA3FW<8C3@^Pjd>Wamk)$4Vj-s_itj4;;a*Nw{H*{zectAVvoCI#=L(5ezmAh?YXf`UhnM=w&y`zZ9 zeV`0om$#gZ%o1O_$}$+4?@#{>1SSJ4o>xF3jU0*wOa zP!hvLrR16zi$X58jsPL@uqY-pv_m75Ga@b0NBpO*_)mGiYXtHqyUT@Gp>5~&GJM{y zpKp9&0(aBFkBsoZgZHaDskitkiP}LO!t6c_NmSB~ylMds(<4qo>`8+M!epD@^3Zhg z6foS!gQ!uUUiYjm@$GiO&dDO-=+Ors$m@@Rnj}$;1Cr5EeEJ>013mVYJXvx!e&*{n zHkZ3f-;lM9oIQpbLrGn)*-)P@7dcVTKJG7e5-+4PW+XXB|GZBdN9Ga&b=NM_z|TaoH*ysNrg zz~v*i(pMy2FiW4NIArM%EuDC)LFB)Ngb6qK>Fd>KJ1 z@nkIDVCcH*3=> z-@=(Th{kjW$7^p_4Pwq)KkeH~k$ewAo8GBkdT?_!izd}jQ9;Gd=QkmTR$|^v1B{xZ zk0Z2;Ou+~USM(*yl~xnVn38gO6h%}!`QV&Yf+}S2PE0etWcjRR7$#bRt0yXA7)o04 zlwS}Jf>soQD&dpPz62c8C=OZwlxi>cEn9aN+j;o(xLJ5FrlGGnp6aEgimsk(~CJ3 z5=V;o{AxD1&;)6ucM%J;wSPs+)DA4@@XFN~X!!lb>mrCW%IXjQ@VZ!7U)A#Q7Bc`k(#j>+uaY@D2R^ zu7(C6b7#dZu>iK*f@0Ypafyrk=CeYd?YdISky|!p>bM{c7`X@dkiI?qT=~{>knkON zV`blOdj{~N{V^=s23^H`w_Y1)#30BR{W#Fc*kw0VmP0?du>oOZ5K{gjhVS?wlk2JGw-*-=_a_6POUJ}<8R8Wp z&)SldQ->#U93;LSZKc8}`W}r+^jXp$K9ELD3pAXps&p~<1(QunRL7#ru71ltYzC5@ zBf8}@2}!)pCNppl``gm?>biDAj<(zE>3Pb0uSv=8-0EgXPCllo8=~0W_AXJ%k~cLp z5_Zdlhs<5|#bNsHO6PcF#RymJxLv&8I2n|VYF~OX+|er4$qZE?qzj_SKtSOrOxp5< z%(yZ1(`vV+lj=05m%D@Z7RSZE*_W1IR&9bG`TyOgR-*u~i+}-_)`~xY(64XbUqLU@ z3ozzT*`zDJ>4j$|0@V2^r~Kihe{`O8<3GhyuP@1;B(CjbCLZgUe+Gcbf>fh^ix5u+ znishW3;5g5av?u-eW-@{(zjxa%y~`<57nLlegoqQDE@qEEtTXAEwOG+m9cm&vd zuW)MmwK^Lfkg)z`cz~`QV#o;AWpYSNMKjY4!E!FCg1jt|SqRHYg{HonYE(3Sg2CoH z2%w1`N|hX{yzzR|_fCy&T@CS2KVM&+wFLmNMyDRfY&X==F27UOG-zw~6*Yl)Q`&ty z2=gAWu!eOBos|u?2v9 zrtyRBvqD>~@7IJ$(rHD>fLawc)~(^z-pJX~7Om|1qajEUU*$z=!U6#gv6gVY2jY-^eiA=-M}dvVm!$Y-i4z_U3n|HctX*S`&zu&2 z=+jTsy5n(qT#vz&b<}FD7x`nfL#0UR1n_7H1RQJSbWXE8M{0bK{78c7`N`>O`fM)| zhWrzLj$H#rTR8r5OEu0LgN}w0|zw`%&9<;^7D+!P{?*Eeh>|FF}f%_K$!$SKsBwi{o zh?bLWntL1pPs#{R22GsVQop6^K|A3u0zBM34<&x`58X^qJz?FL<&M|&_XXe+Wn4-; zd*e^KXqXW)(mgJiZp^@p02O31kUmhb z9%i5vDQe*u=#*>fX0*wN%88x_CBgie;@)7)oKKc36G*bC@twD*gMzrV0W0P$X86Jf zD2X-f9`FG?{n%9Sv?LcuCp0ufG8Gs1qM3IDjp zphoL;mE{=re0Q1uC#Hgb659?1Cke)#rN>Qi^tUVn3rdlkkoDoS@ao4=SlZi5 zyUM|(BIkJU<#BkDCp|0QJG!tuWA3Rc1`GPF_mr9Kmu)AZ9dT5QAnH$)_CMvFjsydE zTSO@h=rj5k4$klN@dt)_w0p8>q;I6WC4IFnTiDq4Xk@|D*I%BEjIW;6pj6^Tq>h@BmXbARy+Pcm;T!A*&XdaRt?T zM(POWqb&4iXKw7-{n)+7ol&D~nI+e5viS@^v#2qiH++^RXn;T60nAdw;(NyX>n|=eaDs z-l-Y|>KRhrv(1e}Psrbi%|^{%s+^Lc-#cx7`);X@TKm{hGO1`UxlFbf|NdDHSF+Vh z7UbEWbzB`}XAQ#V>$Vkh9GNXfk_ZvG&Me~FD4o~dP2?8&5-h2cc{12$LMe{{$6Vd9 z@G$58$@97^P>mM7Jrj{%hs6%tUwnwXQTxrVhfWy?uVjJ4 z2j~e9K1c|uQq}MkV?>y6zQ3juNiT1osh9fOqyrn;pdNjOx0~6 zR=Lw*r>y*~`OxB01tgx~gFFVB?4(qIN}o;qQCxy4B1y;{*{VDFsbd~c~s*K3!? zD`$bk#1|vBTfY=+e5RQ6F8!Ond9lFTh&5?T~Q(;ev}jdy|>Hx+)332Y37Q6!zP8nV)egWBW55x0o0l9;aSp_`XW z#s2Hp+MYVAOiRw zK<<8VTec=w&wu%`bQ0#E5NgU#>gS;>u$lS-wA3{_j^wb4Dg84w+%4il%pK^9G0s95 z+S||n+Q$`e=AAc51KNKV-B1G6I_(f%ArQ4$3 zWZfN74;-G|*4R`&sgiSLeY{V3BC@V*9&IxV$My8H$XIS#=^kuxAD+{T#K{Jf>$oA4 z$|Vezz~`XCPen7MJeqpnAixA~o8h?GJ17pRy^)^I#VqEqmn0pQ5GRb~$ICaJouV8W zZ^ShSPftC$`zpJUDgV>HUkjphq2vqI{?#t|2v{tE|6S~Xq6Ec3#tKV{43$unAt`L4 z9e4f$JcPc0B!2QR-hxm)!QQx7j5(Qs(P(CoRx+k?F05bPfeRXNHvVr(tC6$-=#|x_CeoBL|3f?ntSFRDb&H$X*p<^QJb9 zQ!G=_)RSB;dEj|c`EOH_`;%0M*DcvOh(Yt3Hqf0M&aGU|Bby22zmhwOCW-OqAg)mW z75(rWS&u<?Qq8>2W$WaGhoLjZd7F6HI?Ny3`Ua|N1CPt=GDlnk0OgOym<>Xy z^h)Z%L_wASA<9sRn>vj@#z>O?;Fzj^5O4u;Es)m!RQqq!L?J8S>-s*8kb{MnMDfB{ zE%k2n;UCmBIq>E`;tTqwr{cItLt=8^Gz!xul^oy_6GY0ef{?l$*^d5c%&1KDv@BOM z-?b+AGgdf5AA9iWQSqS#bwq23%hEtVBzpSDjdOkUnSHsT3PRK$gt;kNOU2yZ)R{W2PY|2~vmQwXgQtM;LErkd3=r zS7fsJ{P`GMw!*@td;4|vTI`uLOtS%C4)QXzMZbIK?+wLSov|e?vjcT7l{cd@)mYDn zzHm~JvPmPLzWEFt}+ z|0@(GB_)FhQ}{3@MfZE%cPH+v7eEzgPe;B|d*=`HZoC&;X^2cv8qPzzEmK}To~%Am zcF&D8eT)5o6*lMte}zuChpu#>Ll6g%rKy5W9=gnG7|y2J#m(7|oI`T&Wi((Bo1G44 zs*i)DjFYbM9w)eo3wwClC%kSjAKsyZxGA+<70d7{D757gPhsg6Tct=%g#9&(Ktqep z|Dq&0&>Gkbl9uL>lz?`9Y9&2J9ez_-bmKO2H6tF&k$(UiY*V6BUB!|6=PcbT--abn%{52ESffi zj@msK;sM8IQ6`*dV+5%};Kp4AOX!OPYXUWGeC`aqS!~B<6zzu#R|ep=@?m&5%Jta1 zTb!mC<5Ot%WS`Y0-j#$ouoy;IY#XfP{71vKA0q-!w~xbrbDoLduS&Hn^jyFRXrXy{ z4vab0lHjfW%yT`oh`Bl`(##5s&&xYr{HHF*er@dTYm96MV_|sEqcKD6ROq2jk*enK z!zWh!>(Co|m|um?9}JNj;)P?R$g&MJnw#tMSvS@;0vGAis!TO~j#Q4efj#r21=R&8 zkP=ag{-02C2>GM3-_b0i>^WX`^a4rNpY&Bw#^?I7%ZCL>Wb0?*1 z)|S3F>#GjPGL=Fmw5q{&;d(5RDSE)H!vfPxV6PyQOfj1mg$eG!IcHaYhsBU14au+uA&UZQXH^fR3G;VznHiSI@%GAE z=BdEdt3ra14xjX(qzVqy4uVS#d#}|VPFhVZ6Y;H%Vue&ZX*_6OsFleb;@+4`BR7{drGie@0=Xub@O6_b1={O4)lO7X8TjG#2(2Sa8Q(>le++5~dkjrhNyckK`7PbIKl;8nB+GoGq2kC+Y@+UA z46h_*O!)Fa7Aw~JcGB~c#bzz)Lw~q3Bul*3fXVicbyrBIm71%BLp}qB$qK(dOAG%B zmWNrxpP4b*T|Dz%xi0+Npm*fXGtZO6w#keH=ez>%qAux}%F>Q*2&f=!*+BauC^89( zlhdQ*8Iee|31YO4Atg9vuy)^sDjM~e9Fio)>a0lHyv|} z^nsi0O*nZ5U_t>K#&lI!R_y9o=VpY$7N?UyIP)jm}iM|yM-*v`s*1^M`m{$_1$)|org zL{95$(!I9V2ug+j2+=y!6z;?Jk!|`pw)fT{_1<^uqcD6=p&BRiPVf`S?v-Q7eXQQ; zk5OiMcJmQ+IX)3qvs!c#_en91sXx1snd-<8o&D;Mwt-B02uIY}uPYoDygcO(OTmj< zBVHrqW3vg>Zx7zY3q2}ZJM=u?yXcV}#7xWEc#hezMM}boF*zWbmy3xNq`!ay#XzG9 zhCh9ug;HeIL`CbX)6swE`n>EAEwtKBcWP%`Bs%lkT#Xu^({Y2+^69+?yJ7 zrz_MZ#-mQpXThCFbcUn~eryU0<0r#wAfB~S{rZ{D<42@g+WeEI>KI1YS?z10Rvd9i zrYJ)fMp@d`eE0T>ij^1cSWhg9L8rWHO6O7Tj(f$6#pG7`7gyysz_>B?hf3LK1p)Ec z;N}hZu>U_3Fd*R?5cLW>8vPOa2@cjz zq2}~bX6LH-@&YoEha1R>gl~cflPPKnle(XhLVpFk^@jW9H#}7*ykS3iMjW<<7@g1#q9c<03TNiNyJ6~nL*O$I%uK=Oq9UZz zl9F?Wk7<00FhyIjgGp6Y3iM!HixUFbqboNPE8sdP`Ei0Ja)PKavNlpAiXKh{Cgz_K z*Tr9vhA9-;xC3Z9>xO*R`|7&Hw$a+C5dHO@lcUp*s_iWNNyObgMJPtP`KVrx*fpl7 z`SrF-wYIOovtmM8S`#dcjxob3X#g9^8UP@79s;-UJ}+hP@^PPuhGV>Do#UAjW`)kP z1pLgIISFRAy7=j{AW$wkJueZT5RnNh8?Aev7KwTM_~R>BwlpM&TTdf*XpyQ>&{Dqr zJ6aBe&;hVbKLl18EEG6`C?%FoiQTP+8#DXARYu^z{KsCGlLdGcSyP(i1F>lQE8|;L zm|vmFlPk&_g*$ic;p=jIsB(-xr86?kwLwGt`Wrv_wax1;%ExH#f_LbUfvm}dzCnCF zZ0E)~zYpf7E{C;6R@6lRm!a&5{JG8bm??mMUMVVjX5HB|Z$v3d&P7V9gR<>9mc;`Z zA+U&D5fvdwNQi5mSo+QRIG@}{jGk|&RDFRUMMkn9L+3a;&{DFJ*AfQ}W)$8^j4T~x znf;c!mWF0LkmRnccFk>u?IA_t95d`U-I;e*h2b~QFDGLU6tD(V;lmtqu z$bFgJ6AOw9W=TXDp>7o3-`N`2bD?NrV@1*=4N|mDL}yJ1_)3x$$>@ef8}^2!#ZYXf zd+()==fCuHr8qmiMnmJJRaN+Gqf_>~at)3dvB{lD>eL(Ww5BZKkE^u5U)@8#0TG zU~S!$Ki*F{LExbALx10Q^O<2_AJ2v~~OdQi#Zltzw zUSrIVJ?nd}sJwYup-*%st}e`gDgQq6v8P+X=z1|Kh&oUQe)qK3f`_@?o1G~Kp}9c3 zVb;21Eo0M%a(`GOR?UZ=aK(4=EdexN95H;m=K8I#SLhVnccV%AHFGmPVf5&ebdNJk)W7D7=M=63kzxs#mqOF1n19u3{r6MOiZ zGmyOW;M>$afRbPan71qgoqgx}^g}t<(<|-HzJ|FdRG5ek=QC3f#y}^)blNsdnk(Pb zWJ=zXS1r3wfvpGUx<4Ilkj!uHA<4ifL|cXsA;V~yNW;)=LOJ-59H z)d!B+KX+XphiDntXd!R7P!u@IAQX5=INWZ^mt3-XdM@#yn|scv!-;#KM%_}$hPee! z<7up^uc1XoJS!^&lHVfW_@gLZRAevUH&NI;qDt0%4kLZ~Mn?y*DbYwGpAxjc3*wZ}D-_uej^nzX~`u! z;hfW;$V?W&V}gS)>T;vLLKlrx0_{*%3!y+aA#zF%g=FeoRV2lEh=;XGAPK>luu%~K zRn!$>^ztCZHjQ?%6&@k_E*kO0ZqgKkF%k21Ce8{X-y<}qQ`w4^m1Yp$34?s6E$K&i zyf`;`Odlvj#`6FNEXdNVH7bH+cJT>{w|*Q@y}5QVI&_l!L5VT}Z%>?d6vnQw=>C%ur)i0vt?gOTSLu&#Q{Eq^-e8qP!t#6w?L;V!zv$VXY4{}i zdwr)qK~Wixhu&?%y@?;NATvYyS=_s%R=ppLE|C>pg1bl)`!)B;w@ZHdDQ-S@kaGs( zHD`Eh-Q6|{z*$@_6$-G1)UB^mtca^AtXg}1-FlsMUkmfGO%w~*uNsw0ok|)pO&noU zSt+!AUgn&?)3k6~_08IYHzLJy?Z(yk_4Ze*leuEYSP%YZ8f%&Mvd+Jwdb6B?_U^uJ!{W3=9usFV;2~Q%KY~A%YEoJ zI#W~~oCrA_%mQ(xi6cCO2q;Obl)TC0YV}^?+)#|qtby@cmvajTh*oOh(VfU8rkVpc zC~7kcucnaCTmc7*YWc203`0u9tf$7|XEVZ=08-zC@69{+n<#|3|1nNbVADYj^D)H! zh9v(2WC@k;wbBx)4)T4k)EB7gfYjel!{xbxOPJ0J)R#^;pFnd z{2G%3eyje(EQH7E@2I8#cml{g?q3E?btUE|>f|YLle~D*&g!I@c}*(65DpwlV>OvG z>XP+Y&CL)qi{NwVPm*|D4UTT(#-f@l7ERx9Fpl0+v3rH z?tsgQ*B*X2o7T$p7B9k4C7NeLf-R<;odtkxGRFGztU~9EE5IzP)!?W5Dpk^D*k(aET+n>Q|n;6T({) zuK~=wu3M3mmw~XK3a8Q%uStL@(YWOtX1xw`;fQ#gh-m+T+tzsgu-%L;uI4>1ufeIF zlD>P$5?!N-qTkIDxYT*{$%|d>m<>Tg5oG$zLA<8XTFPeXJsDW>8B}%yo-8Qhk)muL z%Hg9*2)#U@qh7aZjgCD5175`Ow<$DIC&~PL{j~M8@W$4L##=g#r!#Kk-qYY) z7QDdBbr;O+fETCPWciT~3kqQm*Rz96VT2kav;T(aaM5${w`{Na1M&gZkia>~|Nf** zDE?!ESOSO{QXaM#f)TFW)Cn(NUlk)hO3)u{6)vAvEd>If(P_!+SAt48>ZH)Zk4)2p>=>l8Ilw+AeE+D6o$Xp zzBCR?Txg8;0rQY=#D;!U87x_AMRj{AUdk$M3}!8YD|ykUf>OtF5g@6DX&dN3GBc^H zc%U^>s7B1SLTP@fPcAp6dmJF$flw%hDdSPbmJO1WXcbODeY#O?AX4&{M>7MzkQM&f z4+Qe#9FyKaT5iLBrTJ5G>~qjTpDXy$`!?OE~d!u2C<~3I1OZK6+4Z0`e zPX8?OD{&$|j+==*K))f!$7jXqFfJ;{v6x#PhUMqR$jsu@gRXcu_mQ}bIStbg36r-Ov&nO`!z z5huDOI<7d~Q#Nec)ak+s6;HZ3>p8n)GH-l@-9ZMS@5f>;_|AOl=0r{=*Ls2V!FM;BiQVYLK8Vdf(-lAq3BN9#dv(^GJ*1Q#%Cv~eP4IULa zTapyZrH+0sH}^m?DtHz}i0HUb>2J0qRK)=15=AMNV900+6*^^AO(mO>v`m+)Y5tZZ zl}f@CmPA)BuxB%L1B4YPXe){W(?*1%89VEr@o6#Y0Vx=($`xNNOpTI?m;A0|eqp*b za7SDMNhX$QB*GJed5Seq;l-@}pO@-v%u3Njvvo6SBJg(Dj$SBEod@ZS_;Lei2YdtT zY2lO3iVj)@@x-F%nw7riSNA}MkrCUUYmyFlkbfS6kfwJk=K{}-CqoGbBkF~{Locb- zMGAb)!sb-`cL&EGhvvUJ@~$`eJ5Y8|uAjV&_Pz=Q78oeut~rb-FRrhqk{`va50?^` z2kVvu{tXe)TOD`rU1zizu(iuTP}!ebey+{vG&&m(*_M^&@0en-B( zr*jPA5{4|g^h1vlH40v>SgT91eW-VKB#PHqT5(avPd6SQRB5B?@Xbbo;z{;?CA>1z?&A&BNieac2RKKv{nyrkpA!Uvwh6>dbF@s;fi#ez@excQql+0}@i z%^RNI#p*7Jx=Q~KIQ#d^qYa3c$o}(JT~`H)4^9!r3r`e9`H83c;D?4_gt;F>!mYw1 z(dY;D$7zVdYW9!zt0^!dB~o-y=%Y2(eIx0P^Q)7uP*qkaI=OmVNq17nZDzIgau#ITb-v%RW60s9-D<%A3 zk@aYmgoPaFL1<9Y&G?FQ94U`js0Nu5{LZTxPha&EIe!*02)Sf?cX~HT|EpQx&dYEs#o81F8>5L6W50GlzTyq6nzOQ2yeqo_m1uLvR17 zBu6N#kf$EM3oE<6v!kB_w%5s@qu+Z&Iz&#Q;ew0YM9&$qQiR4m)j6vYxBU@eUnSlz zLb^LF?nr=Lm|0G@(HBE>v=Nl8)Az%Zcax15XZI<}b3DB-Q@XxRokp1plZoU(EeuQ1 z!~sq6!q;f*9v0}b%n|6aWu(Tc8m-5oWFi;?H1OXM66L6^SEE2(ECgXm`ZCTg3w6+> z$qjvX>M*Dm_4($l<>$f7%GvUiW{fBn(-!CAFS5y0_`EQVAW0LT;#EjIxk|RdJN;A z{qcB#@(SzGRPc+RkXW7Q)ZzMB1N5H$rL2VjcdlC9G+P?;%WSYed%-X{)Yfq&jJHqqUj(kVz(ClCLDjO`h~b9f)6(9vYbnz_1aY=B8TrSB-=MhWWY7 zqmHBMryh3b(Y@T>ysih(=9XN;rewOgHdlEb9QLh-nRreaCQfD=&8BMW*tXS(X`>RJ5YTLf`#L%i_GEv z{L%I9G6n|L|K$=B4a1Oig-r!4E0l@I@(PU?DQYx`1u|#Gs1R%^4{vuyuTjN)Y=AIN z8+_XtX4h-)s1to8ZDe0s{R;QtYoE*0VVQ3jae{$#J8w|Rk(aS&*xJ5HIY z-ke@K$dy-Pe3cz$z~Sgsi7Yf;%hlZRIx zFSTF zmo~AUY|($2W#vzL7{AnS4=#N8zTa_@YUj{mO?g23^E)1-w9Pg2_ z`$g}AkWdZd-T%-Pet|B-G`;!db;ra&BhDV(@ioaJ*7=z2>(qnr?YD`6<1lDTxETb* z$2dPaA>O4GF5za3qjHZNTk`&QG_)tsx4zMj5Qk?Y^rx*Oo9+N{^eG4uGC;N8Vi6|m z;RdUdWQ|o5BCJ>78nVKdLAh>t=44~Rah;U%xC8fgSQe*g6ZgA?%_jH{1A}B&WWDsC ziPo`L2riND*9%Qf)mcl*&~@Az+*3ooG#l5HD7dxTth$ErWibSNJ41pFR1##7R{B;h z4q^^5kx?O{k$s6CrtB&lHjW4L0$C)<> z8WQ!-#KAw*aW$TteSXH%G@OJ*Qp&kEGKJq3*DvJn;UBy5GMDZ<+L;-*uwW2rLPdlR zPE0H^`IO5YWC|BK=R~Q`-QBXL-5C6Ek;i<>N6Eu$1dO|xr-S5|uEFOeamO$n%v)Xp zd&8@xR(KBm6}XK3Y_lQYQTAP}{z4QZJ98UbbIEvsvO57?<#%3f^}AU)3E%Fy4HT29 zvkdArshHF!UJy{rLCc+Pgz}6sq%%CCC>=9?9~T3xNm93E>PE1rM5V}|K*y0E6dZhH zyg6;o34M=&1+gh{1vd>(@i?N*B0EGvZlj0lr;LZ&)+WDK`J)#I;-7(S!AS;vLQ_jDt1mzYTdb#bY!NI1Cow!a2 zW58?mraEp4cU51vHa44oGAUS*NRf*9F5GQWSUtVO*{T3ew~$U+)_+wmWJnEx{2!SX z*zDu}TCzr1dhm zy4ktRoN-d3O=K;^D3VpbS7uxv-1VR!OPIxlP5EwW%{k%a5KnU*Dvb1< z#z`sUXAZX-b-|@neWrd096gu#hzf~_U1}o)9b9AIk7$b#qyz!>S*dLq^hh+bc$BKU zdt!z=C`T4`1bC0)6ZO4HqIrFWD-T#Ddo>MIksyw=9@{YK1kTTS@;Z<)GdR*1DMGVS zmlu{fZSSsSpIw>lf>c<@qFuf~BFI&?rZMvp9Gn~+N)ZS;aM<2prJcoz({P<2gv-$J` z>7Ou16>xDL4pgsjiJ~;|%U|}%AKi1h=IBFJ5jp@3S?S>-jn_eh0XYM$gm;g=^Q4b6 zD?dAV^CQw+i78C&kw#;WoKepbVcubU^hMsnyTE}|r9g%|fx5T~wTX-Kj1i8KuAJW; zg^=ZYvKx|J84Uh_-`VgzvS+7dmYbk%@SMk&(>dxvBPt9RB^L=@ik1Fr-jAOl^}pcB zyF0X)7aZbQdeWpwj6N?d7(Gi>t<~0I$;^#T+F+3`p^&SgtO(Y*U82}P*ubK2+LPh6 z8Nmh_k`i+qG|`4@2?LLvO^@U#J_zFRe&Hia0KWI5Su}D#ohw8^k2oKVNHO>UzvKDk z0x|`XK2#hh+9-)fGE~bk$V3z>f*{+CkmPKiVo)SxJggUpF+rRmHo3-f)nnF=N+w&2 zbLI~0j9DB~&a|{hld-DXs4LNj8|BQoZYp*pdFao)wI0a^k3)#@LyUFJ;L0F;0sq2K ziLhbdu=>SIBo61qUWbN%u@2NC^|r#Th^H5quffkNjI+Lgb_a0!|Iq zE0gaoM|=bFVi&tR%9Jnn9p)O)zx4u%daF^+gpZF)8IS53Hb>563i|Ld&zX&5&>-#z z+eO3+&JOyU(s#@eRfV7jQHs?H@uk4SibS>4sUJh-B0YVGf=x0Zy|#s}215PYxAL!u z%r;+x^07aSo6L z?Fkzp#8;OF*2`zE$4|^FgaI9JDvVP3pzTTDn zgnd^`ZNIFd)838g;C=%9Gw!wQR)2F+4S!m(s_;r6?E-w?q1-hvM*A#jWnRZ=%Kn3}qguTTlzKXk+G3I%~KM*1nN_=ywCU>f6 zKvY2*W>+Jrs!JORl%_MMG<|w3;75e6mBI6E%WOieNaidTJA8Iu_J`_%_?{XyhfNTYV1%)c!Oai~6m(d24CcTCiX#T^ zH}T~9HB}2w7rNihJI?geKK|y(u^0Zywg>IP4Yz$fRgX&Oq0fLKj`LUW>XxgBN{QXj z-BMPaCu~eu5KrKt{~KNUbLfG?lqJy=@0P$4{<;6VAn*gQr4k%lhv;ez%fG!x@P7<| z`6B>2a~aQN-o#*ivVc&)ng!Ne8s;a`7y2dR7byYuOU<0Ii!)J~S84y5AE~nde!X=( z*idwANeE6)Ymj%sNe+O$ghz?AHjx<;%wHL`6RqwVwq$#CFE@_NqQ2DxfNz0Zw zbZA7v7aiYkgE6T2cvI~}J=$kiaGgiS@nfzWcfZZ6RLn{~S$_XD=DMR1Qpce-u68VR zeY)~c*I$2|%n`od`x%s2Q)8!`h>%bcN@=HrA=6TQ%FETBve|fppw0;9_w_f&X zZu>-T_+D|1>sa^X(Uh^UOeGeA^A~?1eZ?99%JKBwx;DC;i*-{@T>+lWTx8*bNGNUB z)m+(DS_L+qVFf2NQ(8a=Mg^ z4v8iqf_E#1C5St@2I%-_;~?Lx_~Y*q1{z?fX0(?~&qXQwUk1v|ysB3a;aXZ_o(?tto65VF=)eyNFanL z9rRo^5)5{JuBBS?a(#mDM}iVFl(;8t>9w};Q18p|G}YWOyT#^5 z{bWPV6B0FmRh{gG+nxE=h*@M>D!SGz=6p^!>XF-f|2h;p#*L1 z6+nI|^Kyv(J_uOI${E^{O`H#gsd28WoOAzlYE9Hsb+)V!<=xu4hvw8q<7l9)WYvKf3c@0ORg5Ji^YW$&bSJ}S=YOf26=$7-MikN{J_KC{AFu4 z4a<D^*?w#L0d))wm=)d!1*nfK4MkLr& z+*8 z2DEXkF~>^h1jMehgIx@po zV{rn`f-3dhN$LBoP&}T${csLxU-lnCG#0++@Qy{;Tq+=cPnTNP(yu@p8Z%f^6mlw@ zZKL2P=!73-xX=owb9+A~zxy4(`qSA`{)?jJ78Qt^Z0&*Qt&!Re-yM(Yq!6kC0J<^Y z;u8-*ww@v_M2dIKc3ul&7Jnd9=t>tR+}`&SKfMTuS3{uNfP(_wvkci!k1a2lt=$y# zi;e_mUTEN!0&hC^&Z{3o$7_Mj*{mc89wF|eUH*0Fqec&!~ojHv_d>^o=_8|1pk8|wxH z(#MP#riBGSQC{-i0`x=O-%fq$g27iw2^F{I8mHwhI6@5AZG<7$eA$+sj#RR1y*lA< zH(Z49~=^ix#?pmEj4$Bqv~>1wkfXi$sV{zZ5?I+h7x72sSCvu{bOVF9MdRspT9v zRW`>=V~`yq#!y-(76yCN%ZOeJTHVty*>*yFFg=29Lc z+!z~-MX>p66jy@QZx5T@0q4$gS_i#u$$_5mqF!Krn=j|2NpBGq`i0GiuB-HVnji!~ zRsVw&u>W0O^EV<{1=Jb@yxZHm)~Ec=bGc>QgN&UJ6ULkN6^6_|-{CHb^fGad)Ba1@ z*ikcRS6$rI7vRGww0Ktu*~UM>GVV6vyMCV;Btcw~g^XY$d_~|o?nr_5h7r6c z{htx&t#WkH2~bRkK`0&Zkc`VSb4liIM7jJfs$1ox^U ziTL(IOuk_%$4QzdwH2JKmL*`27zpP>V5~J5fv3nSE?NI@gdJ)Xu4)W{qEh#uZ#lwO z!ET(TI4}ufUUzgPidMvw(8dVe!%%b=RHl`k|^tq-%g7Tb2?cICY<c3d<{O*E_qCe4T&hDdaoGP?zWE+6*Q%3Fh9S-;Vo`i6{brCEHkyh>s2-pZxAqR=Ru0ar8_e;%QWu z9Q{KtenH^`jff-!SUb5bh&5@na+!G<8@jT1;(?l03%0tbTA;cEmM*HK1SBr#=j?A0 z`7LD_5V3EU08mX~MFX(7kDF|?hq9wZD8|fXnoQb)tsJhQA7F5mLnOhUF*EGpYe*9@ zF*M^EytGlR6~m#M3^%+LmTh5pNwa(uIP23OW?&Q3U&pnz3ZLic9hqsh&Ng|FY#?d& z?_;dPB;CCBcqh8u@g^^*Gov466V^eHx&HSpO9lgnFd_n^7NGyTrnZ0S&wCbw(}2Ze zYmNS<)xS(3{iP&l#^9tw=C$l&B0P2Vf=YGWog-dyG+h5#`pD{TYk_jSb5r3+;Vy~% zU6%0>O6IqF8l63-#{g`MC=}m0vdeSMs(?cawNEeq4$@Pty`cfwYCf$`Um^q3v>8!H zr2D$AA}(zjk0y3bkgLp<%?w^_W_B)&Euq));O36U=s8(H3HLJKp1a<1q^Jy#>9 zf-1@!dWyuAQmg(Ofc6 zR2;z9L>ZiqUlK8|xTM$hFB#VMY8JOXm;AtpT{lbqKNRd!#UW$qvHv0-3 zPF1n{5hN_nDndmg112I|Q$lw@Npn^T+YRx=T!EJ;2uAM4d_3WWNdi)rF4o9jSF?qM zhN1xfJEZJZ1&4gdr6!I<2_l)iP01b}L5*aN*-#0>(mEF^2@bLjCIrDycAPWXRWIbm zPbN`@(~j2YCb6T*aIbq#7*37k-LXgigtj`r_WH8>s11fmoIXH1{n+D%@XIzte3UINJ>)5UvjsDWa*Q8E=6t=HK*FAs3G=I2m%poy~|2Cig$gQT^zWp{T1gFLe zF(LdpcnCt^w?D)#;N<(8>$g`KSDe&57mTS07e1Nd!*1ChAtDt|l40x9yIB%%0{&ej zfG`1Id4@#ez7lfoS2Orq$mG&>kn<-$Bi;OcRHhu#$fQssctaSodlVhpC`0Hwm;=fX z5)D<7(t-K_e6CSi}qjU|%`LWlIyOCcPl4Ujck-#b33w82z zJu3`I3c|N0zNctp?wo=nG{$%~GR=e*&{bn=&n5SVC`{NK^>D|>bwHpFAp9o3@gi7j zG#gDvJqX|vIN$}b$Q>~rN2pI4$4Ryg&2nmP08>3tXq{ZwmRCegI_$!ksP;GW#t>$i zCmbLol9SFx)>UN-U&ZhCllySTvRMB_-3=yr0)q7Tzcr*FIzLt}Q{2=aK(o`v-@f0e z-$tTDF!UQ>vyL7Vt@qV}eAoZdIPvYx-yp%y@Qwr8Zsn$f#~r-cVxQ!(mu_oNSU>p# z>UOD*&NrxdpTiD%Hx<<-KE=IXo_CcUYAq;=;rxh7xLv;z|Vi8<(v|--BsYL0{{OJG`H16XFy$KCzy|~& z>!^QC_Z}+aP!)y#2|p;VjLC}xntR=e9 zA#6JbK^|GfEM9!Pr?PHi((To>mktxpuws-#GY;0N92zyu57r%XYaxDph)M!6H>O!~ zYUGE+VLoGs$lmW+a)@T$~~` z_Bf=F`4P;Px|j1pm~gUL*Ph#AGZuXv(A8YnCJ_h7$>BcV9%^Uzqs6mMM_lN1jD$DV z`rNK+rjVSin^1req(r6AqiU#Rm)^jv$Vg&S6n*IV_Qit#+t%i5IvzFqzW0y%{<^PL z9T^cv^#OKV<8mCXD&uDa=}j;(39ojE`sHk|S28t6S}OwX&B$(%)--Nf{NvH{cG>#f z%|)YCWhbxr#kqd--Deybn*CT~GN#A*G;T+F#rD#k28fvRSWwh&v{^LqmO?J`lc(O} zTzsMQF+@Tb62c+@x$8E^%b`yuPKcx~=rm0?QVesMItgj{+uqG5)SjbDX4hJ3Yq2^7 zu%F8{N5qdjYufN73+-?Tr!DceHBRD*ERPpy>iymO*kh^~ISEimSc$-O13><|!j;=p zFi2>Fzm%xC!g zOL9~qmWN+U+4ZdPapq?O{vZY=%7mv@uLCHrTGtVYg}3;=EE(#EC5VN>VM+&E!!foc z4|ao=Y$JQzB>;re!GfY{a>LSQYe;Y?RBlWFZ0S{~e0nlxXwlC?$Vkhrg!OYS{9LU> z0b>?zAzV4}8aA?dHgT{#;-OynNYFN{P;j{4%xsgrG)!>Qu`E;S8=<9?5&N()7q0LP z=O>9vdJBx9qxRSD9Y^D>fhpf38PlMrX5XzdmQ9_>+}MF3xD zz9t*|8b<1lo1el}w#=g;a#=NondL?Cm=4Z+nMS>@edvg{@}NSy>KoO}Mw?FYf*N78go!4*lDE?q{*gx_qVfpewmc)E+&6wuI5BQr)i9}>BI=1H zOn*rD4e~c`Qm15l-j^z-XXBY9-_;oPxIOOI_TTCEb8Gi|y#>QK>aCR3rtIaYysc+q zwW4;`=|KrFJke$}&|X(ni>-n*CLSlTNbD3K^aim~BHpwjU#h^5%tXHM#$fALM#BEa zza)e%ElLsmo(VZd%IE{w#2ZeUI;+We6Ijyl5JEZb1UE)T#O18}k&}!$ka8K_rO$E^ zcw_ii|38Q~IE+EjQiOlNoxGiYu6R+wv0f791Q^MXbOyvXVcq5}vMwKg-7o(8@$U|% z??ayU!@CbfYU=U;zhhi%Li;=jnY`zU)Fp>QM}^nht>KQGZO$P~0AmM`ECQ3n=~#p6 zFSFwvxzbdV9x3GM(`a>=jSREwwLggh696TmN-H=DiJSlVHv~!(5k`D42>7GMyzVew z#guH`nS2B@5_KU(iLLC6B+b{7UCfY@8yUSHnjvi~u0cp&z=$G5LqfMnSP%=@V5;b@ zQ3^Cc%om>9j+Q9_EjPa(y2>=VU+E?24h_Grzw{f#f$cZwn4fW+EOz6%W~WIMRZR`w z0YY7+Vn)MPkLE&FTBnwoTXl^lE0?|wryoDXEv!Ayq__Nk6eCla_pAHs8dPZA{!oWoIjYHD8R zPVVr*rh1~ev>Lp<9wp^Uvw`yJnHFf`CJV_DA%WKtTIRlodWPeZR^NE7byYDfo{f|^ zdu6)~(q#E>Jcs(E3Q2b-a~87qmpOw+JumiW@a}ZtYt?lTZ+3o>4)UMhrEgW_vA6Pd zzh@<9h%Tfi60{5J!oJ)oaj5dz)1Jv3rr}8C&BLWIIt$iB+2NY3nb@?F-TFP+Vd$N| z;G^4uB3(c;{1Rg(fku=0fdE(XBYx}`78x@Wc$8@Jg6X!skc~Ev(TP0O`7+3P{D0Pr zAOZw9k{Hv(KljUDBMkJtqU=uqUN=NHgG#RpjQb)=)EDp$4MsSz_q-hc;9qP%ktE=! z568#DdxE>*?@Zd?g^v1o_cjFMA3XPL9{)6Rb!Eouz8ol%MF(o)GBzblN-# zl7tf4g0V*p7D}=z%UTl7M6_%2jf%)C#AX`9W1Cr6MN)9X20nUXBIHmxDSFJ|O4__A ztrV6*NDgb*XP3$>6e>GhMnRJNFoU@`*8Xzb&(#lf%eWFtCv-M}HP?a!T{V#&+oEe= zObn|aM=QQ}_b$kL=O?$2%9rP_pe*>Z3V{T5R%$j0wqm8mlvcmCkHt7b(q&)Oh(eaz zB<1M9*pRj|*dglmG$&06@K5t$`<<^^TS31Oh5rPJcL`_{a)uwm-Bp9h_}MZ0UJyW$ zrF5)+wzT|j5WWfoXbfQYiBri^y!Zd{egSZ3BBetztX<7nHL$VZuN_ zs;K|6(G_;wo&$WJD>7+gDjn&gKI@yorjjV}@++>$Me$ih+;iWDl&C+3FPPdf-tm$q z{PV~D<}C;SzyPf?m!@M}9!<)Iv2J!Nb=cKHwwf_ymMDsWFgpsO8sRvDC%FjdP+6)X zGZKT*61$YF1$8f0`DPM0qb4#m!&on1{=$CEVpC;?1`E?LP#0+~2cjSql1BdQ7(v5| zq;k+FS0P!pS3z{3eUHynq|-Ep)$q)Mni)AFf=Y|i+M#Sc;jUPZIBijc4t zS)^*OYD9Z>|NJ*b{8tpdw7-?i-+?ETA#oYuCIrUy$z;^9$pLWRSI z`abKt;t%+9Z!Igv1@Nl`j2luR5pr{?f!G zqwy?OWIOKD$qt*d+pp+!>oXJ5c3v*?MO$zQAuvDc;1Fd7)zBg-%t^*<2PVdJXA^a~ zpxDwQCzi*^e0DD?h5mwv}H5rF@dBl^u8K6Azb`}f*b^wPA& z)3QQ5fp&4*&3Cj>`<3^}5qCK+>>0(m*K@xgBi9_^P&JrfoIz;2+*%qgUgM+0VHD9U z&(0L!cAtzCB$Z}P>X{T#&7EM^c|o6?#id=S&#nt#+qX+Q#VWl7&$D<$_PySb`8;b| zEZKK=%g?@YvI^1oDv4ErKmlK>pgllT!Joko5`_Oy1#7^99t1@1M|mD9U}l#H4MQ$?zC@jwdIe&xO3bbCoRFRy3Hu z#SHR@l9-d;kIWW%bSQMKQKLnptB5WHij|Cc8VZJxS?(vz$q*mY<4}_ZegW;2E7gPx zIf=X-#?(zLTbmaJL1#xp4P`b>Vo|Se82W+<<;$8ISHL*)+v;{5YJ$A@SlbjQON#X~ z!Uq5Cjg?3IGGgr47k!hq?0_G<_Vvjb2z1@{0bgeVz#DCCq`ZrwPkkv*j3Y8Gah8PU zQfA3o8%#2e!N-JV@FrXQMGuxL}2qQ*K3R~X~F)yU}yO|QwKc;9Cr)q30P z*?WE;9W;&pnm%eCfVOJVlM+uPuY1Q&&D}%OCPkU`jAj;5Kr@#K$rk?u-ebTtT4Ame zJ89p*kHuUxxSpAsqqHLef?P1ee|hwbG7_|dB7 zmjZG0Bm`j;Q!HI7+}Lyw2_)#AZNr_z(YVE&^;~S&+(FHNtHK3-Ey>o%DWs-X^@Tuc zm#-Pc>%SH)Y7UdX_k>>Ci;Xx0jXLK@a4KJHeLr_z``ICletbOb5p z&PV>UYGq6i_yJfPol9yjCBw-ANQj7E@F6@88^YiC-c!i{6O*q+;lAtFkP)LUDha7 zZuOxs%`C<@>}}dk+$>W}ey)ZQr_v3CO7i2HP`x9P7$}nnjP%gQ^%*wfh|7#?CdY6D zX9mnOax|gcwNL!y0zT+zW$=~-7T-(y2_=ss=E#E5Ac`ud#lzsHN;8vm5~W;X_cby_ z-JDj1*(7p>t`9m{I*bCjmx2<7O%qBfA+z?vK%#VH>Dn9?qXTi-P^Vy-N4->hw?_rd zRZ_HgVS?!wrdo*G40f_VeUogPkUjBvD1m8D?mj9U#M>K@5L*w+rQ1(yQnu=5;({hA z+4hEl66DA@paPm<`A~CM@&=rs(Cr zWR9SuuN&B2b$Z72=J5kW&DxDwzmc}=SdsNG8DV^9dJxGZsoN~Rb{XR`YVsjWed_qY ze(5*dvejs^>F_-yAj~?tzn-~ybt!n%VOP618?NponL7obR)3n~nRip0hlX-j6& z`ytCVii>SVXQj~b;h|!w8)I50xQR%DC4#Do_v9pFZF-vqp>aW?3mwXEcPvSXW!Jbd zDS^(&A&1Y%#>w%D4Mq|O*YAZjWY$_cl&m5hl-MN%Ww!rwu3!>PF_h0`_-hl9?afZc zL?-13_gZk=2xIOlpeK-W3swlFsDi%jKo z{dog3hl>~;qnC~hWTG}%RR>@U4`8aR=WoOElt zRdH?ZOwT5X?+{EsY1t*B{C{sUI!X!eZ$elXs5%Jp;-gEMG`=c~e}^3$i*I=r`YX7f zAgq72&851`Rc3cen|r)WWqP`Ks1Js;I-)U&FYipq`@{$RuE9~pJH>uC)H`loYgcX^ z-L)_4nP>j&`_7GD>)<^CKr_)Jc#8Uv#vvIC%@%P4gKWLd>KoapU^?uqA#|^AEm735 zqFMQsEJB-IQY+ODCv{CB_F9dCUPU2dvY$potDzx`(K@5#I_9GB8#T%JpVFmONw6m_ zQIRTJ&ji200=_-aY3CfBcf08@@><aV$4kF4RQ~FJeFEBgV0~8;6zN_CA$`wD0 zTC~mQgw}u0lWS}HoqU_|x1zK~+1ak}{@~iqFL+HY5I$G^Yzdnyh`65aiz<=lw-5De zLL6UqWvjYZ=?y6&EMLNYJ)*Z}rmH5gG0ln?rcuq6mTQ{T{~_ud<1$^lx2vhiw(snk zY&Y4q?a8)n+xFCC+nOeOvhDY7@BRGWuh++Qt=~G&^H>Me!=0Gz3#mtssN#WE-aZL~ z@OR5!?|)z`lCng%Dv%T3_0-qGa5D(QE7_~?Aa$bDlw>0H*vk#+8(Y%_dawkg+Iw8) zWBBP6+#vvYew+Fb1L4NW`i3o{#go$pXVq)78>V#`8Ax zi{~?AI&JY28SV~$qt||qhZUg^PP&iqN%+Ah(VqriJ&&F!fKMklQq3!eybpbu-@ydX zYnr{~O(H(Tfvq>u=`i9hRZN2qzo`ZXNQH+QX-1==u?q=s6k%0p_X)@fgW3JaU>r+8 z&O3J|`HBM`WrH&dN`B`j5EsSlg9F?1zw{3vkp?0b%&jr#B@y5U9`*8=IR>UpGPxOfN zniv0}Yqq@*yNenm)yeHSd*~s%7(;ot(YyV&2w^wN;27u1TFjl1v!9H^7?1bBUQ5l5CylDL#4Z66U?*nDpu< zl(NL}$yvZf-KWb~o5Faa-Rjdg6FsM8-bo+W{JBX7re%v-#FsQIi~hvWQ)zLWseJQ9 zvueU~nqXcV+7*N4axcC(?R_2G%gsJ^+o@ea*BGAHli8+S>+md1e0(6`CJ)Dw{?Nid z@dr{sv(~65*%mP=6|3+k_gO2G=l8raNdJwzrN)Yywg&q=K|r8Kg^EHT>I|1QiFcd#QWQ~lwa%(qJL zbyuq(*P9M>l+NJAJ%Iarp7ab?^K3ezwaW{4_R}2OyO393C{E=&mdGzy%?tl^#O2C1 z>FX&1f*{}n&k*|ZrJ|V@$cC?sO?ZWU$AJE*y*abs zAJQFCJ9_zxeWeWG$Q6|`PlXf?^botis1|0JWoNNHg8UM)Zg@0Yk1lYS@-xG_WO%Bp zrhI4RBmuy?NHNUy2-p>jUPCf>@JN@SU}^GRE!f)!=i)4=5_XL zuss~m4fKfd!pA_c? z5xSNG@LTT%T~2g5?vNkTqC}~Zov46bEdM_{OCF6ZB1%3@?9)5-UnT)^P35`l$$N1W z7QQ5k_m^dn_hMhM0=>Vz@6ttG_io1x`?$@V5_Bl&z+;dEZWI%8{iTnMnf;UskK%9*lN4Y?IV{Okn2`duvy^P} z0dr*{C~@RAGLcX`iIfnqqj@WaM&GAm*_^*Wds0^^TxCphsC*w}tE)e~=+5`@T+s}wP@F7GOY=n~o~sDLx}$jugi zt9chU=AYAgrGxT8^FkMK?A*lY`EK-O#Y?jua{PxJnH+n{S!WUU(F?w?0l9K`Z=!$b z@-8(V)RYM03T?q9JSttzljkE^HnxZW6*wR;VuWd~Yk3#u&T+2~vXn!nr)dx>ifdB9 zEE7&h)DVtMUBAN2(BTFcss;XBYWG*|beyx*x>}7+R(+)_D|eO0ewB_(D;&Z7avZn; z9}Xs{HGI-`Dma@jTI87wbt-rYD+d`%4ZXJdxF9M_5XkU9u`DOnDr8WLs0P?O9-C7J z7+o%=w%%^=hSOf|dR(exnVQzlPz#G(LpTLQTDW*SeRLDV?<1LZO~|k@=9i2^J}ljT z*$m;K)rA*ay@<0<+g2MsX-3B##XVae0eYu?zD7S~AHYF_hMe@7C?x9hX=vEx2dp`y zfpuiPI^j(nVE%?jHTC&|wVs_3tz z*yXiC%Vo9_$6qieiCdDOD_F_nbatu}C4TG|O4C4~(cP7l8R_!#C{oGY_s(ad-5c!=wW#> zYEQ6#kG|o_P2C+$V^HnSvXX8($Td-Z(f(0uQ>!BYs}oAvj0laj=MgggFy%sPZ`*Qj zkP@m(>3+0Q-mWOLN2g0g#KQDMl-@sVyNua%`Fa$^FuJ|p>C7+M?JmJvB#rR}am>gX z2R4;&OJZ$|1-sgc{2|Y3$7nyT^PC9p$0FwJ1O|=B{Rk zyF^Cqn(}7*YfH_5`b+^h@|qj~P6s-r(tJ=$j# z1N22WtzeUydp%fHTLTEC;{XKJDSa*)u`l8Q#%<%3I zGtI-jQdH<>H3-8*c33vr&Ay?3$^hoLTbn{mOKQQ zQ&Gi{DB;;!J>TzWilp?`=xgepHp4pg@#l7oA(W}XG&n8P)Li#Rw9web{eHigWiIaJ z6%mFT;IIK%S>q5XkeP9_6wR(it*#n~;ZDMF3t(mP5KB-6fRx7wPVfS`hG(b-k^k}F z1vsRz5rFpC(qulLYNyBge>Qz#f@WahtZN8Fn9Cn_Lm$*nBN}*S?P3;pGf>VDC2)KKP!_5Hs$Jf20F?zGM%oqu#xb zj=Yr@-t3vBQazIRSjQZMyr4LTSb&nxqdJ1fb1RGqOay2uN0K{omEeO%5#sbNytg4{$bTk|mO8@cS&*Xl+p)!=lHG|oWpb#s8G zC!~iy%5(UqcMJo5D4185%|oa)aDF>is4wb@-`U)!3%A*+r14A)Wrk!S@9BHo?DnpW zG!p!L#gqvJf2heg)Y8Wfc;vq$H&R03sxN0y&+B#n1C{b%oCJ zCt7!&`MC=KAz|RexdnOweFYVv4EkuUn=3lZWzE(uZkcmFZGT48TTZ~9)8nmg`prBm9&n#`qQ?=4?nQCtaw$jUi5?nzN9`u{~btT1&}aG9b!%)oqLBUbHp19%^7q| zP=bC7M0R^vg*)}&08j1V6zDQpBF$a0;(ewTa9bFD(NMmSkCpkOsuqL*alP7|YD)fM zo4L*UDh!3_%jzx*TX5?KE)A3MaLL{QUl5j0|atgJi{*4*p6Agmk+DFd0%h1d87&gN=MWMc4^dvXzl_2GzZ$jeAE)Cwt zxNqiOxxQ@ThPLx+nYk3Kl_Gv71~ zNBKH^?|bPCJ=I9-qV}Q?rBXct0|^Nhlp#WgxLFClI`Z+P$!Rz+ zm}$~`BZNm1!U>4R(NjAlqp&%NT$@v{YN_iBD6m9qDq9yvSLQd{Zo}5qjt#kQ zmx0-`fY-L8=$ktm@VN)}tD^h~5MJPm^ploNcPoHK6&5a^H~df2|N3ltu4YL0XUP2V zyxRK95l2LKq-W$epaWLo$x{jP3-7*VYP7DhChgF;aJ`7Q(Nvtn&;d+c(p@PBza9 z=8mRa?-tP$YG^A5ov5;TZ`q&uhqd?h#w)V_bUN@{{p7*V#R4VV_)I;(f+HgV^^`%a z>c-*$Vj*|0LfXDt`3$MKD`>foZN7P<{AIc}?T@+~O)AOBh5^cSqA^m*Fj5T<#Cm>+ zi$wf)weRK<@3frS)AL-Q{S2^|jo)~O8t^JKwE2YIM=_J!nj30ROKu*s{IMEwurV;d zwrEKk7A%CUhK=BH;K`U`#l`speq_ji!}L>oXM-$mRe3d4|FL+;1JR?U6tc zv1YF@8IiUT2^OCNuiNV8vn{LZ3`Q~US}6$;aif77%w7k8VvLJ&D=b@She= zW7_Gvb7yS*=gX*blVnIz_mq*EYwB7>w4O?|eMJz*pdW$$zbpgbltF^Q8URMdnWA*F zdAoAv4E^abZ1u zOV#YC@PVi>V*Nmq41y7oJ22t>D;sEE*WY|&U)?^4m@rK_j5LpZ-D1hc0X4M>`O$pq znp@{QTEbT$B6q0tlRlh{pi53^TkXG1#BWXC-G5CPnz3hEWy#iJb~oZ%b=}x`KEg6v zfBB!LSls~%EL?%=AI9WA2DUG*&-MoTg-xNF{@q%BF(kWGgaF@wt4+S}fP1r! z<^AVy4&UF|J{q3=lq~yKNrA(`$3$K)UZRogN$z*Od!Z0WJeXPV2em?vfjPjoMyHYu zssY@T)l?!c$$f?hILuV()vVU7QGW8A{QQRMYPCwH{qBj=z@Y8dry6R!JrEDzs_&mQ z6ELHaE+kq${pmB4bujcFN(X;*tS}8OK}-Z`+kon?Gu<_@OIo11OqdS(aWjQ0mL@^` z5Z#zaqI^UcAh6IwG~?|he7t!pK=N)Uw>Knq;Tuc4p)#Yc@rC5Ut9QjYuQ5je67dM` zA>RvzH5H*J)%Sa!_74lY>$(-^{4I|V~(@7a+Wl1CoX+yj| zIE`2XoSIHQ{cJNfz`jX695T5S9XM*^jr=|Td@Sw|H^$WftL?>R%RKkiVsP6pm6Cj> za?Pd!TAAFzSDidEsBln5{hunE1AZ0ygNxMbnjLGT`kq|RZIs>;M}WINv(CX2ilqkrHB95*f(2#Mb*5Kr8pW~9VL82Pcq z-Cz#|sy<+FitLIJ;iu`%` z9l=fm2m=uZzaU)ofy{3WWj4WXryr7EVogNx=@hX@*f_T>In+;THGl|)rS!p^r@Whg zDCvOw4*pzTo+RJ*Cno?C1~Baa1_PEdZZ}{s))KS*utE(+NR7HpX^xUHlZM}sAkE#7 zFJ2Ht?Ws%sZLC)S4L1#&GCgN3=9r~|A!5~TeH7&lm1wOAMuPm$l<42CiBL;#$sY61)0NrMo`_FGgbsG9*n zQ8}<4hF*OI&EV5Iw`}jWu)u$lKR%r$;elFWfFETdOy%|eENnqU3`vnRiW)-|14?PA zL5$lTVkNw1-_Zs>78YMvDIQ}^SoDNfz$KX^_*B@BbJv{(;&KfyCHT8Go38avu->py zUKMYSX^QPM0)}PMq`Y?o zeT1-|)t8#fg*820sxWMv-nXg$-fis*nU&pE0nIsVZH|2z>$057HZG@s+uq@e+rAsB zbZ^pvfNG=Jed_hCQB9@kTyOmcFAiI!zUM34dA9NH&9LUINJdw6`HzCbqgpBb_!#=9 zL}I7BlL`Qbl{7JP#(Af*IdOlM;y4}vq?e-H4oc9%!vL?WuETOAKF3KM!~+*-*H;=V ze$D|TyH_Z`(0(+DKU1a@A*PXWZ{eKr>Lx==qO@%p9>y28^1Zd3Acm{Ot>dosx&YjX z54w1A?_xUD))zOuwQv1+^8P_f_+l;FTa1Yn=XmZYJvq@Bovq-zlK#Y@CKQv0 zG&Dwd(Wu;vn0p}IUV?XFSv)f6H<_ARX;9H|#cjq!f`+dLv6`G6HF&_E22EB_%h z#38(qfkAISq>^#l@~yON8S0Sw8FXCmIs_);5=jPJ7DjAhbWRuO71g}`338|=a)=67 z7+7Ta>>M|7R)6H|mG0aplsla}ceH?%l?I4l#>UO|kZnF=ukgQI+x0{a@K_|zOq)F`jG>MM7jqJ;P$o|@- zes__*0XVIdW>B(QX0 zmdLYf(nl<3b22FGJYBs0!B#vjiI*%tAvPkW6;vItT~-dhcwsl%_qfW62nz6z3FuZ3 z2LXC$p5oOE%i~~cn?76m64z8lG3BBq;va$V0Oz;;f3GIwlwgqj(0`P~gTT)qc#G75 zW%~)^y8r?9N@AqrE+?Y<(kJBj56R(62OvT6;)a2H{Z{i(9XyR;XI&WJ>cI%-JD5PA z{{c=#xHI|YC;sfXm}-l|RCrV9`2iptwGwpZmvPs_*C@+;T1wbv zTX%P|!bH=hY^R%5ZZ_|7_Qzj$^Q@@+89S~9wX>cU>}bHS0z|{g2h1-=N0S-1%_K^r z=O}VU&&N7n&%bOBHwunw@AEoQyDg$B5nV#`Od>;u99T{k2sbCDE4wS(r`8nYZ^=~&qLca|0!-s>R zCZKGvc-%Fx<@KEVsC!827JvL^muf=(7aI8FD}@_UO1>C8PrP=2!jK^RS0690%~v*a z3lJ_^{$z!ZHB!I3w=^kUZ@Q~N`ThB_+Ha7H-d6>hA1RT#m>kXh6HdCg$Q~RYL%`J$G0c0CwUq!z94GPUmv;$pNHG&Hu+=7|OL`?%rb~X!yNk@HT zo^H0mawUHZGd&rQJyBuivIH3j;xYGoH|%G0go|3DV--`#$Ob2zg?r!2NTxnu}*LEdco5_5R` zYIKPZC?ffYXC5x!@gr@;iL-BDa-|=HMqS zdL|ntLKN!%LLNhaQpByNj1Y=nbnLIC)kM-Pz{5EguK|Gm{SOKLT>0^0{5xWN{#o}i z9$Ifp7na8|C75!)BL%(K{>j}EI|AATBfnOYJ+6$(uL~zLo>Ukki#s@&py+J9kMeO} z3gnLGo*_uRDs9e+q|Oua=xa06j|2@7gEB0lpgw+_<2PMgdf^kc`Pk$lJ+>@9t90F9Ta`VwTOwGcx$x%7ToZk+Z^$y?cC${e zoSI-Pi1enTqk+)%k;%}o;k1uu!Kb3%nHLLc3|4-!Nix4bI*z2D&7{1SRaDUUjrZW# zOWHEY5PZKI%p?VG$NxKA<*d6oF2Yw?f1>fg?|`3`tv8NM^`=vvC*l_lHLt$@S9HP@ zsJ@1U+XAX*b62Zm1+^~%Xpavv!B&F_lR*JFBwq!B&v$M=u)Zd@o1@KKbJ&o#`boZA z&hs4G;O!QOin&(c(8bJ;I*FOge0xy8xtsxoIOr_yR>h2(BCatT=*5l$%@O#|^9=+8 z3O*Lh2qd1G=>9Xi?*R{ZzXT!3zQ+kuOgq?lu+8O5%BqbndRltqmhchmuA8OrJg#h` zJs_t{hd)i2rTnt`_hp&D>TCz-=3fqtcs=gRL`-P?T5jk7FD8d<2n`4l!R7Gve_LWX zq7`E>U{fj)3RzV*uf-;RsCq1pcKLCmV;9V<6k_rbBY2T*|LAmlyj*~bSWK0Ywkt6i zVlN{nbvmVak~7v`6jQQFgdR5;a-4Ll5RYcN0Op5p2EIp3ox^LuEYqH7jDRX6H)vQjml?5U2igkrtvISoPtkQQ_;L_pSn zYBCC_JF2VB2D>yxl>bAK9m>aF)o<@)7*LmiTfek#KpbiiT>NYdi@44&OzaS(9y1&8 zb%b@)IMqTDx%Q=g=+&$Z_ezSr>_Hfq7(Tt2*Iev+7w>v3T6T#?u*7{?0{pMs|JY*( z)hht8F2t_f#uNX40C;gCVxr%ojjG~9^(2kyBVr8?*Zf}mxSbfyPw3}N=zKtI37}6#` zo2Il;7@m(JqYX3vkJ2H7tW7bC!iANoUxc!0st@WV1d5E}veav*dl_dHGP z72S94F?I@3V7a}LGJQzUq-x19EkVLoT%9s_;9jTa1cF3@wJedB9^*97v zqVp5_DM(S7L;`9?g6QxS_VJ=pjDJbNEml|LS==U3;l9O1qhMzRj7ENE0*%EihP=3S z?CK6`aj31g`(-7qxld|3KL>ud2exxq;XBakDIvT}EUFfJZV(`#)1jviLF%Qmi>wa0 ztw~N;xvcJ24yG|4#DZ-i5sJB+rLSQ`@XMpCUSycH(eXL!{Aw0U#BL~|R2u{Vi1B{$ z`$Qt)06@(RtU=-DhV?VA&(tmtSZHu@VgYPqw0Oj9DQ0AdFfi?pWs$BTUvV(szisa_ z#_xBJ$J6_`U$}B9aPnF59E>UMixm^kcn)*7q^^ae-aLGR6ZE2VAsL*`jt|RcMZiii z<$V|`dGtEyI<{r@N80OcM%zR57RL@?cjHBOhI)flSvBDN0yxd5V+4YuT4rOS6O60; zr}|uzP#YG0^El)OBpRihEGR2kt94n(RUP(V&>JRjDsGntgJK&ui{nMzL4^<_;h_I0 znIxeND&cEJ45%nbzFfjm9D}vnhAW{%xyaCyx$623ebqGNcI109Y8&H{1t+EQbm)Dx z=#3jt)7ReFi(gK{+5b3f%7;P;sa=SOZO(AtliyZkXv*fN5>214&s$aE>>YA>gTOo$ zc2|MqUz#CqwTkQFY!#J^)1&@-A+KfHv2WQw&wy<=tWDa;v-IPpN(szl$iuXY-=`H{ z$0UY-jx~L#b#R?eK0h6V;OW1gKo5jy7=r?VhVYW4NDxB*2CnS70{;Vo%{51Vm-MqskeQoK*7^5np>@6-MO6gQ`=R zOJI-xU%tol_6u)EXeU5Lmiz)V7qw5^R%6o?4$@T>rB(fUr55Svt~Cc&hc(1 z#oepCKRy5I5a^NfTD3OF6~lgbcy!g^){51uT$FWlp0-gfG1Jxd=8sJ|i?20)!{Dj4 zL8d=)QJa-ZMLuBJJhB**BY@26by#!-3@^pJSN}mvSev$zW8W3ys|+4fl4iuC^@~=7 zyX!XHs1CXK&JoFyv%+@wpxITq2zrvFW#szHfF2c(J#n$4#Pp}B{0upyAK}a__F7IZ zCD(QsmG{_cCt3?zO!h7syW--s7xBzMvaMoAMw27=W&uBQ#?=za_VAsS+mHjZ?z)8S z3lIbO7yGKdm52MxRVgNn1ZGiw`3&g$a{t@>kZ8RuFWxe{lMXA?;&6Z;#5YBe|D^Ag zANkezGTf}>mVi%1a?OjFL@gQ#l&?xy>k1Is%HM+~_U!hdQNR!X#_MtjP}FtOli3^C zV;mFSe#I?$y0P3;Bd}@vtHPBmhdzCj+$-PETxVC(T`kxkc^F?3lu4bK8QU`i&(f_`*4m?Kk2DFM`nb!jZJED0v9l|~3k1)!wUdyZM$NK1!+#2;*ar>%e` zAr+WU{}vu$r!ZZ82p@=w(Z@=B9vOCv@#CaCWlYMOJSI7DdM@98oGY>5XB(>2$|tGce0>K84Z9`W~_oc?`SnwoOI%kg1J}QI8lS{;s4pC zx{@94v7ScGx-mh&{yb1l8J9WsD=|ZpfB#pyxUayAF1a zv_Ay!r7`a;yZ#0;C-Wogx@^N&RHN^KOss%Bs0SnmSoY87Y7N{sR1~BEP@*1c5})Ri zt+!llyvEN%^(A(xQr?(!bbo4i9DU}*E_yHf9dXAy^aU+3`go!yGLfix076#pmHdY> z*(?~@LdT`lnc1}_#e010c8uI*?$#J$*0m_AD-9??#=hhTMjl!IwM}x&k42@tiEmbB zl}>Av2{tlhh{@K}>8IJKfw@_!ZGRh1!faO>9#TAqsUy67>=BBl>Qc#s8CcawG=;zK zD7*g{H;Vdfz2Bds%BLF9Hqm2JgDg4P?B8&J3_r=gViL{+=`srMEGa2GrJ|1H=@10?blHS~oeRWSvhDqDy(z{8XwM z+CYC2A!y)2Rw6U;s|sd6pEh(bRE!V`ASuv595E~;9NwkA{4yAOcZwctdU|VdG;(g? zvTLo<`~6(1O<>!ED)>#T1wp5F9eUO7Sc->?i{tS3hP~?VMJL!-Tu)=<9}5TT@Oji> zs#%D?@Uj=rFH~_oOKQirjjgl3*8>SN$=n{#aag(I?saYe=EN1^a4L%39UjjDhagG@ zht7D<@6X{5df>f5Ih7wOLUr!UJ(hiEonzq3LMe-AjAba?44Akqpc5c?pRN9h{bm7? znWTS#$bVK$+UmZF-t>oNP0-p;L9ygD0+vSe{1&IbGBJ!4b z$l0S$Ogy1moP-&A%TS?@bG~`4h~*m&jT}Lt*vK>z01**9U@#ZMk^A%y^X%wU>r$Df zql#+BTI)&<)n;XpJ5Oi1s%9z9cZ*lr_v#nAVFsAzMN@Ohy0aOCw$iEaF}-M|=ZIWd zBB{cPO2?0U)a~-CW7u}vb~UG89S@#liQe!vQk*sXi^AhsIlsiOV5n2!>??IT^(M79 z;rX@4uaf4r98@*Cs~nGq*hrk3I**vuYHE9AL|G-;I&}wDXbgq_7~Tzac3v~)YoZbL zb1D?;esgPsWhl9dBLklaUF{tB_S>&1{At~Ex#-*$#g)#JJ~%ahdAKKUtez!b`p}W< zo^Sfs{jH=QAFDxn9@W5N<18F!NOsq9+na@%p3s$Ep{B@^rfuP@f>6(w3IULR0N{UI zYd?c+{3wNiX2B8BZxQ<&?;+z9ar20Hn3amZ1-zRF@y~?6s*m`< zzOtJ-@`lFcA>%ML*jx`2$Z5cZYF;ZKw($>`#{wfjUG{ttd28wqwX1pKz-S$mgNAqO zK~Fyhc6o^$cd+zEEBAcrbQX+y>A@0$u@^;CgO8n3u?J5xOtDEdit~vh7c}~b(c)7k zN%L#Ta2Ig>g!Co4p2d~R$Qr4{g{mB2kYHpP#0j?PvK9`~5N5Zx<9Z&jeXRT6_hC++LCUE=+m!vYw;9<3u3gF9?>W;dR)aH$luJ!-9 z!G9JVIwKeqAOJK?{U=T6CC1bb7)_~rh{#Y6N)u9Vo6r*@obw_cI)8CwF7`UPIi?)D zegRPKNO>4OE71Qf(WvIi08UtdGU25{cF2FplK3v2yHu9jz^mfFlg5Z55K3Nqq-XKL z9`-iwb3%I)YD#!^{iMN?USU$CDXRQyVGs>x1&%CYpsDFj$+Qw{s5Qi*{4tiQF!Cs( zZO%D4CVf}VtUFuAj{l$mN9Y|_Ooti0fPVqA%CYzFgB*Wz;c}_Q^g2t%%S$)EQR0^? ztBT>fH5cdiPB$^ru)B+f)8|)6VdEDoQtMNRVuIJw`L6#?tXg^-NTHR^SHf^8mYZ9+x)*QZ-gi3c1eEa|IG(g!Z zeoTZx84!qkYyocb{#B5Pq6k1Er^U@Cie;xp0YDOV2w|bU+}`(%Kl&KoC`VtfZ_XjR z#?z>)Nw0ueJc-}tXQJA^IJY|-IK5?a#MX;}$!ft}5=slva*VV;a zOz=@=X@XaZVqxI%*pEW;SsjD%XmikerM>=JXueS zwU^!Q(p_El1A-r?o0Yk8u-D67y5}bo73Vy+dNP|sv{zhmJyJE6Jj?acCOktQNi^Qh ztE`Neget$k-^=BLDL@9vy2MT_2SuF1G^*1495C*%t zMggsbNSg3NLMxPi+&u{b{r%FJ2`Av_!`K_T@lg3h293{1Hf+y5IeRk8-xrFg&Nz zf-a2UWitm8rfA+q+=>|PYu9ezdiDTcchrcNVx{YzS9VlgE5fv_Xr!dRX80KHBZ<^q zo~#%gr4OAD@9PbVTZoTE$n5}Ik@r>>(0WNO1SzT#uJ2{2eO*9-pO(<4tpP4Yjoql| z&y*tlNChG^1Be@^TJeNYsJ}>1&io?R8W%3#G+t>!_MK{_S%HxXA&~t}(r4&qy3`Ds zQgLbN66av)e1EwRDYv!L%SAc)0hB~Z*-qkR`zJK-Y8D`>$(I#pMhjFO6cH`rUGPAV zZ(6zRAy4~?uN=gDQWI%VyZ5VL2H!)v8AI+=yAZv4?TdduYg1Xm@T`y^p+Z&%Uz2ac z>4TlV#RJ<{>KD*_s1)1JBlQ!KK>>Qt0Q(+@KVB5F&j1HIVc!VR=uows0xF~^TOl?M zKQ7dr+@)jO>-vX*BL2f72l%h4HTQ3g)PM}(SlH}MhNxJR^8ufa_d0RkYN;D1;VYf- z{h0LktDh(}Xo`m(V+)Xim+--=2h?IpLk+lpY0DP=s8)nqR0M}tjwLYV^Y%(gO4y;3 zOS&k5QqJUEulf}^7BRV6251-;`dHhiN~a1;^zhSLL?pRfusH0`Gq0XhdK_kZP4bZf zBxVHZ!<4mIaV+;j6@?da3=8$fHn7!kP8SFc+cHk3c(lDa=|c`g<@Wb&9c1=$W!T_a zwiV#r?(K54pHDBAuCriqie8oEelRgTfN0`{-UlFn&FwZbuLtuA?}RR(r<|fxwGl~r z%Piqh`JvDDH&KAZP>F{^vhxro9Y9%b9QgPacR9056qTK>Hc+zKYk_ z!EUn?;C}!C6a30c&xgO_iA5?Yy3i%W+af7;Np^!5`U2ZU5%+3qU^10IwHL!g%}87p zHX}}&2mfq{N4V{d7)iKp9s3B@T}DNi`X1frih^b;AWF`vA6C}O4R(U~{l`bxu_w7% zqvcboRiA-5wUhRY_&DXwcFwj$&IgJ;8Wpz#x`@Bv^rTsd1Zr*d{)E zt~v@4ln@9~$)Y=F&``1XG_RTv_YB{YB2YzA$FXA$H%yPVmBJ0S$}N4i-A30kVSr^< zoF8x#7tQ-!v(GddbR?Peka7ZP7*kp0a^89{A379$WQ)rpA*27xAxVvxBs{4!>AvF6 zVF!unaTo4JPE>}9Q!5xAsvzSKh|NUT<5sBStjKw$KA|LQ_E+;MeNk@A*q4T;9*gn0}WMm3d{q z6au)fVqtHzwFh?OP#>0s1X9*DiKLUr^U)~Re~Pl+j*XjO>1!Dyz?z*|YRUMEr68l2 ztBH$*St3*M;+0rKz$=I$F&nqoUS@8Rvh_PPVSkCSDWgb|)(yJ(ZHV( zYewI6--Cm?rwCt@M!j#Z)@8=oqy^HXZU|bQ<&HkKH{Xf6&!zd{MjBWcNEEK%`Ox;w z6Be-hFiYmbGpRekFX$k;5pJ-MskWQl?Gmo);Iqr>;!e>S3K>xpqP^?JA~ctOWI7*O z4pgcxU`OJx|CTNdh~Vnm33CaSrBH^|46u#&#wqNDmgzB2tx}A)5&9`GPlFfnH7_V+ z3EJZXbyNmWb;W(J!HGLLX$5PC5(S2&%rV0jRFXJbAi)g%{T2&Hu=Gpr_b%LGTlcHV ziN|6H%&&PBzkMVM0lXthkQVT0c;k2kgs1Gf#fVs)WP}^=_p8dvM-hL}fG2}8thJU? znQpI<(fl*__-GML*`$`{^5%K z!+wVY6{Hl9B>MMcMQ{WD9HjNqog{VhfLY;qd*sCZt|y9{ljrS(57MO@SF(Hdac6d{ zBHLkkds;Q=(LBQB<;y)cy}kf`+>cgjkASrkIHPE;4$TV5U!GE$KPWfYL^@msz1 zGaJv`MxeU)Y#H-3hPuBNJMBd=yAjM{ZLqw0o}xkZ+6`0kM}p=31ELMe{5oF6Md4;F z>}_`9wwTWv)w8@V$KlxtAC}@`uj{y8L)3$T=uBc(>;3hr96a;&va%aRg^GOd({}BJ zUhRY9O*eV!)LQ>tqgY}Hj9xUhU^I6!pTlD(I;14?$g+UyNA>5sL^$TH3!-Y z!!ns&^)?jV4!G{Lmt7l{)Jts+0MxRdl=qr67wOjQ$6ewS^x^Ixplzk$*w(75}?Z zE(pMae*rfZhzCK#3G=akGrITL-f{Yz>H^5fN%2S`;-t)GiumM6$a}tD!d}PU$4U8z z%3t5ay;W{xmiw>PJHL86&w`|>Bty1P79gGad?k7~xHlGiGw#kW;E(We7P!qWh6L21 z{-71~IX8wZet@ig2}NAw@Dh17_6ngRhw~c&-=11MDcONBPaz?*oij`ZlPu66D&&fq zSJ>HS6C{P;YJ1#Lx?3F^rgq}I9G;}j(n**&Gn`SHbY2+ManOqHH|MXc>)3J46ajnDyM(pSPuX;yUitJ`*LP|pGQ(Zmk_6^N2(1abW?02Q$7 z$dOUJsv zdYwC4cB7&yKTHGQ+Vk(R`Y-+cpBEGW7998kfZ5y8hrs&#&>S^!lqii$5cdgHk|F_( zB#D4VlCH=f3KwTRu{*eR6P>WG<1<%4+9)<^TEdJ#$aR2<&$ong!mUn=$_sSqef4JekWOO(NgBnD-q zOTeINIC;cTWe=Ra`*k+JEh04ro?R$>ys24(^+YDt*^p(`Tnd^3i(O}wj8;{e<(R=- z>~CQSW?*D>*T)|pv}l`euB3mfUb1_bQI!(Yi;X+Q?pV&S+IVb{MP-W2(WTibSZMXd zXW?kYlqGWuSiJNS|HdE?Y4z-I`S>mKa<@DD($@DnSbA~thvBqos5!{$0ioE@^ zERV!N6J+*uB9t_EO`v65s&#L|E7Hri+!rKXIJv7@F1=$PVI0Z5eFU{v3u~VIL9g>GBs_Fq4rlAS}tdHfT-`dIuoOsoGQy9H-uG>Hnud$X+6nKVd{_Nl_CR+pqn z#(FRpqH_KG>Zhq(wb>$(u20lp@ii~dkCvx5R0_^|oKY0P-$}#6QADM~@+5I1Ey`dl zl~?H)wQJsC4^2n}i4yM$G~`c0Ws#B-C<7d%ypw)m8ByYZuV9ZpRVt!(Gt3{Gg(R2Y z#+wT%yQ*F0NA-^g6~aEI5O4(x6bkX}-s|=Sh{BSL0AuVTKrHTBUF!@Fm0J0vpUXn| zPvFT-9wh=6ypoTDYM(KYm{P8ghc+syYnh$n14N; z25Vr-Xj{GDhUNbPv_A1j#-MNzV4%=#;g#L;OW-FQ4~iTW59*Q&#%tIoPRka=$hQiY zyQTIM<-?FrnCx+rr)7aHcx6W#8{=1;rifHe?}MT{VI=tL>^tPO-jLn%d%_(<#nnAA zVe{Gzf(MWf4^=YmgEJj)aJimIZq!j6q!` zISUF{#%R}!a%E(DMM#Tfuqh3?gNP<+PbPR(D`{Bz*|5mTP(aNiQaov*Xs`{xY#~HS zb{T5*D|ZdQ(K%zm$|c(8prg_F|oq9glutCn0rr*1;XcB9Hag1|0C)u zqvG6_B^H9q;BJGvJA=EsySuvwclY4#P9O;$++BhPhakcAP0l^{zP0!@{Oi3}f4!@! zs|crVpNwdmunRJYvD84zzF5n+`VU2bh5z|2{VhLM+9&YjE=>vdvIBlWU0DhI9UVVL zdrzFn2&lSv-3BSg<)iNLg0O-5cC=z z3_K4)3?(_ydTma65%t7}E;{E~o3y~RNh6*#O^ErZdBSkN(plfOr(fl*A-)c4B2k(<*JgXYyLAu$@N`KHws6|BDHu;Dcv( zVR0C9Lqp6fi%wmaWc<+^R->+Fo{^_g#<{|($ zD=nKxOyx8i))+x95l!0WuYLYuu+0g9x%q)vEG-@umt0oE(mZ7~nXTRd7ExFASU9+0 zvN30EFROhEO;SVf0LU`0Nh69^f=pHtY5K-apM;Xf?YcO7HxAd;E1={KX0XKv}& z+_u2iWo~Zl{j+&id9DR5kGdU&v9?(U`?fOm)UwBD0GNmJ)l6#~o0K`j@GAdo0mN;a zlcQ2b$mN~uKdHOTWH7V{&~jKmL#p2hFLWS{k&i83ddLx6^MDjN|od8 z+|1LQIS4n64$mx=%9oIDg{dP-hHpXPS+pW0^eh|2QACaKbASh-hLOWI%;$tx^(0w~ z7@yr9*%Tw>LD%Que4P9f0~^>o@fYUqW$%EgZI;a~zvA=oQK(b!QHre>4bQ#*jOdRp zNuZ`cI42s4kY7fzie%>D!*;lC-mpB3Wi2eRpcmXU@u27NbFU8;!BvIpYix3MR;Qh- zR~Teoe!jJFB(^5AW@T{=LG_P=+pNsn?8-xd?TKeSnW5VM=bLU=2pOZmjwgRJJ-`_g z+OGw*+=bwiL1%@BGnY=0l=L8m(X53K?=0ww4#0e?HGdx~co`jit-TK48ulmxw4b=2 ztg-@Z{@lv41&Ga*zRd*D1B{M_-^j&#KE7eqb`EQgfO8XI@mNvkn683$wq8uuA43bK zvxLaHM=Q|t2Ha8EFpSXM7EDy=Sk!{YB%q4APrg8`A7k+z6pG{^wP1%^q;SxbhMB~} zJQPqeu(eS(bLn> zLisynaEKXb0ih4B>k$S6l4NN|%ED8$zL&3azk(Y#8@8Cn{ofsOl)jSqhI;Bg(8&5ggz_TTs=az>Z5RH#F#H#cq>Tv$ zgOOwC|B>c_i~ezNMmR%?5uh)K$;^4s=|B||Bf(c6s`n4tABV^1DDFd zoBk`u#w2XcNr|HQAwp_vYpXI6%;_}@3eo$zLCm#H4$ALJ@S$oUkWf?{uu{jo{N#vr>?0NzO|wsBC@1@c3T9ppg)W9ePQt!5i!xLo zN;f$~F3@B0(`+M)dia;l(NRs9i7;`;rXdmld)dfJIOhmDSrL;oUJ^9SDI^c4dh?uW zirS!ptCR3Jr@9VVVF#X8n>({aJ@wN429HfX&0zvbWuTW*TK{PpI_o%7wuP+dSaxX+ z5sU>?v8mK4r&(>U&goP#KyedzNkgIavFxQ#aP3dRkq(OOq`(G-d(cPDL%*^2bo5BP zzwNp1QW*&^2^@hHRCx0AO6V^Pt|G)(KRNn#U!Tb5aIp3%lcMsWQfd{nEl&1|^ImI% z8fKW?%bvFN3c9!dFPzDe982$uUUR#Nopl5iOnbS20P z^fLVU?Id=3<@XEJ$~d_nd=W$Tj)4V>nkhp?R>ETmmwKaeQzuy?mU*$|4&;9OL^|&s zYq)u0Kt&|DMDtuOm5Jo@ACc+xVWgJ-z90mAM@8dgtx+E{dyn_Qpc1 z9iuC}@YT|R_im|H*|&W4_@v)M*l#k=K+s6`ngzZm*ag`TQWKe(AS29L!hHXXY=&u9 z_Pog z6*8mNqrAS7xFV%N6Q&_4gkRx*_Qd|GH5j9!OmU?SQ3t`c*1=cc1u_UI(GNNNUlwC( z50ZTrD$UW(^9UoiLFXzYcZdO~6c5W&Yo9^sN9D%!z~W>+87y&Z;-`XcwvB~Q`CyJm7>f=yp5>X3DO*tY`_YR+ze!1qZFfE zK%RFC%u9?cTnS>hj%aU9lY5LY7*FP3B5#GwV4b7fG4;wJgd2JgCiH4lx-cSPS680@ z5<@cw<3vLm%2H%%=?rRJ7n4QGjUyt#}ooZ1TVRbkri1mvGei3{X{r!^4 zI@GSONnE+=P>*Au2w-lY4O=gMux@=~Ddql9MH&DZ=c!S-W=OOe6}MO?Z;38gax|E~ z`U%*2M*AJ{Shx>B&NCER`Qh6y(^>O&7hP&toL_|;wARTO=!i8k>QUirM)oSqibOH! zbcrl0-RHiX4fAwcaG&iTz7y*>45M1T@B@_-i!UC%I9 zlUonlGxD^|UE1Z2-CgwqTa5qLb+#Up1yK;Ql%(-)_uXn4;sSj4A>h(VBSdN7DPdyK z&19Tsp($6tMl*N!R5b^kn!og7yy#!sGMCvS`J%o=C1va2eNZV_kvZ+Az_>ShfQSxK zMLngF+j*~Ox{HKQpmsHY+5bLD6{?Kvi3+jv5^!-%=@($~wYdB?>zgGlvGa;9JRa@Z zy!rswM?Ix@uQ%I$O?*zH3s9_5mB+ebz3ifq{_?HQv3*D2%~T-4H(iw=|8%+KRU?* zt`n<59XpZ_4Sj$~gFvIBB$t49k-@fX0xkf^ zu|%z|zthRe62_;W3oP>mTVri_9JBHbidXT@eOa{BZ+dKs-iz<207<8+k;1tCS9%Io zJ#vP|8CS}_%s!-op>*(BK+udPo{;$6z)L7>-K6f2Aj`aHy`7i#^jf@f!~2ilP;PWY zB+)cIOS5ROTYpLwK5wz4U;b^78*-nft5*gTY!q@1?o1 zt4FP8mQyX5RsA`AcG=CLhX1}xbXil&T-%Sag>Ep-pV ztD|l*`o50Px_9Y~p=FKd8{w6~P-rSfrd4IN3VZ)kmsx20m1IL{|2*#UTc8_$<8P6W zx72uOjhPJ@6ia(AH{y8lgj)#W&i@js;Ng)n4$(OKuH^699{3mlXd_9bai)}#p6KXM zZeCG|B6sj4)jhMAk&gqQnEKf{VnSb6<`QU$Y$9Mn?5O9~jN(m| z^BqJF*vS=;GQFfH9$nABSITXNWY$Kre)1=jirdfsl>1kS@*)9;nkRTN>+R+8qv|- z86XzgU~^_pC^9CTbBty9(XLL`R^Xa02i&ZC<+eSli4p`67Rf2n{D}5OaLDAmTqVj@ zGCBvmE$whR1dQXk`>o?Bq)nxOTz{PAs0nB!|498QIXESIOBV;DE3$kRj>qiJ>zz z$oj1P5EE4b#xeP%G6F1l2wad zT$|sj=J0!OB??HCpq@Z;Ey|r6U}As+d@(rOLKBmq^Qa z8h3XlW0U#u?_13Joh@pDftV9_CiiP!t5&@{DZH#RYkkyGrFk+ovmEoM{Y^B9)a4X> z0c$Onylw;bU4op zWxD(HA&+pS@s|K+yNiW`7H4z$dOo&PZ;&2U+~v;5jkm8;PYrHy?6Q*kFQ4CUbLY!H zH;noEPxA8v=+6$R9{a2LIbDOlG9o;T3pTfJ@_0LToHT&6%m<;R74);;@KyR{SJn>~ z9X54=!#yvu6r(K$0lN{pNw%dY-S_2g&Mx5Oh&TO#KIi zqlQlM2&5aFlVtf&$Q^PTCce}s!Cest~?Qv^%Pl0yT~-ap-4 zP~M!+|I{}t?LbMGZlcBR_Z(7*({;$tZ6vp+vrR6OasXC&GkGQ3mNE>dB-2S&CS%Y@ zaLt-ndQ8;Cw3&|z7dLLmNtRmt zHqq$VHOrA?-=Uc?qC&mZ ze(a}M3?S0-QvGb^{>Q-cmg-A1a;>!2$Ksv2N)APG)9pdhcfes(1FWzA-#dFn1t62J zeo3exu`+`A#|3~SW2~?;{DnZ;e;G2ZR~#Izyw}tBAhz9eM@^P{YbgMeIGo$?n`qhd zgX$73%6cl<1L;GS)N3R$c<@{5WVl5>FT6xnSZ)CI+Jnm744oHzT>sp0zLs;f5VvUF zC73_`Jek?`dTfc??!^#YL=6Z8IcV2rNwbK!9=$gK{=`p5hz+>rfsG@JYHEpDFqS%SoS1jSZGIi@KT! z{LeA>v!l^SWZ6gQDj?I?(;xcg%?Rd&kKZlMcgMAsaJX zH3%`Y6sSy-*9&F}Q;m)WxjM6+e4e2%oOL>5Uh*|a{AJ%L;!C)-aJ10!j<@n}x-MSb zc=BDUBHaG0RDpHR``}ypmu3c&oM5-3f6RBj8iC(A;GZF+&B;XoDkHVP!lXDgRFSMi z+kIdVBq)ZQWiWo4AGfab6WX6a(J=fTRH zu(=Xh#u;ZtN4ofM7`1F)dPzqMnD2Qp0(oppIw6P6-`&*K^eoK<`LFJ1G zR5e8|H~vSTed*V``R``1tU73GQ$PPqaG-Lly~_#VV^DZs9+MTfcm2$R5%S|uM-Mnp z0^Kdzp^*Wnlj=n8hjDLzMDE<^c%4c6BH)1Vq3D6rbAeu$tH2@37_T@{Ny{mo4tv z=#=(BwT6c^+M=dMp6kzYDZnm+4(g@=)_L^g}?C1r<_@ml!Sd@-Kfg4;-PgySJymwy%`1I8|66*ygw$5j|c3&n2|2xY`? z)esG3jc~=dDf$QNI^A%Q{*vicx{i#p2?qggI^;SMv3Ofi3*>vY&*w7|%BM6+Rxx5e zQ)w#3E|P<@aahNqDsTITW$3Z$60$Flch9>BE1Ld1Fj$NH7u8-`8HuG;i3ayYSQ8x1 zoiv^B$E<%V$`T>)AuQGeSL#f#EvV!)Sl}=G4G|qDZARV#P>ITcq(Ss;f|aZAmCs!1 zIe=AqMF$PG7HpsPw+C^8WfZ({J_}x|Y}Msvuj&PG586t*@AoQ{Z_v!gPjUy`B~0g& z?EVI(mGDlnu1b5iGB2NbXWs7)^O~G@795vS%pyTy3y-=vM#ve4_n$(Uy7p-W9rTfc zhrO>=tMcMXt=Z;SOBcjK*||#$OUFh6?4hMY2xXN%a{Wq-c@V+?U3s*08&uexf&6w> z&b!ape2X1)&D3W~cq&p{JDBWzj*uB$V|r+w4}y1;~9xUeE&zZs^*nA|$&$*0Z) zRs)O|H8PqsUH;%_eD0ygHa7|0U+<2V~7;776hD>vZ&=6xuMT!iE1AqC;Js z96R&iRR%9fCb%4r{%LT)crttC8x6`bdNfISF8HNKki1z6#Gdw$fsIMD9C`aW1to~O z6c*SP1QI1V)Oiy(2SFHe_3~P}X$_gPWayHQUBPJ4I{&# z3Miga`ZPZ@>ipo3^}_$sI>NYcu!Nr=iZR9hL@rgFz4m2*FFce889HtyrCwf>#0(-*^tKbVHfY8?@o}st z1)Zdan775pO2_n(#eBpR0vlFoF2Lx7IPo%f>R0FW4-#XQromL83)^4wk-&0>uUvN8X zD3qM-e}y-}JfJVPE0B7aM!F52q|_HBgsa1F#^k)HH4(d?xZ97~r=_}`3JdjK{i(;# z`n_zx*k+P{k2 zzq%pF#E{^WPzBTI0HSdM6&bRXZ%kqr+`pX@pSpu&e+6N_LTMa5_dtcHj#d?-hlVQ0 z2}?hNH_+}fi>6sv2mVUKTb9x z9Rt>zc+TQ(BYYHc+~?Bzt0{|h2jT58R%B%MnsRcqa@4RnWi-e?%_jVi zl~5EoA}$dmfT-4s4)|saoWiB6HxeGPA`MGJrp;zak>It+6ujFTqQ#Y7E^+Mxvf1-P zTjKLF2fC-pQ(tu*L|$Iy7bqT8jO*V#m`AnUW(HY3=L6J?`mEMbtvxYvhnX2=? zhh^#dJpQd~=u)!o_}PZ#R#i6*no7Cef<_=wEtDA4xN)hUxbz7M)sb z$@i^q@I~+>E=bh7vhM_t6#%$EMJkk4YI=Yv*lJiQV!q7|43-I`^BJt0>(n$p6RP}n z97{;tdHtSr@MLN>n+r-^C#{Wsj#z=?KGOLyg~(#Hr`dh=#o|?;|FwOJcShYpgMZ)j z#*byjK?6gv{WLBSW%Y%t=fiiqjZ8HyQ42pDhvlo9{acwmD?y>H>E)w8^FYVMsgpLP z%PuEM$yY6fMHT2n1`CUBxpO}*2Oh>iEYzBFM3gPJ$hs#`e?!Nm-1mzuK@d>^QTO4p z{Ws5xd5_F0728>f{y12=HFUV6ti@5Rt;ptTe5UUjvr-pT2#6k78z?D@6hF$twW;Yk zkk=Vq4)%Bw#@q#|E%yykDL$C`%D~$>B+QJS2R24WVe93e6)o~u&f+MqVWcLCHmmP# zwR{$6uC1AQPb+&y`q^Q-9ahF6AV!NiSH@~GX(ed6xyu9AB$I3_ zqw!C>%FS#<)b#VFhViBMqmD%b9*0VWvBM_S=3~@&&?6Ecl6uKEZTZF`6iZ9@*`;GC zR8vG#x|O}Jy>N22b_BRJ>z_086}4zjTTyZ#gUlqlW7`PBqrI}EXYB3zBHW))9H`6U< z!1TWANP;k3ky9kCJvAm?m9t-eo>KaaL4rejHC|hmh}PE*&$3aBA^-OHe{`YXbuJXZ z4@lBrYYntlnrHBfGXQ)h&~Cnrg?K+zP3G1)D`gG>nNn|z9!O<^=(bKn<<{N`zcx+H zLCLVDeL_$WBZC2s(BYcOK^_O)ysTrL7Rc$&{m*=L(E&=-~5N*t5JaFd9t~|4&G}^3-gR~nn z64$w%=NyVA97(%&OYz`@*MmHTmk!@`EtB{Z4n+fafj%tWI7Kf8GnLZ-v|0(=Tv%Oa zRQ(-)KdRtu{O5&!XUfpx_mqEE>~xqBqnmd}uwdWDqI_@eOHe)=-v#5`vNPpnYrLBt zi_!X4rDugZuIT4VVV}i7t+&0WcJyP|C6C2(@8_))VfX_!rK1{v)Da0f=v6#Ln zpDg7s1p2q1dEQ}XvlrIrE0M$@h{2WR`HN~{3M27iL#m)ok@zkjJsN9hk-RJQ%ywz*L_J^49|j&mA==X zIm$$JMDE3)pJfJGA&#i#6)rSjDuMw!YnMHlx7?q!k;COYX*k80tfe_*Ae zwh3?DIx9d+YDMQP6{a$YFiBeZ3lX$)YD z4g^N;e_1$TOwKT9;b3J_Y}^mxU@HLlClL1E+g{Ca`OFgJM-JUryPdRe1OexN_SS74 zG~)7dIPw~B{Tlm&Y;X<-xqfw@iEzN`jDm(J+#>0xV4cd6_~r^fEUq2OU#dw% z_dTEP620TNGEr7A+~V?um)pe?$B_O& zIYk8Yk*fYAOLR*3)dichKZ?Q}JMr@y5_@$pX}f7KS$7f{wLCN75%=d1_*jI?=yrsj zj>_S1_|(>xA>u_2rD3z!tJ1SS@{rVY@_6v@ChHDPaqb+M>YJ>quD+0Mkp4GDqaMsY zfQ`OIAhOB<-7DaL9+37wo;q+@U@bX~2w80mVp0)r6pVN$cdx4vS5K1(=xXC#;5ta> zW!&qEa5mN0oYF$eKp8GttKmxK>LMiU&E(y(*=YCREfUzB|G4&hXGPvwo{0lYm?i4)*hl~*rxl_pTE#MC4wo(a9g+DUMQ%{|6?a$ZrX`Y z8YM4Yz4WJBYnT?Pr)2a;kt-^oDznv!FtlfNVZ?DrYU-~8dxA9}iMzDAK4uZ-$}L1o zf19t#6qacyTW+{$7+n3WSV*uVFpACnBb<$}yi8puOsyCP9pS(SMAdZm57+!i2ADBM z<<=M5D8~P2vHl`3j1k~H(Oz4!Kcj!SpT>U_p)U$Be84o7a|wVk{aSk1(^lH(>%bee zdCvmYa}(u5z&Sg&@O>H#tuNu%k0cVkCKzCfMk@O`E^+q#Btq<^hGgO;a;<>DAkR~* zBC7URhAS2Ln-LBD)*y3GZ*%St+w#SbbIF~tT#hz>aM;vEi7r~7zl_YOZ)IbyZ&D7G{l>dLhExavJ2XlfZa zZ8x@>YO~ff>)=MDpY$Qs4E6|#q`lT|aT8&&fW>N`Bj_iW|&414pL?rdm#D5{w4*4gJ9HL*C`A z;T%94aHCN*6;3YL6AsaZ`5eHI8-HX+pRa+QsC|&|vvL`mj%s*ud0vXE!sEFuYgpjG zQu_g}LJ$H~KNtdxnIG}ggF#zl++y=*RI7s3TX4UDfOxsT4?Mru$Mx|5hq1dB;KDuj zykNk961{Hc$xfY=_a+pk4FF0o5B%~#xVSzuq)6o(c+^&-G5f?PZcyYSNk2(gwV_L6 zRnK+c#d+S=DarD~bmRr16XvA1MTe{CE`kf(c~PCL8!qcyrg{XRm&at>A4}bY!pd5f zTsn$8f>f?i-&(rYbTXrw(>|?o%;4b%FgQgf$3|nIJpgg+n~K_&zMCN@>>(w3G<}`6 zUe2`>Ti!x>YZM)6vM3*mT^c4twgbdKPI+=(jI#b~A0LhbyzTXhh8C)_v3B=mt^l?PJTf|&oO z7#h|>20mpe1LF3l;_YMg7)1UI8CSoO2f{!r{WMYx7v-s`SrEr2otJSbQ^=~4Y}#}@ z`dmCRoK00a?$inA8wQ45mj1&T!Q%gHVE;alz}?0qIEnPH{_|-VygB+;{s|OG=8<8@ zprf;zK?oSPC0ln$dO>&+PJBlFmL+x7L%wiAs40sU)h9x;N4^3q>QMF}V~iYBHN1a& zGk>qiyp5o~;-CNWE*Il>EfysK^{#@B9VjV+^^K(cA(Q2p(QoD(%A(G8$A%wbHPBqINB|SYKJ{K#46D|7*PO-Z@CyY6bgXKe84`PEW zK?vTuv-G-f{xLC*df3c(S{Nq`%^qW^af%wM2Nb|uQSIs zP97xES=00lbye$Hf7ky1ifnl?2b2@rr|a3T@2>yeaFVvCTUUe`wc{wHuIhF$P& zZ_o=qOpsmB3S44xfKu>MN=v6Sg!s@d6lMN5m^A6pyLw}#OZtZm%C~N%-bdykp*PLr zH__FM(R-AR?}3MRQ>PU|M&3)$7WK1`Eq)=_W`b8Gz`}&BvXq zN%wOwKO*kLDyQ#=4DEz?>T?7B+SE0Z8E!2H|ak@9bECrXF_gSrfq@J9AR ze+gx%Lhwn5mjX?bMWQKT6aiB92!p&#eY>hgUOmBhpj*?x8ksk0Uj?gKPZ7+l6h#Ro zpaeQ00}>z#xE0sQ{X$U6PO{1p{M~Lk8UG=Qh?Z_c6Ny&*t?sZ#_LBH*mhUv}zMeYU zlK;*uW#mj_I{WFs6tD92cRIlBMn{dnZHCy;7Akea z1Ke;rOEVFx2Imd*u+y}GszSPu0f;ycH4r3%-ktvyC!S5;kS%eWDrGHTdMu8%N+{SS z4YG`qyBr}3mhP45hN>A3OM3%W!TW&QU4+}a0@#Bmcu4}9^doLlqqeVi;y5||4ios6 z^igEwOweA=ErbK~W<{l+lnf;s0Mpe{cOiLCWpZOAY_#f#F?$+{E!_ zGbzbUiwaYUeIcQ~_IkDjyrcEp=uzJIO)YtSh)J@It^^XxPHF%}tprin1Zy&qUl86{ z;DQ<|E>Xm;lD}s11w9WfAXu1>lU>M8s`IAbAv@>ZfGDbCSK28_-<2Ql z5Ec0?O;D2Hsu}|+3og-jA!K$l!hC_lnfL+pxCz*EyXukm9~9{3XK{j zied^5Hya^IN>Wv0-#F}Q2)HeRIYP{iKQh#gPLqAbnLywIv7ckYl;%p)f5;Vl zYClprgWbX>U)B?4HTH%(-XpSnh@6yMuyiC@ak)mFZ?5o-lOaT-LLr>(K1@(F{h`E`> zNg&hG<&{`EfgB5fG)HPeHc$nf9xP^Zg&Fy+r)QSrq~O&I?nUb6JhymJhcJZn(97UM zYo9Vg3E>Nh)ZwJtV|D?%5ifyjZ&Rbay$5nxZi_n=yITWj&_K3#ejz0R`KV0ZCM>&) z?)QtH_wnW^v^SG15@IQygaj0Nio-pzsW*ijeYD-@rZ|KYYYN_s4SQA3JCtu+n~5RZ zRZ)N=nS7Zj7RLhuDYQHqAX}C|GtBLY2(79bETeu_Sn-2sx(q2EjUttHzI2rskYk_g z3D$yqpd{kssA1B>?wR=&i&LxxWv|Sj{Av~varPUk@sadv)65z*P>^+oLo_!B0G zpk?uV_}!+Bx}u>aJII50+PXRI73|M8!tZ}CAI0Hb}8HyiQpIUn1py&OqzFdA#D zz4H(kA$vrk_41MRViKp6T_B>SM(^=RfWx<%kE&5qULnanPUjedI6m%iHnW6DHw7KF znf#KfqfCLFb@#abQX~LF1T9+yE+?5+br%+z6!*SDENoxEqA22uQa}N1NLD|i5?zxE zjvRkahuqcl1IMx|7eW9{k*!~H9t`c44bUnL^KfRslyzQFX7_|hxoSj!E{U|yTns-& zq41Hd0RbSL5LNsQg8YeGM-e;WAzqh*sl#cf_Dm4zRO5GQ2*H;;zB|_I0e24U#T@T} zZ|%;R-Mu(3oJT*$8Zf2NZJJfDo}AH+Eq1NlO3H?g=_7+5_1h(11wW(Pc(`Uta%$Ly z75jD$v8om5lQVf{)%kKDCP8kH{s$HV_ZxN~b{~mDLFY5b83Fsyt>{CD`n&(^r!gMYB&W97}zPP;Cbr-1*S>Bk2dU@a3-Y@$!mc zz(*0c^zJ+3i|LHbSpE5{c>xQau~r|)#__Tky`u_8r#N*L*Sy{ZIWYo)P~pOvJIeary=~ZI zK+Yw^HBVhyxh*=?fOsv2>pSzJ(F(-OgD)>kSJI(3?u zDaD|p$;To4>|niP*UVB^7`mlFJU2Dd%Kh^JI(swTmq)>H&{B;YEuQ8{eTx6G2Ed=b zbkEfCySj6D&YC>qT;@2pg}F)z@mKry@G;Rkb|(a7wxq*3+*Ofj>W=OERkJ-tEOkAE z{swE?0x>tz693J$uWDaMt1AK@&tjiiGTQ%``{u3jOnp|VdA>Fo(2NOrTV$^=n#nn( z0dl|aDdM5yI^~gqBvZfZntum$Lqe~sO_bq}cO~c1RU66Hzs&jOwcp&hF|B1N(1BtXT zu_}0XC2JnfLSH`q1!y6phsezUDsYv+IDSsG7^G4^kU7Qu>5Z@0t8+mrIZ5nqUSapT z%*5#6xMWdr2;lxqbOD4M^_70+=bqjZrH8j5BhkS2TNMFmGF(SZAqCXqP8@1zy2$G- zhl>mJt)F%cEx#F^4}MzJm|NGjEa|6ve&?^zbf|<1E+XOM`SmUd2?UwwFzsWZ^VneDc^=_9d5e0t1gl(!J$w?hqFY84G%mlEw0mwq?Z)Te4~ zNuKIN(O9QXS zOna`3a$L9%P=fNN2-32GXn|_LV7ho>;drg^BR5RPY&_Ho8cs2vTV#s#mi&v;mgu+L zf%`Y&@M#cJU6$ugS#$R@QhBX6u?8dInlc^7td&slWVr0AGO*Y=9KD^GI7-)En192h z;qJDBND%oK@|25^)DPj{`Uod#49OpRhi*z}7EajN@W3c@`p>~SkW~qB69Yz`=eP-9 z`UI`vk{}OB|LwovJ5Lz{wjtW4_!GHl1b)%;ZiA}}mkkpaN+m*t8?&9FW|ilyYJjl} z)7=;(TpV<$_a;2}4zbDf_0h7GycB>plD^6tCf=3jb|f?ZFsRfJ^a&~RaQK-3aLHZU zbaQ;rovJ}CbPCMUSqO-q=~ftdiSKdOJYHVC*V6N}V{*^<-gu^9OlH-BPqRj<2Qjjz z!ihzzvz5!6DPua+k6%?{UipP8Ev{TI6#~|-ffc58s&s^gg|6M= zn^T?7iiEpP2Y=wo)}3W1M7x+lQYS=PL}9B@OG)!j3M>9Rg70ili=+7PrwYqysK?3Y z8lLfa5o%HbO`Z=G&Y7a1){!0er10+N-*tuOCh|MM@vNAaO_zDU>88k zHZ*L{{R)ufJlxc|wJYU51qbbn*Y=<*Ea)*Tw!2=H6S>y*sw75S23QZ}KC;?-J?aIq zCkmnBl5e81P$mS0iQbIO>95ZE}^5rv2KE8r1lUcO$Hr3 zP1a?PKjqAKN4Znb4IK5G+xstTrHjJ^hBuv7mK0fZOu%oP!oA(SS?i0|Yc2OX?2So1 zZYgVA_4S;_OtlEfYU4SE*zcI#{CYLH2x$g#RL@Q&LuK3=v7vcxk}K01Ve?DLNc`3(JSG-5)Hdtxt#*!5y&M1$Per z$+4p7AYvm>ou`Yk%oEi*2Wp5fuKILACbphXeW(1J%?Os36Yv7Sh-*4XRui zXt3&HbYO&{HQBo6E&g~0n11yoWBP>z_!jqWD(X+Y=>3ZRLsE7|GmFiH&F3N96cF%n zDeSG~U0BuVQ2b32*!}LkJG7strWo{oOp>~XGG7Z_^DxkM6QMRo+*9@SSI~N_;Lc#C z=v05yvyNkKTjGi4pKo^S9fTFzPZkBCj0VE39ei_2;ax4m^VCDx#Kolyc?rq2G zz4ZN0JsMnvHD48LE5^3;JlrBKYhD&p4+5+@>{~pn8hWOQG8qx{%L`@~wZacC zCClagn7(;Fls0Q?$#uO*_v!vr?yPk@~nJWS5*T*Cr( z!Ne<|8|-zl#jHcGk(0TIvGSzV2SV{lfhxelfG6{cl|A(OfcNSwBIV>bd zVK7RLh4!-Zzonq)Fc~QFrW6@EEr6;%TQR(6H=Eog&I1?qTQ6#(_&a`1wo!#mobRpz zEmm3@l|dtwbD(}ASiUH=M}03v_VxjEd?0q<1ddH#yih`?gGa*C03DD6^m@BQE{zOs zelyPL;TV~AaM3#`G;@w;Rjr;-2+|CcWY9*iEuB=`)>*JU0?1`CbjVXqJ5hQtOUC6!K!9nV-=otzr~RVbJ4NfV z{EFY_?G2q0H_z~T+Cr}@T0?{qSk0IrLC#8Q5! zqe=`1?ctQVk+|~nlYp>{U+Wq3kCgkqm(KrV>Ky~)j<)ab7){bR4cc&G+n!)zJB{5W zjni0dY}>Zk*lKLsHviM!dw$A=zPgX6?eba%zlWAYUsF~3iF=m0TJY_uzqwPy#=3N zMGtc&L_q-=MI0QDd%2JfL;5AMJ?i=X>4EI6_WTbwQJ)@LmYZ5m$>k=EiyB06j}lxc z{3btYIXg$^fE_5JC_J0$jYKgN-&HRUi9|?N2co;FNocTGCMBpf;dmFMD!dY&9}aa)W^PvODpkmom!^9~#m^(Qg`osv-Y}C=#5xGFbYMti6tt4Q7|>ELc%rx|P%B6{ zYZVv^No6_l>OG_ay&|-|ULO;gSaHa5`i-keQ;-x1e5p5mgt?Ug;Pn()Wpg2p^X=r@ z@GOyzI-Kzv2MkIWTHd0vp}R57Ely8L!Uz$n7ko`0j`vy^L?-vCI4*6g5@9e#GYc`< zmXXvwd2~t$dz;8hP9*um2Ow{QJ(TyDRdF!c^%>tny$g-e%&1mTj~gEQ|HN_?%m5)+ zxE%0z`w#EhOZ~GpAMkodkBdQdo#w~7}t#&O3+59Q@l4;18}ezF~-t*2b9+p`MO+0 z1lnB9&8U+*`qLs!x1dC~ivIldPkd~{)YQ;BHOCsWq6JnmHAb*xip*KTvTWpvUkFB7 zph>_cm?_c-rkEuUNFk_(mIsC*)s*^Wuc z$K4~osHC53hNx#(5VS|34BAnriWL=3*lwLX_11Htj~K76=L6k{ocA~b0Q+Fp*OVt3 zjI#7?Ovmnin2M3N*fy@AC0nEkmK_Aj#s6M3#FEmuXuS&Y*R{8ii_an84FNXD4{Udi zZW8Rlv^~Qh0)c~bh_2S(88HOE^TjJ&AtI!aMfpJ40YwnXohLtR!bsO|4|ySY zVa2{1tk(y>u^&&YO5{2sd%{ELjD|qQJhe`J{rK>vcQ>TnNIp=n_B}6^`D$!#ZU`rk za8AobSQL;@&M00C5|i>K@A!-sKhadeTkI zW7a_Nggh!G*~AD1=tqX*6C@l%Ho_>vK4<=o|9D*5q@#3|ZT9TbfY4acujHcIo>p!% zyrQK1)`>L0$;pDV^X2;dbw~C=cDYli^I>TO#;Po~)bg=Ibfs7fuEJ=9TLvBlDbp^5 zVF;Y~iTwnVBvulhMmVI2t?IFUEdE6P=NUL*ZBmIM6nh9l{r}B=H0B79XYECWTwPKI_hn?Q9|qZ zZu>*o0Qt{_^4jGJsg_&aX>#juznYScoNT{b7#ienbKrgtk<=?enG)43$SF7{B-ka^ zz?KMw^e0vgx~T(3GDV(J2vxz-Z|YyOT;^$cAzy^i>CH$VqJ*2We#vC__!TKEW8@IU zJV9$zP%A>x6id47mSDj(n18jPPK?uxL9ik3G`mhKmT5mO=|K!i2qGTTa*+DDgGMvN zMf+21Ps!$^?uIw;c_-D}dujk4z&{cA=>u;Mq#nRM1e%%J4mvCH+7U(Cg=a_3X9^!u z6R2@Yv=Y%K%mlce$R(Rr-OW-#DRj|S8FW&$Prod!r$rFFKh>JtFcc4|!O_X?+_dR`%nr4%PoXx&@h5L8@#H)g9$wFp24Od zaj6EA=UoKHM?>bQ$yYsuWv-#F`!e2|y%mDID4&4klb$<6`16q(gNb<~HD%YMn`Cn@ zArfbm7bhfdxq2(Xs6F>)#XKKV^MZQVS5jbc>=>q`I?E*HR$32rre}2Mqx$ zs!?KxTNA{Kq(H{^DXsVEd+eBDQU_zYZ{~-*Of5#$ezl}4j>pX0nHaFZSH^TaNruGS z!zKkOY^;en!NTl$foMw=9nHzsJ+o^b9SBjPeO$HV{9vSd{8d^X)P?N*yz$EJ^(t%) z4tfZ>)P=A|{I1NhvJ?6HN(hS9BY*91KYP8hAIDEO7mhNCn*jYWQ0xUrLL3j=u^Dd|&nx(h>*_bgVlnK{ zx<=Nntp}8<%DT*rSmD44<*d5mDbp5<0rQ3Bh#h~MZd*Sb$0#|p+;)S3Z>Q|c%-uiX zjBw^?6R>2(T1$5e9Td7xVrtE&M3T-Tzl}3#^1u+!+-_R8iVnv*sQLy$bc--UnXu64 z5jUt(vwe%6?yaPvf@8MNRa(j&i*S-+zt4C`2oLlOL>KsmfSpUGQshUic}CcDpwRZ<$#i7fON%eTi>p@=1o);)TU-!7p0qY7L~ zATSVYCKtQQ#Eg(8bXB+!?v{7Fq1x+7-g^FUM7`KbCGdFV9@|vk+18DSZxDHqgYC%D z2kW=0lyAzFduj5glnG1%yeis)>`RRtWVeLiWtE%i{5h4`W@CTK z&?)~x(jM+R4GeGY4-MB2v!1gV!7k>SV=FT;%;;xZDyl27$uW_QR;IzFGf_;#SamA3 zEX0Mc^)djh8boT*EZ%(2Aze-!Wnb4fvzl$WW60R4sHF}pQzvGyBF8&gWs zND@ZwYDZs*qao8h!H zpkeaFi!Vfm{i^nuV|;cSz&gjjI~5sGd9N-T?6-+IX69XqSUvB;htup^77n_=+J#Kl zZ5*94Q*qWjyg7@0YnqHq_0SZjS>rn=<+CoxU|F2Wiml-jm0%J!n1Y1Th}UH5HA1-( z2<+Ibb*N21U7h|TApi56>6Q?#RKN@K@sa;6Mq)e4LZ1|)>U-!>jmyP2Guw5l?azJJ zuRKj!1rIMd?w^{*8)6DvbGLq*-G~^)6!5&`=f5℘G-_er7Pi0u1sqtB?ox0M5Yd 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 literal 0 HcmV?d00001 From 80a0a9f9c299c09979da17c0d18721a328711a21 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 13 Jan 2015 21:29:19 +0100 Subject: [PATCH 082/481] M4A: Reduce number of prefetch blocks from 2 to 1 Testing revealed that 1 block is enough! No audible artifacts when seeking randomly through the file. --- plugins/soundsourcem4a/audiosourcem4a.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index d3d855c8da7..ce7ba40c7a1 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -30,8 +30,8 @@ const MP4SampleId kMinSampleBlockId = 1; // Decoding will be restarted one or more blocks of samples // before the actual position to avoid audible glitches. -// Two blocks of samples seems to be enough here. -const MP4SampleId kNumberOfPrefetchSampleBlocks = 2; +// One block of samples seems to be enough here! +const MP4SampleId kNumberOfPrefetchSampleBlocks = 1; // Searches for the first audio track in the MP4 file that // suits our needs. From 03d16a5e76d1d60628db1e85612e549d6d3213fd Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 13 Jan 2015 22:52:08 +0100 Subject: [PATCH 083/481] Align MP3 and M4A implementations --- plugins/soundsourcem4a/audiosourcem4a.cpp | 50 +++++----- plugins/soundsourcem4a/audiosourcem4a.h | 2 + src/sources/audiosourcemp3.cpp | 110 +++++++++------------- src/sources/audiosourcemp3.h | 2 - 4 files changed, 71 insertions(+), 93 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index ce7ba40c7a1..e269550659f 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -174,13 +174,8 @@ Result AudioSourceM4A::open(QString fileName) { * frames2samples(kFramesPerSampleBlock); m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); - m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; - m_curFrameIndex = getFrameCount(); - // Seek to the beginning of the file - if (0 != seekSampleFrame(0)) { - qWarning() << "Failed to seek to the beginning of the file!"; - return ERR; - } + m_curSampleBlockId = 0; + m_curFrameIndex = 0; return OK; } @@ -209,14 +204,22 @@ bool AudioSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { && (sampleBlockId <= m_maxSampleBlockId); } +void AudioSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { + NeAACDecPostSeekReset(m_hDecoder, sampleBlockId); + m_curSampleBlockId = sampleBlockId; + m_curFrameIndex = (m_curSampleBlockId - kMinSampleBlockId) + * kFramesPerSampleBlock; + // discard input buffer + m_inputBufferOffset = 0; + m_inputBufferLength = 0; +} + AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (m_curFrameIndex != frameIndex) { - const MP4SampleId sampleBlockId = kMinSampleBlockId + MP4SampleId sampleBlockId = kMinSampleBlockId + (frameIndex / kFramesPerSampleBlock); - if (!isValidSampleBlockId(sampleBlockId)) { - return m_curFrameIndex; - } + DEBUG_ASSERT(isValidSampleBlockId(sampleBlockId)); if ((frameIndex < m_curFrameIndex) || // seeking backwards? !isValidSampleBlockId(m_curSampleBlockId) || // invalid seek position? (sampleBlockId @@ -227,17 +230,12 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { // need to be careful when subtracting! if ((kMinSampleBlockId + kNumberOfPrefetchSampleBlocks) < sampleBlockId) { - m_curSampleBlockId = sampleBlockId - - kNumberOfPrefetchSampleBlocks; + sampleBlockId -= kNumberOfPrefetchSampleBlocks; } else { - m_curSampleBlockId = kMinSampleBlockId; + sampleBlockId = kMinSampleBlockId; } - NeAACDecPostSeekReset(m_hDecoder, m_curSampleBlockId); - m_curFrameIndex = (m_curSampleBlockId - kMinSampleBlockId) - * kFramesPerSampleBlock; - // discard input buffer - m_inputBufferOffset = 0; - m_inputBufferLength = 0; + restartDecoding(sampleBlockId); + DEBUG_ASSERT(m_curSampleBlockId == sampleBlockId); } // decoding starts before the actual target position DEBUG_ASSERT(m_curFrameIndex <= frameIndex); @@ -252,10 +250,11 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { AudioSource::size_type AudioSourceM4A::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + if (!isValidSampleBlockId(m_curSampleBlockId)) { - qWarning() << "Invalid MP4 sample block" << m_curSampleBlockId; return 0; } + sample_type* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFrames; while (0 < numberOfFramesRemaining) { @@ -322,13 +321,14 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( } // consume input data m_inputBufferOffset += decFrameInfo.bytesconsumed; - // consume decoded samples + // consume decoded output data pSampleBuffer += decFrameInfo.samples; - const size_type numberOfFramesDecoded = samples2frames( - decFrameInfo.samples); - numberOfFramesRemaining -= numberOfFramesDecoded; + const size_type numberOfFramesDecoded = + samples2frames(decFrameInfo.samples); m_curFrameIndex += numberOfFramesDecoded; + numberOfFramesRemaining -= numberOfFramesDecoded; } + DEBUG_ASSERT(numberOfFrames >= numberOfFramesRemaining); return numberOfFrames - numberOfFramesRemaining; } diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index f2fecb79a41..7687e18e9d7 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -43,6 +43,8 @@ class AudioSourceM4A: public AudioSource { bool isValidSampleBlockId(MP4SampleId sampleBlockId) const; + void restartDecoding(MP4SampleId sampleBlockId); + MP4FileHandle m_hFile; MP4TrackId m_trackId; MP4SampleId m_maxSampleBlockId; diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 01b852d5b89..50596f7895a 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -46,7 +46,10 @@ AudioSourceMp3::AudioSourceMp3(QString fileName) : m_avgSeekFrameCount(0), m_curFrameIndex(0), m_madSynthCount(0) { - initDecoding(); + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + mad_frame_init(&m_madFrame); + mad_synth_init(&m_madSynth); m_seekFrameList.reserve(kSeekFrameListCapacity); } @@ -95,7 +98,8 @@ Result AudioSourceMp3::open() { } else { qDebug() << "Recoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); - } mad_header_finish(&madHeader); + } + mad_header_finish(&madHeader); continue; } else { if (MAD_ERROR_BUFLEN == m_madStream.error) { @@ -201,28 +205,12 @@ Result AudioSourceMp3::open() { return OK; } -void AudioSourceMp3::initDecoding() { - m_madSynthCount = 0; - mad_stream_init(&m_madStream); - mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - mad_frame_init(&m_madFrame); - mad_synth_init(&m_madSynth); - m_curFrameIndex = 0; -} - -void AudioSourceMp3::finishDecoding() { - m_madSynthCount = 0; - mad_synth_finish(&m_madSynth); - mad_frame_finish(&m_madFrame); - mad_stream_finish(&m_madStream); - m_curFrameIndex = getFrameCount(); // invalidate -} - void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { - m_madSynthCount = 0; mad_stream_buffer(&m_madStream, seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); m_curFrameIndex = seekFrame.frameIndex; + // discard input buffer + m_madSynthCount = 0; // Calling mad_synth_mute() and mad_frame_mute() is not // necessary, because we will prefetch (decode and skip) // some frames before actually reading any audio samples @@ -230,7 +218,10 @@ void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { } void AudioSourceMp3::close() { - finishDecoding(); + mad_synth_finish(&m_madSynth); + mad_frame_finish(&m_madFrame); + mad_stream_finish(&m_madStream); + m_madSynthCount = 0; m_seekFrameList.clear(); m_avgSeekFrameCount = 0; @@ -265,7 +256,10 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( lowerBound = seekFrameIndex; } seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; - } DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || + } + DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); + DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); + DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); return seekFrameIndex; } @@ -274,27 +268,30 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { // qDebug() << "seekSampleFrame(): frameIndex =" << frameIndex; DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (!m_seekFrameList.empty()) { + SeekFrameList::size_type seekFrameIndex = findSeekFrameIndex( + frameIndex); + DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); +// qDebug() << "seekFrameIndex" << seekFrameIndex; const SeekFrameList::size_type curSeekFrameIndex = findSeekFrameIndex( m_curFrameIndex); // qDebug() << "curSeekFrameIndex" << curSeekFrameIndex; DEBUG_ASSERT(m_seekFrameList.size() > curSeekFrameIndex); - SeekFrameList::size_type nextSeekFrameIndex = findSeekFrameIndex( - frameIndex); - DEBUG_ASSERT(m_seekFrameList.size() > nextSeekFrameIndex); -// qDebug() << "nextSeekFrameIndex" << nextSeekFrameIndex; + // some consistency checks + DEBUG_ASSERT((curSeekFrameIndex >= seekFrameIndex) || (m_curFrameIndex < frameIndex)); + DEBUG_ASSERT((curSeekFrameIndex <= seekFrameIndex) || (m_curFrameIndex > frameIndex)); if ((frameIndex < m_curFrameIndex) || // seeking backwards? - (nextSeekFrameIndex + (seekFrameIndex > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jumping forward? - // Implementation note: The type size_type is unsigned so we + // Implementation note: The type size_type is unsigned so // need to be careful when subtracting! - if (nextSeekFrameIndex <= kSeekFramePrefetchCount) { - nextSeekFrameIndex = 0; + if (kSeekFramePrefetchCount < seekFrameIndex) { + seekFrameIndex -= kSeekFramePrefetchCount; } else { - nextSeekFrameIndex -= kSeekFramePrefetchCount; + seekFrameIndex = 0; } -// qDebug() << "restarting at nextSeekFrameIndex" << nextSeekFrameIndex; - restartDecoding(m_seekFrameList[nextSeekFrameIndex]); - DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == nextSeekFrameIndex); +// qDebug() << "restarting at seekFrameIndex" << seekFrameIndex; + restartDecoding(m_seekFrameList[seekFrameIndex]); + DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == seekFrameIndex); } // decoding starts before the actual target position DEBUG_ASSERT(m_curFrameIndex <= frameIndex); @@ -328,19 +325,16 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); sample_type* pSampleBuffer = sampleBuffer; - const size_type numberOfFramesTotal = - math_min(numberOfFrames, samples2frames(sampleBufferSize)); - size_type numberOfFramesRead = 0; - while (numberOfFramesTotal > numberOfFramesRead) { + size_type numberOfFramesRemaining = + math_min(numberOfFrames, samples2frames(sampleBufferSize)); + while (0 < numberOfFramesRemaining) { if (0 >= m_madSynthCount) { // all decoded output data has been consumed DEBUG_ASSERT(0 == m_madSynthCount); - const unsigned char* const madThisFrame = m_madStream.this_frame; if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { - if ((NULL != pSampleBuffer) - || (MAD_ERROR_BADDATAPTR != m_madStream.error)) { - qDebug() << "Recoverable MP3 decoding error:" + if (NULL != pSampleBuffer) { + qWarning() << "Recoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); } if (MAD_ERROR_LOSTSYNC == m_madStream.error) { @@ -355,36 +349,19 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( << mad_stream_errorstr(&m_madStream); break; } - } else { - // if not at the beginning of the stream - // the next MP3 frame must have been decoded - DEBUG_ASSERT((0 == m_curFrameIndex) || - (madThisFrame != m_madStream.this_frame)); } const mad_header* const pMadFrameHeader = &m_madFrame.header; - if (getChannelCount() != MAD_NCHANNELS(pMadFrameHeader)) { - qWarning() << "Corrupt or unsupported MP3 file:" - << "Invalid number of channels in MP3 frame header" - << MAD_NCHANNELS(pMadFrameHeader) << "<>" - << getChannelCount(); - break; - } + DEBUG_ASSERT(getChannelCount() == MAD_NCHANNELS(pMadFrameHeader)); // Once decoded the frame is synthesized to PCM samples mad_synth_frame(&m_madSynth, &m_madFrame); + DEBUG_ASSERT(getFrameRate() == m_madSynth.pcm.samplerate); m_madSynthCount = m_madSynth.pcm.length; - if (getFrameRate() != m_madSynth.pcm.samplerate) { - qWarning() << "Corrupt or unsupported MP3 file:" - << "Invalid sample rate in MP3 frame header" - << m_madSynth.pcm.samplerate << "<>" << getFrameRate(); - break; - } } const size_type framesRead = math_min( - m_madSynthCount, - numberOfFramesTotal - numberOfFramesRead); + m_madSynthCount, numberOfFramesRemaining); if (NULL != pSampleBuffer) { - const size_type madSynthOffset = m_madSynth.pcm.length - - m_madSynthCount; + const size_type madSynthOffset = + m_madSynth.pcm.length - m_madSynthCount; if (isChannelCountMono()) { for (size_type i = 0; i < framesRead; ++i) { const sample_type sampleValue = madScale( @@ -413,9 +390,10 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( // consume decoded output data m_madSynthCount -= framesRead; m_curFrameIndex += framesRead; - numberOfFramesRead += framesRead; + numberOfFramesRemaining -= framesRead; } - return numberOfFramesRead; + DEBUG_ASSERT(numberOfFrames >= numberOfFramesRemaining); + return numberOfFrames - numberOfFramesRemaining; } } // namespace Mixxx diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index 578444287ec..8b07ccb8c32 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -71,8 +71,6 @@ class AudioSourceMp3: public AudioSource { diff_type m_curFrameIndex; - void initDecoding(); - void finishDecoding(); void restartDecoding(const SeekFrameType& seekFrame); // current play position From 0070fb88fe1146db9e0d23625a10e7f60b06488a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 00:41:50 +0100 Subject: [PATCH 084/481] MP3: Add missing 1st frame to seek list --- src/sources/audiosourcemp3.cpp | 41 +++++++++++++++++++++------------- src/sources/audiosourcemp3.h | 4 +++- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 50596f7895a..90addd86fd4 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -68,6 +68,16 @@ AudioSourcePointer AudioSourceMp3::create(QString fileName) { } } +void AudioSourceMp3::addSeekFrame( + mad_timer_t madDuration, + mad_units madUnits, + const unsigned char* pInputData) { + SeekFrameType seekFrame; + seekFrame.pInputData = pInputData; + seekFrame.frameIndex = mad_timer_count(madDuration, madUnits); + m_seekFrameList.push_back(seekFrame); +} + Result AudioSourceMp3::open() { if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file:" << m_file.fileName(); @@ -79,17 +89,24 @@ Result AudioSourceMp3::open() { m_pFileData = m_file.map(0, m_fileSize); // Transfer it to the mad stream-buffer: mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); + DEBUG_ASSERT(m_pFileData == m_madStream.this_frame); // Decode all the headers and calculate audio properties unsigned long sumBitrate = 0; - unsigned int madFrameCount = 0; setChannelCount(kChannelCountDefault); setFrameRate(kFrameRateDefault); - mad_timer_t madFileDuration = mad_timer_zero; + mad_units madUnits = MAD_UNITS_44100_HZ; // default value + mad_timer_t madDuration = mad_timer_zero; + + // Add first frame to list of frames + addSeekFrame(madDuration, madUnits, m_madStream.this_frame); + + DEBUG_ASSERT(quint64(m_madStream.bufend - m_madStream.this_frame) == m_fileSize); while ((m_madStream.bufend - m_madStream.this_frame) > 0) { mad_header madHeader; mad_header_init(&madHeader); + if (0 != mad_header_decode(&madHeader, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { if (MAD_ERROR_LOSTSYNC == m_madStream.error) { @@ -174,24 +191,18 @@ Result AudioSourceMp3::open() { } } - // Add frame to list of frames - SeekFrameType seekFrame; - seekFrame.pFileData = m_madStream.this_frame; - seekFrame.frameIndex = mad_timer_count(madFileDuration, madUnits); - m_seekFrameList.push_back(seekFrame); - - mad_timer_add(&madFileDuration, madHeader.duration); + mad_timer_add(&madDuration, madHeader.duration); sumBitrate += madHeader.bitrate; - mad_header_finish(&madHeader); - ++madFrameCount; + // Add next frame to list of frames + addSeekFrame(madDuration, madUnits, m_madStream.this_frame); } - if (0 < madFrameCount) { - setFrameCount(mad_timer_count(madFileDuration, madUnits)); - m_avgSeekFrameCount = getFrameCount() / madFrameCount; - int avgBitrate = sumBitrate / madFrameCount; + if (1 < m_seekFrameList.size()) { + setFrameCount(mad_timer_count(madDuration, madUnits)); + m_avgSeekFrameCount = getFrameCount() / (m_seekFrameList.size() - 1); + int avgBitrate = sumBitrate / (m_seekFrameList.size() - 1); setBitrate(avgBitrate / 1000); } else { // This is not a working MP3 file. diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index 8b07ccb8c32..aef3612d7ef 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -54,7 +54,7 @@ class AudioSourceMp3: public AudioSource { /** Struct used to store mad frames for seeking */ struct SeekFrameType { diff_type frameIndex; - const unsigned char* pFileData; + const unsigned char* pInputData; }; /** It is not possible to make a precise seek in an mp3 file without decoding the whole stream. @@ -66,6 +66,8 @@ class AudioSourceMp3: public AudioSource { SeekFrameList m_seekFrameList; // ordered-by frameIndex size_type m_avgSeekFrameCount; // avg. sample frames per MP3 frame + void addSeekFrame(mad_timer_t madDuration, mad_units madUnits, const unsigned char* pInputData); + /** Returns the position in m_seekFrameList of the requested frame index. */ SeekFrameList::size_type findSeekFrameIndex(diff_type frameIndex) const; From c09af15ba08dbef206763b991d3e39f99a4428dc Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 00:43:32 +0100 Subject: [PATCH 085/481] MP3: Reset MAD decoder after seeking to avoid audible glitches --- src/sources/audiosourcemp3.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 90addd86fd4..86f50e08677 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -217,11 +217,24 @@ Result AudioSourceMp3::open() { } void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { - mad_stream_buffer(&m_madStream, seekFrame.pFileData, - m_fileSize - (seekFrame.pFileData - m_pFileData)); + // Reset the MAD decoder completely. Otherwise + // audible artifacts and glitches occur when + // seeking through the stream no matter how + // many MP3 frames are prefetched! + mad_synth_finish(&m_madSynth); + mad_frame_finish(&m_madFrame); + mad_stream_finish(&m_madStream); + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + mad_frame_init(&m_madFrame); + mad_synth_init(&m_madSynth); + + mad_stream_buffer(&m_madStream, seekFrame.pInputData, + m_fileSize - (seekFrame.pInputData - m_pFileData)); m_curFrameIndex = seekFrame.frameIndex; // discard input buffer m_madSynthCount = 0; + // Calling mad_synth_mute() and mad_frame_mute() is not // necessary, because we will prefetch (decode and skip) // some frames before actually reading any audio samples From 07d6bd1d2b86f71c7963f8dc50c0bf4f18883578 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 00:44:22 +0100 Subject: [PATCH 086/481] Improve lookup of seek frame index --- src/sources/audiosourcemp3.cpp | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 86f50e08677..865cb78c4bc 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -261,7 +261,10 @@ void AudioSourceMp3::close() { AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( diff_type frameIndex) const { - DEBUG_ASSERT(!m_seekFrameList.empty()); DEBUG_ASSERT(0 < m_avgSeekFrameCount); + DEBUG_ASSERT(0 < m_avgSeekFrameCount); + DEBUG_ASSERT(!m_seekFrameList.empty()); + DEBUG_ASSERT(0 == m_seekFrameList.front().frameIndex); + DEBUG_ASSERT(diff_type(getFrameCount()) == m_seekFrameList.back().frameIndex); // Guess position of frame in m_seekFrameList based on average frame size AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; @@ -269,18 +272,37 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( seekFrameIndex = m_seekFrameList.size() - 1; } // binary search starting at seekFrameIndex - AudioSourceMp3::SeekFrameList::size_type lowerBound = 0; + AudioSourceMp3::SeekFrameList::size_type lowerBound = + seekFrameIndex; + while (0 < lowerBound) { + if (m_seekFrameList[lowerBound].frameIndex <= frameIndex) { + break; + } else { + lowerBound /= 2; + } + } AudioSourceMp3::SeekFrameList::size_type upperBound = m_seekFrameList.size(); - while ((upperBound - lowerBound) > 1) { - DEBUG_ASSERT(seekFrameIndex >= lowerBound); DEBUG_ASSERT(seekFrameIndex < upperBound); - if (m_seekFrameList[seekFrameIndex].frameIndex > frameIndex) { - upperBound = seekFrameIndex; + while (seekFrameIndex < upperBound) { + AudioSourceMp3::SeekFrameList::size_type medianBound = + seekFrameIndex + (upperBound - seekFrameIndex) / 2; + if (m_seekFrameList[medianBound].frameIndex <= frameIndex) { + break; } else { + upperBound = medianBound; + } + } + while ((upperBound - lowerBound) > 1) { + DEBUG_ASSERT(seekFrameIndex >= lowerBound); + DEBUG_ASSERT(seekFrameIndex < upperBound); + if (m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex) { lowerBound = seekFrameIndex; + } else { + upperBound = seekFrameIndex; } seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; } + DEBUG_ASSERT(seekFrameIndex == lowerBound); DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || From 2dca446c5e290aa88698f1d41edab9e39d030f32 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 00:45:06 +0100 Subject: [PATCH 087/481] Minor cleanup --- src/sources/audiosourcemp3.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 865cb78c4bc..577ec9168bc 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -185,8 +185,8 @@ Result AudioSourceMp3::open() { // check for consistent frame/sample rate if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { qWarning() << "Differing sample rate in some headers:" - << m_file.fileName() << getFrameRate() << "<>" - << madSampleRate; + << m_file.fileName() + << getFrameRate() << "<>" << madSampleRate; return ERR; // abort } } @@ -311,16 +311,13 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( } AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { -// qDebug() << "seekSampleFrame(): frameIndex =" << frameIndex; DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (!m_seekFrameList.empty()) { SeekFrameList::size_type seekFrameIndex = findSeekFrameIndex( frameIndex); DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); -// qDebug() << "seekFrameIndex" << seekFrameIndex; const SeekFrameList::size_type curSeekFrameIndex = findSeekFrameIndex( m_curFrameIndex); -// qDebug() << "curSeekFrameIndex" << curSeekFrameIndex; DEBUG_ASSERT(m_seekFrameList.size() > curSeekFrameIndex); // some consistency checks DEBUG_ASSERT((curSeekFrameIndex >= seekFrameIndex) || (m_curFrameIndex < frameIndex)); @@ -335,7 +332,6 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { } else { seekFrameIndex = 0; } -// qDebug() << "restarting at seekFrameIndex" << seekFrameIndex; restartDecoding(m_seekFrameList[seekFrameIndex]); DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == seekFrameIndex); } @@ -365,9 +361,6 @@ AudioSource::size_type AudioSourceMp3::readSampleFramesStereo( AudioSource::size_type AudioSourceMp3::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { -// qDebug() << "readSampleFrames():" -// << "m_curFrameIndex =" << m_curFrameIndex -// << "numberOfFrames =" << numberOfFrames; DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); sample_type* pSampleBuffer = sampleBuffer; From b3597a0955d86b6158c5232489d6dd8f16f6117a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 01:27:36 +0100 Subject: [PATCH 088/481] MP3: Workaround for unexpected behavior of mad_frame_decode() As suggested by daschuer. --- src/sources/audiosourcemp3.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 577ec9168bc..810563e26bf 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -370,6 +370,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( if (0 >= m_madSynthCount) { // all decoded output data has been consumed DEBUG_ASSERT(0 == m_madSynthCount); + unsigned char const* madThisFrame = m_madStream.this_frame; if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { if (NULL != pSampleBuffer) { @@ -388,6 +389,12 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( << mad_stream_errorstr(&m_madStream); break; } + } else { + if (madThisFrame == m_madStream.this_frame) { + // still at the same frame after decoding although + // mad_frame_decode() returned 0 -> retry (why???) + continue; + } } const mad_header* const pMadFrameHeader = &m_madFrame.header; DEBUG_ASSERT(getChannelCount() == MAD_NCHANNELS(pMadFrameHeader)); From 4bb6f2bdcd2bb83a27811cd81921cae5691d5305 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 08:05:01 +0100 Subject: [PATCH 089/481] Delete extra loops from binary search, that was bullsh** --- src/sources/audiosourcemp3.cpp | 35 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 810563e26bf..90f68971f2b 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -261,36 +261,21 @@ void AudioSourceMp3::close() { AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( diff_type frameIndex) const { + // Check preconditions DEBUG_ASSERT(0 < m_avgSeekFrameCount); DEBUG_ASSERT(!m_seekFrameList.empty()); DEBUG_ASSERT(0 == m_seekFrameList.front().frameIndex); DEBUG_ASSERT(diff_type(getFrameCount()) == m_seekFrameList.back().frameIndex); - // Guess position of frame in m_seekFrameList based on average frame size - AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = frameIndex - / m_avgSeekFrameCount; - if (seekFrameIndex >= m_seekFrameList.size()) { - seekFrameIndex = m_seekFrameList.size() - 1; - } - // binary search starting at seekFrameIndex + AudioSourceMp3::SeekFrameList::size_type lowerBound = - seekFrameIndex; - while (0 < lowerBound) { - if (m_seekFrameList[lowerBound].frameIndex <= frameIndex) { - break; - } else { - lowerBound /= 2; - } - } + 0; AudioSourceMp3::SeekFrameList::size_type upperBound = m_seekFrameList.size(); - while (seekFrameIndex < upperBound) { - AudioSourceMp3::SeekFrameList::size_type medianBound = - seekFrameIndex + (upperBound - seekFrameIndex) / 2; - if (m_seekFrameList[medianBound].frameIndex <= frameIndex) { - break; - } else { - upperBound = medianBound; - } + // Initial guess based on average frame size + AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = + frameIndex / m_avgSeekFrameCount; + if (seekFrameIndex >= m_seekFrameList.size()) { + seekFrameIndex = m_seekFrameList.size() - 1; } while ((upperBound - lowerBound) > 1) { DEBUG_ASSERT(seekFrameIndex >= lowerBound); @@ -300,13 +285,17 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( } else { upperBound = seekFrameIndex; } + // Next guess halfway between lower and upper bound seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; } + + // Check postconditions DEBUG_ASSERT(seekFrameIndex == lowerBound); DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); + return seekFrameIndex; } From b39046319a85505db788ea0b0c92026c6271a80d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 08:28:46 +0100 Subject: [PATCH 090/481] FLAC: Call flush() after seeking according to API documentation --- src/sources/audiosourceflac.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 926cd8dbaf1..6e6aa10cbac 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -128,10 +128,13 @@ Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame( // clear decode buffer before seeking m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; - bool result = FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex); - if (!result) { + if (!FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex)) { qWarning() << "SSFLAC: Seeking error at file" << m_file.fileName(); } + if ((FLAC__STREAM_DECODER_SEEK_ERROR == FLAC__stream_decoder_get_state(m_decoder)) && + !FLAC__stream_decoder_flush(m_decoder)) { + qWarning() << "SSFLAC: Failed to flush the decoder's input buffer after seeking" << m_file.fileName(); + } return frameIndex; } From 36ecca0a4c2869b86dd5105c623db9ae6cb3d40f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 08:29:31 +0100 Subject: [PATCH 091/481] MP3/M4A: Never try to read more sample frames than available --- plugins/soundsourcem4a/audiosourcem4a.cpp | 3 ++- src/sources/audiosourcemp3.cpp | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index e269550659f..108aa3bf405 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -256,7 +256,8 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( } sample_type* pSampleBuffer = sampleBuffer; - size_type numberOfFramesRemaining = numberOfFrames; + size_type numberOfFramesRemaining = + math_min(numberOfFrames, getFrameCount() - m_curFrameIndex); while (0 < numberOfFramesRemaining) { DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); if (m_inputBufferOffset >= m_inputBufferLength) { diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 90f68971f2b..b43fb48b300 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -229,12 +229,16 @@ void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { mad_frame_init(&m_madFrame); mad_synth_init(&m_madSynth); - mad_stream_buffer(&m_madStream, seekFrame.pInputData, - m_fileSize - (seekFrame.pInputData - m_pFileData)); + // Reset stream position m_curFrameIndex = seekFrame.frameIndex; - // discard input buffer + + // Discard decoded output m_madSynthCount = 0; + // Fill input buffer + mad_stream_buffer(&m_madStream, seekFrame.pInputData, + m_fileSize - (seekFrame.pInputData - m_pFileData)); + // Calling mad_synth_mute() and mad_frame_mute() is not // necessary, because we will prefetch (decode and skip) // some frames before actually reading any audio samples @@ -354,13 +358,20 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( sample_type* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = - math_min(numberOfFrames, samples2frames(sampleBufferSize)); + math_min(numberOfFrames, getFrameCount() - m_curFrameIndex); + numberOfFramesRemaining = + math_min(numberOfFramesRemaining, samples2frames(sampleBufferSize)); while (0 < numberOfFramesRemaining) { if (0 >= m_madSynthCount) { - // all decoded output data has been consumed + // When all decoded output data has been consumed... DEBUG_ASSERT(0 == m_madSynthCount); + // ...decode the next MP3 frame + DEBUG_ASSERT(NULL != m_madStream.buffer); + DEBUG_ASSERT(NULL != m_madStream.this_frame); unsigned char const* madThisFrame = m_madStream.this_frame; if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { + qDebug() << "MP3 decoding error:" + << mad_stream_errorstr(&m_madStream); if (MAD_RECOVERABLE(m_madStream.error)) { if (NULL != pSampleBuffer) { qWarning() << "Recoverable MP3 decoding error:" From 6bc379f14c6898fb16bd99afa1811cf2504f096a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 19:40:39 +0100 Subject: [PATCH 092/481] Increase prefetch for M4A from 1 to 2 (again) and document this decision --- plugins/soundsourcem4a/audiosourcem4a.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 108aa3bf405..638423b5c48 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -29,9 +29,18 @@ const AudioSource::size_type kFramesPerSampleBlock = 1024; const MP4SampleId kMinSampleBlockId = 1; // Decoding will be restarted one or more blocks of samples -// before the actual position to avoid audible glitches. -// One block of samples seems to be enough here! -const MP4SampleId kNumberOfPrefetchSampleBlocks = 1; +// before the actual position after seeking randomly in the +// audio stream to avoid audible glitches. +// +// TODO(XXX): Replace with the smallest possible value that +// allows accurate seeking in any audio stream compliant with +// the AAC specification! This theoretical value has to be +// confirmed practically by appropriate unit tests. +// +// For the time being simply use the current value of 2 which +// seems to be enough as experimental listening tests with some +// M4A example files revealed. +const MP4SampleId kNumberOfPrefetchSampleBlocks = 2; // Searches for the first audio track in the MP4 file that // suits our needs. From 91fb8820956f3389e675eeefc4469e6786f8a7ee Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 19:41:22 +0100 Subject: [PATCH 093/481] Metadata: Read only the first value of multi-valued fields --- src/metadata/trackmetadatataglib.cpp | 90 +++++++++++----------------- 1 file changed, 35 insertions(+), 55 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index b79b4d2ef5f..c497db23924 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -34,60 +34,40 @@ inline QString toQString(const TagLib::String& tString) { } } -// Concatenates the elements of a TagLib string list -// into a single string. -inline QString toQStringConcat(const TagLib::StringList& strList) { - return toQString(strList.toString()); +// Returns the first element of TagLib string list. +inline QString toQStringFirst(const TagLib::StringList& strList) { + if (strList.isEmpty()) { + return QString(); + } else { + return toQString(strList.front()); + } } -// Concatenates the frame list of an ID3v2 tag into a -// single string. +// Returns the text of an ID3v2 frame as a string. inline QString toQString(const TagLib::ID3v2::Frame& frame) { return toQString(frame.toString()); } -// Concatenates the frame list of an ID3v2 tag into a -// single string. -QString toQStringConcat(const TagLib::ID3v2::FrameList& frameList) { - QString result; - for (TagLib::ID3v2::FrameList::ConstIterator i(frameList.begin()); - frameList.end() != i; ++i) { - result += toQString(**i); - } - return result; -} - -// Returns the first ID3v2 frame that is not empty. -QString toQStringFirst(const TagLib::ID3v2::FrameList& frameList) { - for (TagLib::ID3v2::FrameList::ConstIterator i(frameList.begin()); - frameList.end() != i; ++i) { - const QString value(toQString(**i).trimmed()); - if (!value.isEmpty()) { - return value; - } +// Returns the first frame of an ID3v2 tag as a string. +inline QString toQStringFirst(const TagLib::ID3v2::FrameList& frameList) { + if (frameList.isEmpty() || (NULL == frameList.front())) { + return QString(); + } else { + return toQString(*frameList.front()); } - return QString(); -} - -// Concatenates the string list of an MP4 item -// into a single string. -inline QString toQStringConcat(const TagLib::MP4::Item& mp4Item) { - return toQStringConcat(mp4Item.toStringList()); } -// Returns the first MP4 item string that is not empty. -QString toQStringFirst(const TagLib::MP4::Item& mp4Item) { +// Returns the first value of an MP4 item as a string. +inline QString toQStringFirst(const TagLib::MP4::Item& mp4Item) { const TagLib::StringList strList(mp4Item.toStringList()); - for (TagLib::StringList::ConstIterator i(strList.begin()); - strList.end() != i; ++i) { - const QString value(toQString(*i).trimmed()); - if (!value.isEmpty()) { - return value; - } + if (strList.isEmpty()) { + return QString(); + } else { + return toQString(strList.front()); } - return QString(); } +// Returns an APE item as a string. inline QString toQString(const TagLib::APE::Item& apeItem) { return toQString(apeItem.toString()); } @@ -222,23 +202,23 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()["TPE2"]); if (!albumArtistFrame.isEmpty()) { - pTrackMetadata->setAlbumArtist(toQStringConcat(albumArtistFrame)); + pTrackMetadata->setAlbumArtist(toQStringFirst(albumArtistFrame)); } if (pTrackMetadata->getAlbum().isEmpty()) { const TagLib::ID3v2::FrameList originalAlbumFrame( tag.frameListMap()["TOAL"]); - pTrackMetadata->setAlbum(toQStringConcat(originalAlbumFrame)); + pTrackMetadata->setAlbum(toQStringFirst(originalAlbumFrame)); } const TagLib::ID3v2::FrameList composerFrame(tag.frameListMap()["TCOM"]); if (!composerFrame.isEmpty()) { - pTrackMetadata->setComposer(toQStringConcat(composerFrame)); + pTrackMetadata->setComposer(toQStringFirst(composerFrame)); } const TagLib::ID3v2::FrameList groupingFrame(tag.frameListMap()["TIT1"]); if (!groupingFrame.isEmpty()) { - pTrackMetadata->setGrouping(toQStringConcat(groupingFrame)); + pTrackMetadata->setGrouping(toQStringFirst(groupingFrame)); } // ID3v2.4.0: TDRC replaces TYER + TDAT @@ -313,7 +293,7 @@ void readXiphComment(TrackMetadata* pTrackMetadata, if (pTrackMetadata->getComment().isEmpty() && tag.fieldListMap().contains("COMMENT")) { pTrackMetadata->setComment( - toQStringConcat(tag.fieldListMap()["COMMENT"])); + toQStringFirst(tag.fieldListMap()["COMMENT"])); } // Some tags use "BPM" so check for that. @@ -357,29 +337,29 @@ void readXiphComment(TrackMetadata* pTrackMetadata, if (tag.fieldListMap().contains("ALBUMARTIST")) { pTrackMetadata->setAlbumArtist( - toQStringConcat(tag.fieldListMap()["ALBUMARTIST"])); + toQStringFirst(tag.fieldListMap()["ALBUMARTIST"])); } if (pTrackMetadata->getAlbumArtist().isEmpty() && tag.fieldListMap().contains("ALBUM_ARTIST")) { // try alternative field name pTrackMetadata->setAlbumArtist( - toQStringConcat(tag.fieldListMap()["ALBUM_ARTIST"])); + toQStringFirst(tag.fieldListMap()["ALBUM_ARTIST"])); } if (pTrackMetadata->getAlbumArtist().isEmpty() && tag.fieldListMap().contains("ALBUM ARTIST")) { // try alternative field name pTrackMetadata->setAlbumArtist( - toQStringConcat(tag.fieldListMap()["ALBUM ARTIST"])); + toQStringFirst(tag.fieldListMap()["ALBUM ARTIST"])); } if (tag.fieldListMap().contains("COMPOSER")) { pTrackMetadata->setComposer( - toQStringConcat(tag.fieldListMap()["COMPOSER"])); + toQStringFirst(tag.fieldListMap()["COMPOSER"])); } if (tag.fieldListMap().contains("GROUPING")) { pTrackMetadata->setGrouping( - toQStringConcat(tag.fieldListMap()["GROUPING"])); + toQStringFirst(tag.fieldListMap()["GROUPING"])); } if (tag.fieldListMap().contains("DATE")) { @@ -393,7 +373,7 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { tag.itemListMap().begin()); it != tag.itemListMap().end(); ++it) { qDebug() << "MP4" << toQString((*it).first) << "-" - << toQStringConcat((*it).second); + << toQStringFirst((*it).second); } } @@ -417,19 +397,19 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { // Get Album Artist if (tag.itemListMap().contains("aART")) { pTrackMetadata->setAlbumArtist( - toQStringConcat(tag.itemListMap()["aART"])); + toQStringFirst(tag.itemListMap()["aART"])); } // Get Composer if (tag.itemListMap().contains("\251wrt")) { pTrackMetadata->setComposer( - toQStringConcat(tag.itemListMap()["\251wrt"])); + toQStringFirst(tag.itemListMap()["\251wrt"])); } // Get Grouping if (tag.itemListMap().contains("\251grp")) { pTrackMetadata->setGrouping( - toQStringConcat(tag.itemListMap()["\251grp"])); + toQStringFirst(tag.itemListMap()["\251grp"])); } // Get date/year as string From ce7440feb0f9b30677adf54bac67a2dcb32be1c2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 19:42:07 +0100 Subject: [PATCH 094/481] MP3: Mute when the decoder has been restarted after seeking --- src/sources/audiosourcemp3.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index b43fb48b300..bdf13a5da2b 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -239,10 +239,16 @@ void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { mad_stream_buffer(&m_madStream, seekFrame.pInputData, m_fileSize - (seekFrame.pInputData - m_pFileData)); - // Calling mad_synth_mute() and mad_frame_mute() is not - // necessary, because we will prefetch (decode and skip) - // some frames before actually reading any audio samples - // from the stream. + // Muting is done here to eliminate potential pops/clicks + // from skipping Rob Leslie explains why here: + // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html + // + // TODO(XXX): Should not be necessary since both members + // m_madFrame and m_madSynth have been initialized just + // before. Might be removed after reliable unit tests show + // that those calls are really not needed anymore. + mad_synth_mute(&m_madSynth); + mad_frame_mute(&m_madFrame); } void AudioSourceMp3::close() { From 4e2ab29898120a8db20f88c256f42529bf5875a1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 19:50:10 +0100 Subject: [PATCH 095/481] MP3: Always decode the frame's header before the frame itself That's how it is done by GStreamer. All frames that contain ID3 instead of audio data are skipped. Tests still fail, but at least no audible glitches during listening tests. --- src/sources/audiosourcemp3.cpp | 180 ++++++++++++++++++++------------- src/sources/audiosourcemp3.h | 4 +- 2 files changed, 110 insertions(+), 74 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index bdf13a5da2b..31376c32ae7 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -8,9 +8,14 @@ namespace Mixxx { namespace { -// In the worst case up to 29 MP3 frames need to be prefetched: +// In the worst case up to 29 MP3 frames need to be prefetched +// for accurate seeking: // http://www.mars.org/mailman/public/mad-dev/2002-May/000634.html -const AudioSource::size_type kSeekFramePrefetchCount = 4; // required for synchronization +// +// TODO (XXX): Fix the implementation and determine the minimum +// required value for passing all unit tests. Currently even the +// theoretical maximum of 29 is not sufficient!? +const AudioSource::size_type kSeekFramePrefetchCount = 4; const AudioSource::sample_type kMadScale = AudioSource::kSampleValuePeak / AudioSource::sample_type(MAD_F_ONE); @@ -26,17 +31,6 @@ const AudioSource::size_type kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame const AudioSource::size_type kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; -bool mad_skip_id3_tag(mad_stream* pStream) { - long tagsize = id3_tag_query(pStream->this_frame, - pStream->bufend - pStream->this_frame); - if (0 < tagsize) { - mad_stream_skip(pStream, tagsize); - return true; - } else { - return false; - } -} - } // anonymous namespace AudioSourceMp3::AudioSourceMp3(QString fileName) : @@ -69,15 +63,34 @@ AudioSourcePointer AudioSourceMp3::create(QString fileName) { } void AudioSourceMp3::addSeekFrame( - mad_timer_t madDuration, - mad_units madUnits, + diff_type frameIndex, const unsigned char* pInputData) { SeekFrameType seekFrame; seekFrame.pInputData = pInputData; - seekFrame.frameIndex = mad_timer_count(madDuration, madUnits); + seekFrame.frameIndex = frameIndex; +// qDebug() << "seekFrameList[" << m_seekFrameList.size() << "] = " << seekFrame.frameIndex; m_seekFrameList.push_back(seekFrame); } +namespace { + int decodeFrameHeaderAndSkipId3Tags(mad_frame* pMadFrame, mad_stream* pMadStream) { + for (;;) { + const int result = mad_header_decode(&pMadFrame->header, pMadStream); + if ((-1 == result) && (MAD_ERROR_LOSTSYNC == pMadStream->error)) { + long tagsize = id3_tag_query(pMadStream->this_frame, + pMadStream->bufend - pMadStream->this_frame); + if (0 < tagsize) { + // Skip ID3 tag... + mad_stream_skip(pMadStream, tagsize); + // ...and continue with the next frame in the stream + continue; + } + } + return result; + } + } +} + Result AudioSourceMp3::open() { if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file:" << m_file.fileName(); @@ -99,40 +112,24 @@ Result AudioSourceMp3::open() { mad_units madUnits = MAD_UNITS_44100_HZ; // default value mad_timer_t madDuration = mad_timer_zero; - // Add first frame to list of frames - addSeekFrame(madDuration, madUnits, m_madStream.this_frame); - - DEBUG_ASSERT(quint64(m_madStream.bufend - m_madStream.this_frame) == m_fileSize); - while ((m_madStream.bufend - m_madStream.this_frame) > 0) { - mad_header madHeader; - mad_header_init(&madHeader); - - if (0 != mad_header_decode(&madHeader, &m_madStream)) { + diff_type frameIndex = 0; + do { + if (-1 == decodeFrameHeaderAndSkipId3Tags(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { - if (MAD_ERROR_LOSTSYNC == m_madStream.error) { - // Ignore LOSTSYNC due to ID3 tags - mad_skip_id3_tag(&m_madStream); - } else { - qDebug() << "Recoverable MP3 header error:" - << mad_stream_errorstr(&m_madStream); - } - mad_header_finish(&madHeader); + qDebug() << "Recoverable MP3 header decoding error:" + << mad_stream_errorstr(&m_madStream); continue; } else { - if (MAD_ERROR_BUFLEN == m_madStream.error) { - // EOF - break;// done - } else { - qWarning() << "Unrecoverable MP3 header error:" + if (MAD_ERROR_BUFLEN != m_madStream.error) { + qDebug() << "Unrecoverable MP3 header decoding error:" << mad_stream_errorstr(&m_madStream); - mad_header_finish(&madHeader); - return ERR; // abort } + break; } } // Grab data from madHeader - const size_type madChannelCount = MAD_NCHANNELS(&madHeader); + const size_type madChannelCount = MAD_NCHANNELS(&m_madFrame.header); if (kChannelCountDefault == getChannelCount()) { // initially set the number of channels setChannelCount(madChannelCount); @@ -142,9 +139,11 @@ Result AudioSourceMp3::open() { qWarning() << "Differing number of channels in some headers:" << m_file.fileName() << getChannelCount() << "<>" << madChannelCount; + mad_header_finish(&madHeader); + return ERR; // abort } } - const size_type madSampleRate = madHeader.samplerate; + const size_type madSampleRate = m_madFrame.header.samplerate; if (kFrameRateDefault == getFrameRate()) { // initially set the frame/sample rate switch (madSampleRate) { @@ -178,6 +177,7 @@ Result AudioSourceMp3::open() { default: qWarning() << "Invalid sample rate:" << m_file.fileName() << madSampleRate; + mad_header_finish(&madHeader); return ERR; // abort } setFrameRate(madSampleRate); @@ -187,36 +187,62 @@ Result AudioSourceMp3::open() { qWarning() << "Differing sample rate in some headers:" << m_file.fileName() << getFrameRate() << "<>" << madSampleRate; + mad_header_finish(&madHeader); return ERR; // abort } } - mad_timer_add(&madDuration, madHeader.duration); - sumBitrate += madHeader.bitrate; - mad_header_finish(&madHeader); + addSeekFrame(frameIndex, m_madStream.this_frame); + + mad_timer_add(&madDuration, m_madFrame.header.duration); + frameIndex = mad_timer_count(madDuration, madUnits); + + sumBitrate += m_madFrame.header.bitrate; - // Add next frame to list of frames - addSeekFrame(madDuration, madUnits, m_madStream.this_frame); + DEBUG_ASSERT(NULL != m_madStream.this_frame); + DEBUG_ASSERT(0 < (m_madStream.this_frame - m_pFileData)); + } while (quint64(m_madStream.this_frame - m_pFileData) < m_fileSize); + + if (MAD_ERROR_NONE != m_madStream.error) { + // Unreachable code for recoverable errors + DEBUG_ASSERT(!MAD_RECOVERABLE(m_madStream.error)); + if (MAD_ERROR_BUFLEN != m_madStream.error) { + qWarning() << "Unrecoverable MP3 header error:" + << mad_stream_errorstr(&m_madStream); + return ERR; // abort + } } - if (1 < m_seekFrameList.size()) { - setFrameCount(mad_timer_count(madDuration, madUnits)); - m_avgSeekFrameCount = getFrameCount() / (m_seekFrameList.size() - 1); - int avgBitrate = sumBitrate / (m_seekFrameList.size() - 1); - setBitrate(avgBitrate / 1000); - } else { + if (m_seekFrameList.empty()) { // This is not a working MP3 file. qWarning() << "SSMP3: This is not a working MP3 file:" << m_file.fileName(); return ERR; // abort } - restartDecoding(m_seekFrameList.front()); + // Initialize audio stream length + setFrameCount(frameIndex); + + // Calculate average values + m_avgSeekFrameCount = getFrameCount() / m_seekFrameList.size(); + int avgBitrate = sumBitrate / m_seekFrameList.size(); + setBitrate(avgBitrate / 1000); + + // Terminate m_seekFrameList + SeekFrameType terminalSeekFrame; + terminalSeekFrame.pInputData = 0; + terminalSeekFrame.frameIndex = frameIndex; + m_seekFrameList.push_back(terminalSeekFrame); + + if (!restartDecoding(m_seekFrameList.front())) { + qWarning() << "Failed to start decoding:" << m_file.fileName(); + return ERR; + } return OK; } -void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { +bool AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { // Reset the MAD decoder completely. Otherwise // audible artifacts and glitches occur when // seeking through the stream no matter how @@ -249,6 +275,9 @@ void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { // that those calls are really not needed anymore. mad_synth_mute(&m_madSynth); mad_frame_mute(&m_madFrame); + + // success + return true; } void AudioSourceMp3::close() { @@ -374,34 +403,41 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( // ...decode the next MP3 frame DEBUG_ASSERT(NULL != m_madStream.buffer); DEBUG_ASSERT(NULL != m_madStream.this_frame); - unsigned char const* madThisFrame = m_madStream.this_frame; - if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { - qDebug() << "MP3 decoding error:" - << mad_stream_errorstr(&m_madStream); + + // Always decode the frame's header before the frame itself + // to skip all non-audio frames with ID3 tag data. + if (-1 == decodeFrameHeaderAndSkipId3Tags(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { if (NULL != pSampleBuffer) { - qWarning() << "Recoverable MP3 decoding error:" + qDebug() << "Recoverable MP3 header decoding error:" << mad_stream_errorstr(&m_madStream); } - if (MAD_ERROR_LOSTSYNC == m_madStream.error) { - // Ignore LOSTSYNC due to ID3 tags - qDebug() << "Skipping ID3 tag data"; - mad_skip_id3_tag(&m_madStream); - } continue; } else { - DEBUG_ASSERT(MAD_ERROR_BUFLEN != m_madStream.error); - qWarning() << "Unrecoverable MP3 decoding error:" - << mad_stream_errorstr(&m_madStream); + if (MAD_ERROR_BUFLEN != m_madStream.error) { + qDebug() << "Unrecoverable MP3 header decoding error:" + << mad_stream_errorstr(&m_madStream); + } break; } - } else { - if (madThisFrame == m_madStream.this_frame) { - // still at the same frame after decoding although - // mad_frame_decode() returned 0 -> retry (why???) + } + + if (-1 == mad_frame_decode(&m_madFrame, &m_madStream)) { + if (MAD_RECOVERABLE(m_madStream.error)) { + if (NULL != pSampleBuffer) { + qWarning() << "Recoverable MP3 frame decoding error:" + << mad_stream_errorstr(&m_madStream); + } continue; + } else { + if (MAD_ERROR_BUFLEN != m_madStream.error) { + qWarning() << "Unrecoverable MP3 frame decoding error:" + << mad_stream_errorstr(&m_madStream); + } + break; } } + const mad_header* const pMadFrameHeader = &m_madFrame.header; DEBUG_ASSERT(getChannelCount() == MAD_NCHANNELS(pMadFrameHeader)); // Once decoded the frame is synthesized to PCM samples diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index aef3612d7ef..4d2888e996d 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -66,14 +66,14 @@ class AudioSourceMp3: public AudioSource { SeekFrameList m_seekFrameList; // ordered-by frameIndex size_type m_avgSeekFrameCount; // avg. sample frames per MP3 frame - void addSeekFrame(mad_timer_t madDuration, mad_units madUnits, const unsigned char* pInputData); + void addSeekFrame(diff_type frameIndex, const unsigned char* pInputData); /** Returns the position in m_seekFrameList of the requested frame index. */ SeekFrameList::size_type findSeekFrameIndex(diff_type frameIndex) const; diff_type m_curFrameIndex; - void restartDecoding(const SeekFrameType& seekFrame); + bool restartDecoding(const SeekFrameType& seekFrame); // current play position mad_frame m_madFrame; From e7e8c103107ac1e7de7e98be09d00f3b321451fb Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 15 Jan 2015 15:57:16 +0100 Subject: [PATCH 096/481] Fix the (infinite) test loop --- src/test/soundproxy_test.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 75b8b941853..f6ec4968d8b 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -70,10 +70,11 @@ TEST_F(SoundSourceProxyTest, seekForward) { QStringList extensions; extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; - for (unsigned int seekFrameIndex = 0; ; seekFrameIndex += kSeekFrameIndex) { - qDebug() << "seekFrameIndex =" << seekFrameIndex; - foreach (const QString& extension, extensions) { - QString filePath = kFilePath + extension; + foreach (const QString& extension, extensions) { + QString filePath = kFilePath + extension; + + for (unsigned int seekFrameIndex = 0; ; seekFrameIndex += kSeekFrameIndex) { + qDebug() << "seekFrameIndex =" << seekFrameIndex; Mixxx::AudioSourcePointer pAudioSource1( openAudioSource(filePath)); From a0c19930faa9ad0ab2b8410fd9c35178bb3594ff Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 14 Jan 2015 22:25:52 +0100 Subject: [PATCH 097/481] Fix MP3 decoding tests. Big kudos to Daniel Schuermann (daschuer)!! --- src/sources/audiosourcemp3.cpp | 330 ++++++++++++++++++--------------- src/sources/audiosourcemp3.h | 2 +- src/test/soundproxy_test.cpp | 16 +- 3 files changed, 192 insertions(+), 156 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 31376c32ae7..6023b5f5ab8 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -15,14 +15,15 @@ namespace { // TODO (XXX): Fix the implementation and determine the minimum // required value for passing all unit tests. Currently even the // theoretical maximum of 29 is not sufficient!? -const AudioSource::size_type kSeekFramePrefetchCount = 4; +const AudioSource::size_type kSeekFramePrefetchCount = 29; const AudioSource::sample_type kMadScale = AudioSource::kSampleValuePeak / AudioSource::sample_type(MAD_F_ONE); inline AudioSource::sample_type madScale(mad_fixed_t sample) { return sample * kMadScale; -} + +} // anonymous namespace // Optimization: Reserve initial capacity for seek frame list const AudioSource::size_type kMinutesPerFile = 10; // enough for the majority of files (tunable) @@ -31,6 +32,22 @@ const AudioSource::size_type kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame const AudioSource::size_type kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; +int decodeFrameHeader( + mad_header* pMadHeader, + mad_stream* pMadStream, + bool skipId3Tag) { + const int result = mad_header_decode(pMadHeader, pMadStream); + if ((0 != result) && (MAD_ERROR_LOSTSYNC == pMadStream->error) && skipId3Tag) { + long tagsize = id3_tag_query(pMadStream->this_frame, + pMadStream->bufend - pMadStream->this_frame); + if (0 < tagsize) { + // Skip ID3 tag data + mad_stream_skip(pMadStream, tagsize); + } + } + return result; +} + } // anonymous namespace AudioSourceMp3::AudioSourceMp3(QString fileName) : @@ -62,35 +79,6 @@ AudioSourcePointer AudioSourceMp3::create(QString fileName) { } } -void AudioSourceMp3::addSeekFrame( - diff_type frameIndex, - const unsigned char* pInputData) { - SeekFrameType seekFrame; - seekFrame.pInputData = pInputData; - seekFrame.frameIndex = frameIndex; -// qDebug() << "seekFrameList[" << m_seekFrameList.size() << "] = " << seekFrame.frameIndex; - m_seekFrameList.push_back(seekFrame); -} - -namespace { - int decodeFrameHeaderAndSkipId3Tags(mad_frame* pMadFrame, mad_stream* pMadStream) { - for (;;) { - const int result = mad_header_decode(&pMadFrame->header, pMadStream); - if ((-1 == result) && (MAD_ERROR_LOSTSYNC == pMadStream->error)) { - long tagsize = id3_tag_query(pMadStream->this_frame, - pMadStream->bufend - pMadStream->this_frame); - if (0 < tagsize) { - // Skip ID3 tag... - mad_stream_skip(pMadStream, tagsize); - // ...and continue with the next frame in the stream - continue; - } - } - return result; - } - } -} - Result AudioSourceMp3::open() { if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file:" << m_file.fileName(); @@ -105,23 +93,28 @@ Result AudioSourceMp3::open() { DEBUG_ASSERT(m_pFileData == m_madStream.this_frame); // Decode all the headers and calculate audio properties - unsigned long sumBitrate = 0; + setChannelCount(kChannelCountDefault); setFrameRate(kFrameRateDefault); mad_units madUnits = MAD_UNITS_44100_HZ; // default value mad_timer_t madDuration = mad_timer_zero; + unsigned long sumBitrate = 0; + + m_curFrameIndex = 0; + + mad_header madHeader; + mad_header_init(&madHeader); - diff_type frameIndex = 0; do { - if (-1 == decodeFrameHeaderAndSkipId3Tags(&m_madFrame, &m_madStream)) { + if (0 != decodeFrameHeader(&madHeader, &m_madStream, true)) { if (MAD_RECOVERABLE(m_madStream.error)) { - qDebug() << "Recoverable MP3 header decoding error:" + qWarning() << "Recoverable MP3 header decoding error:" << mad_stream_errorstr(&m_madStream); continue; } else { if (MAD_ERROR_BUFLEN != m_madStream.error) { - qDebug() << "Unrecoverable MP3 header decoding error:" + qWarning() << "Unrecoverable MP3 header decoding error:" << mad_stream_errorstr(&m_madStream); } break; @@ -129,13 +122,13 @@ Result AudioSourceMp3::open() { } // Grab data from madHeader - const size_type madChannelCount = MAD_NCHANNELS(&m_madFrame.header); + const size_type madChannelCount = MAD_NCHANNELS(&madHeader); if (kChannelCountDefault == getChannelCount()) { // initially set the number of channels setChannelCount(madChannelCount); } else { // check for consistent number of channels - if ((0 < madChannelCount) && getChannelCount() != madChannelCount) { + if ((0 < madChannelCount) && (getChannelCount() != madChannelCount)) { qWarning() << "Differing number of channels in some headers:" << m_file.fileName() << getChannelCount() << "<>" << madChannelCount; @@ -143,9 +136,10 @@ Result AudioSourceMp3::open() { return ERR; // abort } } - const size_type madSampleRate = m_madFrame.header.samplerate; + const size_type madSampleRate = madHeader.samplerate; if (kFrameRateDefault == getFrameRate()) { // initially set the frame/sample rate + setFrameRate(madSampleRate); switch (madSampleRate) { case 8000: madUnits = MAD_UNITS_8000_HZ; @@ -180,7 +174,6 @@ Result AudioSourceMp3::open() { mad_header_finish(&madHeader); return ERR; // abort } - setFrameRate(madSampleRate); } else { // check for consistent frame/sample rate if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { @@ -192,17 +185,21 @@ Result AudioSourceMp3::open() { } } - addSeekFrame(frameIndex, m_madStream.this_frame); + addSeekFrame(m_curFrameIndex, m_madStream.this_frame); - mad_timer_add(&madDuration, m_madFrame.header.duration); - frameIndex = mad_timer_count(madDuration, madUnits); - - sumBitrate += m_madFrame.header.bitrate; + // Accumulate data from the header + sumBitrate += madHeader.bitrate; + mad_timer_add(&madDuration, madHeader.duration); + + // Update current stream position + m_curFrameIndex = mad_timer_count(madDuration, madUnits); DEBUG_ASSERT(NULL != m_madStream.this_frame); DEBUG_ASSERT(0 < (m_madStream.this_frame - m_pFileData)); } while (quint64(m_madStream.this_frame - m_pFileData) < m_fileSize); + mad_header_finish(&madHeader); + if (MAD_ERROR_NONE != m_madStream.error) { // Unreachable code for recoverable errors DEBUG_ASSERT(!MAD_RECOVERABLE(m_madStream.error)); @@ -221,20 +218,19 @@ Result AudioSourceMp3::open() { } // Initialize audio stream length - setFrameCount(frameIndex); + setFrameCount(m_curFrameIndex); // Calculate average values m_avgSeekFrameCount = getFrameCount() / m_seekFrameList.size(); - int avgBitrate = sumBitrate / m_seekFrameList.size(); + const unsigned long avgBitrate = sumBitrate / m_seekFrameList.size(); setBitrate(avgBitrate / 1000); // Terminate m_seekFrameList - SeekFrameType terminalSeekFrame; - terminalSeekFrame.pInputData = 0; - terminalSeekFrame.frameIndex = frameIndex; - m_seekFrameList.push_back(terminalSeekFrame); + addSeekFrame(m_curFrameIndex, 0); - if (!restartDecoding(m_seekFrameList.front())) { + // Restart decoding at the beginning of the audio stream + m_curFrameIndex = restartDecoding(m_seekFrameList.front()); + if (m_curFrameIndex != m_seekFrameList.front().frameIndex) { qWarning() << "Failed to start decoding:" << m_file.fileName(); return ERR; } @@ -242,42 +238,43 @@ Result AudioSourceMp3::open() { return OK; } -bool AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { - // Reset the MAD decoder completely. Otherwise - // audible artifacts and glitches occur when - // seeking through the stream no matter how - // many MP3 frames are prefetched! - mad_synth_finish(&m_madSynth); - mad_frame_finish(&m_madFrame); +AudioSource::diff_type AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { + qDebug() << "restartDecoding @" << seekFrame.frameIndex; + + if (0 == seekFrame.frameIndex) { + mad_frame_finish(&m_madFrame); + mad_synth_finish(&m_madSynth); + } mad_stream_finish(&m_madStream); + mad_stream_init(&m_madStream); mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - mad_frame_init(&m_madFrame); - mad_synth_init(&m_madSynth); - - // Reset stream position - m_curFrameIndex = seekFrame.frameIndex; - - // Discard decoded output - m_madSynthCount = 0; + if (0 == seekFrame.frameIndex) { + mad_synth_init(&m_madSynth); + mad_frame_init(&m_madFrame); + } // Fill input buffer mad_stream_buffer(&m_madStream, seekFrame.pInputData, m_fileSize - (seekFrame.pInputData - m_pFileData)); - // Muting is done here to eliminate potential pops/clicks - // from skipping Rob Leslie explains why here: - // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html - // - // TODO(XXX): Should not be necessary since both members - // m_madFrame and m_madSynth have been initialized just - // before. Might be removed after reliable unit tests show - // that those calls are really not needed anymore. - mad_synth_mute(&m_madSynth); - mad_frame_mute(&m_madFrame); - - // success - return true; + if (0 < seekFrame.frameIndex) { + // Muting is done here to eliminate potential pops/clicks + // from skipping Rob Leslie explains why here: + // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html + mad_frame_mute(&m_madFrame); + mad_synth_mute(&m_madSynth); + } + + if ((0 != decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) && + !MAD_RECOVERABLE(m_madStream.error)) { + qWarning() << "Unrecoverable MP3 frame header decoding error:" + << mad_stream_errorstr(&m_madStream); + // failure + return getFrameCount(); + } + + return seekFrame.frameIndex; } void AudioSourceMp3::close() { @@ -298,6 +295,20 @@ void AudioSourceMp3::close() { reset(); } +void AudioSourceMp3::addSeekFrame( + diff_type frameIndex, + const unsigned char* pInputData) { + DEBUG_ASSERT(m_seekFrameList.empty() || + (m_seekFrameList.back().frameIndex < frameIndex)); + DEBUG_ASSERT(m_seekFrameList.empty() || + (NULL == pInputData) || + (0 < (pInputData - m_seekFrameList.back().pInputData))); + SeekFrameType seekFrame; + seekFrame.pInputData = pInputData; + seekFrame.frameIndex = frameIndex; + m_seekFrameList.push_back(seekFrame); +} + AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( diff_type frameIndex) const { // Check preconditions @@ -306,12 +317,12 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( DEBUG_ASSERT(0 == m_seekFrameList.front().frameIndex); DEBUG_ASSERT(diff_type(getFrameCount()) == m_seekFrameList.back().frameIndex); - AudioSourceMp3::SeekFrameList::size_type lowerBound = + SeekFrameList::size_type lowerBound = 0; - AudioSourceMp3::SeekFrameList::size_type upperBound = + SeekFrameList::size_type upperBound = m_seekFrameList.size(); // Initial guess based on average frame size - AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = + SeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; if (seekFrameIndex >= m_seekFrameList.size()) { seekFrameIndex = m_seekFrameList.size() - 1; @@ -319,6 +330,7 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( while ((upperBound - lowerBound) > 1) { DEBUG_ASSERT(seekFrameIndex >= lowerBound); DEBUG_ASSERT(seekFrameIndex < upperBound); + DEBUG_ASSERT(m_seekFrameList[lowerBound].frameIndex <= frameIndex); if (m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex) { lowerBound = seekFrameIndex; } else { @@ -340,36 +352,51 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - if (!m_seekFrameList.empty()) { - SeekFrameList::size_type seekFrameIndex = findSeekFrameIndex( - frameIndex); - DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); - const SeekFrameList::size_type curSeekFrameIndex = findSeekFrameIndex( - m_curFrameIndex); - DEBUG_ASSERT(m_seekFrameList.size() > curSeekFrameIndex); - // some consistency checks - DEBUG_ASSERT((curSeekFrameIndex >= seekFrameIndex) || (m_curFrameIndex < frameIndex)); - DEBUG_ASSERT((curSeekFrameIndex <= seekFrameIndex) || (m_curFrameIndex > frameIndex)); - if ((frameIndex < m_curFrameIndex) || // seeking backwards? - (seekFrameIndex - > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jumping forward? - // Implementation note: The type size_type is unsigned so - // need to be careful when subtracting! - if (kSeekFramePrefetchCount < seekFrameIndex) { - seekFrameIndex -= kSeekFramePrefetchCount; - } else { - seekFrameIndex = 0; - } - restartDecoding(m_seekFrameList[seekFrameIndex]); - DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == seekFrameIndex); + + SeekFrameList::size_type seekFrameIndex = findSeekFrameIndex( + frameIndex); + DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); + const SeekFrameList::size_type curSeekFrameIndex = findSeekFrameIndex( + m_curFrameIndex); + DEBUG_ASSERT(m_seekFrameList.size() > curSeekFrameIndex); + // some consistency checks + DEBUG_ASSERT((curSeekFrameIndex >= seekFrameIndex) || (m_curFrameIndex < frameIndex)); + DEBUG_ASSERT((curSeekFrameIndex <= seekFrameIndex) || (m_curFrameIndex > frameIndex)); + if ((diff_type(getFrameCount()) <= m_curFrameIndex) || // out of range + (frameIndex < m_curFrameIndex) || // seek backward + (seekFrameIndex > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jump forward + + // Discard decoded output + m_madSynthCount = 0; + + // Adjust the seek frame index for prefetching + // Implementation note: The type size_type is unsigned so + // need to be careful when subtracting! + if (kSeekFramePrefetchCount < seekFrameIndex) { + // Restart decoding kSeekFramePrefetchCount seek frames + // before the expected sync position + seekFrameIndex -= kSeekFramePrefetchCount; + } else { + // Restart decoding at the beginnig of the audio stream + seekFrameIndex = 0; + } + + m_curFrameIndex = restartDecoding(m_seekFrameList[seekFrameIndex]); + if (m_curFrameIndex >= diff_type(getFrameCount())) { + // out of range -> abort + return m_curFrameIndex; } - // decoding starts before the actual target position - DEBUG_ASSERT(m_curFrameIndex <= frameIndex); - // decode and discard prefetch data - const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; - skipFrameSamples(prefetchFrameCount); - DEBUG_ASSERT(m_curFrameIndex == frameIndex); + DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == seekFrameIndex); } + + // Decoding starts before the actual target position + DEBUG_ASSERT(m_curFrameIndex <= frameIndex); + + // Skip (= decode and discard) prefetch data + const size_type skipFrameCount = frameIndex - m_curFrameIndex; + skipFrameSamples(skipFrameCount); + DEBUG_ASSERT(m_curFrameIndex == frameIndex); + return m_curFrameIndex; } @@ -404,81 +431,88 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( DEBUG_ASSERT(NULL != m_madStream.buffer); DEBUG_ASSERT(NULL != m_madStream.this_frame); - // Always decode the frame's header before the frame itself - // to skip all non-audio frames with ID3 tag data. - if (-1 == decodeFrameHeaderAndSkipId3Tags(&m_madFrame, &m_madStream)) { + // WARNING: Correctly evaluating and handling the result + // of mad_frame_decode() has proven to be extremely tricky. + // Don't change anything at the following lines of code + // unless you know what you are doing!!! + unsigned char const* pMadThisFrame = m_madStream.this_frame; + if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { - if (NULL != pSampleBuffer) { - qDebug() << "Recoverable MP3 header decoding error:" - << mad_stream_errorstr(&m_madStream); + if ((pMadThisFrame != m_madStream.this_frame) && + (NULL != pSampleBuffer)) { + qWarning() << "Recoverable MP3 frame decoding error:" + << mad_stream_errorstr(&m_madStream); } - continue; - } else { - if (MAD_ERROR_BUFLEN != m_madStream.error) { - qDebug() << "Unrecoverable MP3 header decoding error:" - << mad_stream_errorstr(&m_madStream); - } - break; - } - } - - if (-1 == mad_frame_decode(&m_madFrame, &m_madStream)) { - if (MAD_RECOVERABLE(m_madStream.error)) { - if (NULL != pSampleBuffer) { - qWarning() << "Recoverable MP3 frame decoding error:" - << mad_stream_errorstr(&m_madStream); - } - continue; + // Acknowledge error... + m_madStream.error = MAD_ERROR_NONE; + // ...and continue } else { if (MAD_ERROR_BUFLEN != m_madStream.error) { qWarning() << "Unrecoverable MP3 frame decoding error:" << mad_stream_errorstr(&m_madStream); } + // Abort break; } } + if (pMadThisFrame == m_madStream.this_frame) { + // Only seems to occur at the beginning of a stream!? + DEBUG_ASSERT(0 == m_curFrameIndex); + qDebug() << "Retry decoding MP3 frame @" << m_curFrameIndex; + // Retry + continue; + } + + DEBUG_ASSERT(getChannelCount() == MAD_NCHANNELS(&m_madFrame.header)); - const mad_header* const pMadFrameHeader = &m_madFrame.header; - DEBUG_ASSERT(getChannelCount() == MAD_NCHANNELS(pMadFrameHeader)); // Once decoded the frame is synthesized to PCM samples mad_synth_frame(&m_madSynth, &m_madFrame); DEBUG_ASSERT(getFrameRate() == m_madSynth.pcm.samplerate); m_madSynthCount = m_madSynth.pcm.length; + DEBUG_ASSERT(0 < m_madSynthCount); } - const size_type framesRead = math_min( + + const size_type synthReadCount = math_min( m_madSynthCount, numberOfFramesRemaining); if (NULL != pSampleBuffer) { + DEBUG_ASSERT(m_madSynthCount <= m_madSynth.pcm.length); const size_type madSynthOffset = m_madSynth.pcm.length - m_madSynthCount; + DEBUG_ASSERT(madSynthOffset < m_madSynth.pcm.length); if (isChannelCountMono()) { - for (size_type i = 0; i < framesRead; ++i) { + for (size_type i = 0; i < synthReadCount; ++i) { const sample_type sampleValue = madScale( m_madSynth.pcm.samples[0][madSynthOffset + i]); - *(pSampleBuffer++) = sampleValue; + *pSampleBuffer = sampleValue; + ++pSampleBuffer; if (readStereoSamples) { - *(pSampleBuffer++) = sampleValue; + *pSampleBuffer = sampleValue; + ++pSampleBuffer; } } } else if (isChannelCountStereo() || readStereoSamples) { - for (size_type i = 0; i < framesRead; ++i) { - *(pSampleBuffer++) = madScale( + for (size_type i = 0; i < synthReadCount; ++i) { + *pSampleBuffer = madScale( m_madSynth.pcm.samples[0][madSynthOffset + i]); - *(pSampleBuffer++) = madScale( + ++pSampleBuffer; + *pSampleBuffer = madScale( m_madSynth.pcm.samples[1][madSynthOffset + i]); + ++pSampleBuffer; } } else { - for (size_type i = 0; i < framesRead; ++i) { + for (size_type i = 0; i < synthReadCount; ++i) { for (size_type j = 0; j < getChannelCount(); ++j) { - *(pSampleBuffer++) = madScale( + *pSampleBuffer = madScale( m_madSynth.pcm.samples[j][madSynthOffset + i]); + ++pSampleBuffer; } } } } // consume decoded output data - m_madSynthCount -= framesRead; - m_curFrameIndex += framesRead; - numberOfFramesRemaining -= framesRead; + numberOfFramesRemaining -= synthReadCount; + m_madSynthCount -= synthReadCount; + m_curFrameIndex += synthReadCount; } DEBUG_ASSERT(numberOfFrames >= numberOfFramesRemaining); return numberOfFrames - numberOfFramesRemaining; diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index 4d2888e996d..f8590982d22 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -73,7 +73,7 @@ class AudioSourceMp3: public AudioSource { diff_type m_curFrameIndex; - bool restartDecoding(const SeekFrameType& seekFrame); + diff_type restartDecoding(const SeekFrameType& seekFrame); // current play position mad_frame m_madFrame; diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index f6ec4968d8b..426dbcf8749 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -91,7 +91,8 @@ TEST_F(SoundSourceProxyTest, seekForward) { frameIndex1 += readCount1; } EXPECT_EQ(seekFrameIndex, frameIndex1); - unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); + const unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); + EXPECT_EQ(kTestFrameCount, readCount1); Mixxx::AudioSourcePointer pAudioSource2( openAudioSource(filePath)); @@ -101,17 +102,18 @@ TEST_F(SoundSourceProxyTest, seekForward) { } const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); CSAMPLE *pData2 = new CSAMPLE[sampleCount2]; - pAudioSource2->seekSampleFrame(seekFrameIndex); - unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); + unsigned int frameIndex2 = pAudioSource2->seekSampleFrame(seekFrameIndex); + EXPECT_EQ(seekFrameIndex, frameIndex2); + const unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); EXPECT_EQ(kTestFrameCount, readCount2); - EXPECT_EQ(readCount1, readCount2); - for (unsigned int i = 0; i < readCount1; i++) { + for (unsigned int i = 0; i < kTestFrameCount; i++) { if (pData1[i] != pData2[i]) { - qDebug() << filePath << "Test Sample" << i; + qDebug() << filePath; + qDebug() << "seekFrameIndex =" << seekFrameIndex; + qDebug() << "readFrameIndex =" << (seekFrameIndex + i); } EXPECT_EQ(pData1[i], pData2[i]); - //qDebug() << pData1[i]; } delete[] pData1; From 75ac568c3fa429577b1976a2cc7b27321f0e970d Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Thu, 15 Jan 2015 22:06:25 +0100 Subject: [PATCH 098/481] renaming as AutoPan --- build/depends.py | 2 +- .../native/{paneffect.cpp => autopaneffect.cpp} | 14 +++++++------- .../native/{paneffect.h => autopaneffect.h} | 14 +++++++------- src/effects/native/nativebackend.cpp | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) rename src/effects/native/{paneffect.cpp => autopaneffect.cpp} (94%) rename src/effects/native/{paneffect.h => autopaneffect.h} (89%) diff --git a/build/depends.py b/build/depends.py index 97658e12559..1f0d5267aa9 100644 --- a/build/depends.py +++ b/build/depends.py @@ -562,7 +562,7 @@ def sources(self, build): "effects/native/moogladder4filtereffect.cpp", "effects/native/reverbeffect.cpp", "effects/native/echoeffect.cpp", - "effects/native/paneffect.cpp", + "effects/native/autopaneffect.cpp", "effects/native/reverb/Reverb.cc", "engine/effects/engineeffectsmanager.cpp", diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/autopaneffect.cpp similarity index 94% rename from src/effects/native/paneffect.cpp rename to src/effects/native/autopaneffect.cpp index d29788634da..a45facf61f5 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -1,20 +1,20 @@ #include "util/math.h" #include -#include "effects/native/paneffect.h" +#include "effects/native/autopaneffect.h" #include "sampleutil.h" // static -QString PanEffect::getId() { +QString AutoPanEffect::getId() { return "org.mixxx.effects.pan"; } // static -EffectManifest PanEffect::getManifest() { +EffectManifest AutoPanEffect::getManifest() { EffectManifest manifest; manifest.setId(getId()); - manifest.setName(QObject::tr("Pan")); + manifest.setName(QObject::tr("AutoPan")); manifest.setAuthor("The Mixxx Team"); manifest.setVersion("1.0"); manifest.setDescription(QObject::tr( @@ -80,7 +80,7 @@ EffectManifest PanEffect::getManifest() { return manifest; } -PanEffect::PanEffect(EngineEffect* pEffect, const EffectManifest& manifest) +AutoPanEffect::AutoPanEffect(EngineEffect* pEffect, const EffectManifest& manifest) : m_pDepthParameter(pEffect->getParameterById("depth")), m_pStrengthParameter(pEffect->getParameterById("strength")), @@ -90,10 +90,10 @@ PanEffect::PanEffect(EngineEffect* pEffect, const EffectManifest& manifest) Q_UNUSED(manifest); } -PanEffect::~PanEffect() { +AutoPanEffect::~AutoPanEffect() { } -void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, +void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupState, const CSAMPLE* pInput, CSAMPLE* pOutput, const unsigned int numSamples, const unsigned int sampleRate, diff --git a/src/effects/native/paneffect.h b/src/effects/native/autopaneffect.h similarity index 89% rename from src/effects/native/paneffect.h rename to src/effects/native/autopaneffect.h index c41fa334ed4..f63b527ffae 100644 --- a/src/effects/native/paneffect.h +++ b/src/effects/native/autopaneffect.h @@ -1,5 +1,5 @@ -#ifndef PANEFFECT_H -#define PANEFFECT_H +#ifndef AUTOPANEFFECT_H +#define AUTOPANEFFECT_H #include @@ -70,10 +70,10 @@ struct PanGroupState { }; -class PanEffect : public GroupEffectProcessor { +class AutoPanEffect : public GroupEffectProcessor { public: - PanEffect(EngineEffect* pEffect, const EffectManifest& manifest); - virtual ~PanEffect(); + AutoPanEffect(EngineEffect* pEffect, const EffectManifest& manifest); + virtual ~AutoPanEffect(); static QString getId(); static EffectManifest getManifest(); @@ -98,7 +98,7 @@ class PanEffect : public GroupEffectProcessor { EngineEffectParameter* m_pPeriodParameter; EngineEffectParameter* m_pRampingParameter; - DISALLOW_COPY_AND_ASSIGN(PanEffect); + DISALLOW_COPY_AND_ASSIGN(AutoPanEffect); }; -#endif /* PANEFFECT_H */ +#endif /* AUTOPANEFFECT_H */ diff --git a/src/effects/native/nativebackend.cpp b/src/effects/native/nativebackend.cpp index 7f437614f93..0994d6015f2 100644 --- a/src/effects/native/nativebackend.cpp +++ b/src/effects/native/nativebackend.cpp @@ -13,7 +13,7 @@ #include "effects/native/reverbeffect.h" #endif #include "effects/native/echoeffect.h" -#include "effects/native/paneffect.h" +#include "effects/native/autopaneffect.h" NativeBackend::NativeBackend(QObject* pParent) : EffectsBackend(pParent, tr("Native")) { @@ -31,7 +31,7 @@ NativeBackend::NativeBackend(QObject* pParent) // Fancy effects registerEffect(); registerEffect(); - registerEffect(); + registerEffect(); #ifndef __MACAPPSTORE__ registerEffect(); #endif From 33b2d2b88fd374e086ce7d886408ac91d3c5232c Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Thu, 15 Jan 2015 22:21:08 +0100 Subject: [PATCH 099/481] available as chain --- src/effects/effectsmanager.cpp | 7 +++++++ src/effects/native/autopaneffect.cpp | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/effects/effectsmanager.cpp b/src/effects/effectsmanager.cpp index 53a8be75715..5a6c12ca41b 100644 --- a/src/effects/effectsmanager.cpp +++ b/src/effects/effectsmanager.cpp @@ -251,6 +251,13 @@ void EffectsManager::setupDefaults() { pChain->addEffect(pEffect); m_pEffectChainManager->addEffectChain(pChain); + pChain = EffectChainPointer(new EffectChain( + this, "org.mixxx.effectchain.autopan")); + pChain->setName(tr("AutoPan")); + pEffect = instantiateEffect("org.mixxx.effects.autopan"); + pChain->addEffect(pEffect); + m_pEffectChainManager->addEffectChain(pChain); + // These controls are used inside EQ Effects m_pLoEqFreq = new ControlPotmeter(ConfigKey("[Mixer Profile]", "LoEQFrequency"), 0., 22040); m_pHiEqFreq = new ControlPotmeter(ConfigKey("[Mixer Profile]", "HiEQFrequency"), 0., 22040); diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index a45facf61f5..1b7e052aa0f 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -7,7 +7,7 @@ // static QString AutoPanEffect::getId() { - return "org.mixxx.effects.pan"; + return "org.mixxx.effects.autopan"; } // static From 231f2a54c0ce7ff5723af85d5c3134ec111a0b66 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Thu, 15 Jan 2015 23:57:00 +0100 Subject: [PATCH 100/481] period as multiple of beat duration --- src/effects/native/autopaneffect.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index 1b7e052aa0f..00412450b26 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -109,7 +109,15 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat return; } - CSAMPLE period = roundf(m_pPeriodParameter->value()) * (float)numSamples / 2.0f; + CSAMPLE period = roundf(m_pPeriodParameter->value()); + if (groupFeatures.has_beat_length) { + // 1/8, 1/4, 1/2, 1, 2, 4, 8, 16, 32, 64 + double beats = pow(2, floor(period * 9 / 500) - 3); + period = groupFeatures.beat_length * beats; + } else { + period *= (float)numSamples / 2.0f; + } + CSAMPLE stepFrac = m_pStrengthParameter->value(); CSAMPLE depth = m_pDepthParameter->value(); float rampingTreshold = m_pRampingParameter->value(); @@ -139,6 +147,7 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat gs.frac.setRamping(rampingTreshold); gs.frac.ramped = false; // just for debug + for (unsigned int i = 0; i + 1 < numSamples; i += 2) { CSAMPLE periodFraction = CSAMPLE(gs.time) / period; @@ -176,7 +185,9 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat qDebug() - << "| ramped :" << gs.frac.ramped + // << "| ramped :" << gs.frac.ramped + // << "| beat_length :" << groupFeatures.beat_length + // << "| period :" << period << "| frac :" << gs.frac << "| time :" << gs.time << "| rampingTreshold :" << rampingTreshold From 55ce022b2c5f5b0fcb41170c17e59201710ef9d6 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 16 Jan 2015 11:09:20 +0100 Subject: [PATCH 101/481] Logical separation of absolute and relative values --- plugins/soundsourcem4a/audiosourcem4a.cpp | 25 ++++---- src/sources/audiosource.h | 23 +++++-- src/sources/audiosourceflac.cpp | 1 + src/sources/audiosourcemp3.cpp | 75 ++++++++++++----------- src/sources/audiosourceoggvorbis.cpp | 1 + src/sources/audiosourceopus.cpp | 4 +- src/sources/audiosourcesndfile.cpp | 5 +- 7 files changed, 77 insertions(+), 57 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 638423b5c48..4b5980203f6 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -26,7 +26,7 @@ namespace { const AudioSource::size_type kFramesPerSampleBlock = 1024; // MP4SampleId is 1-based -const MP4SampleId kMinSampleBlockId = 1; +const MP4SampleId kSampleBlockIdMin = 1; // Decoding will be restarted one or more blocks of samples // before the actual position after seeking randomly in the @@ -82,7 +82,7 @@ AudioSourceM4A::AudioSourceM4A() : m_inputBufferOffset(0), m_inputBufferLength(0), m_hDecoder(NULL), - m_curFrameIndex(0) { + m_curFrameIndex(kFrameIndexMin) { } AudioSourceM4A::~AudioSourceM4A() { @@ -184,7 +184,7 @@ Result AudioSourceM4A::open(QString fileName) { m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); m_curSampleBlockId = 0; - m_curFrameIndex = 0; + m_curFrameIndex = kFrameIndexMin; return OK; } @@ -204,20 +204,20 @@ void AudioSourceM4A::close() { m_inputBuffer.clear(); m_inputBufferOffset = 0; m_inputBufferLength = 0; - m_curFrameIndex = 0; + m_curFrameIndex = kFrameIndexMin; reset(); } bool AudioSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { - return (sampleBlockId >= kMinSampleBlockId) + return (sampleBlockId >= kSampleBlockIdMin) && (sampleBlockId <= m_maxSampleBlockId); } void AudioSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { NeAACDecPostSeekReset(m_hDecoder, sampleBlockId); m_curSampleBlockId = sampleBlockId; - m_curFrameIndex = (m_curSampleBlockId - kMinSampleBlockId) - * kFramesPerSampleBlock; + m_curFrameIndex = kFrameIndexMin + + (m_curSampleBlockId - kSampleBlockIdMin) * kFramesPerSampleBlock; // discard input buffer m_inputBufferOffset = 0; m_inputBufferLength = 0; @@ -225,8 +225,9 @@ void AudioSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + if (m_curFrameIndex != frameIndex) { - MP4SampleId sampleBlockId = kMinSampleBlockId + MP4SampleId sampleBlockId = kSampleBlockIdMin + (frameIndex / kFramesPerSampleBlock); DEBUG_ASSERT(isValidSampleBlockId(sampleBlockId)); if ((frameIndex < m_curFrameIndex) || // seeking backwards? @@ -237,11 +238,11 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { // from the calculated starting block to avoid audible glitches. // Implementation note: The type MP4SampleId is unsigned so we // need to be careful when subtracting! - if ((kMinSampleBlockId + kNumberOfPrefetchSampleBlocks) + if ((kSampleBlockIdMin + kNumberOfPrefetchSampleBlocks) < sampleBlockId) { sampleBlockId -= kNumberOfPrefetchSampleBlocks; } else { - sampleBlockId = kMinSampleBlockId; + sampleBlockId = kSampleBlockIdMin; } restartDecoding(sampleBlockId); DEBUG_ASSERT(m_curSampleBlockId == sampleBlockId); @@ -266,7 +267,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( sample_type* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = - math_min(numberOfFrames, getFrameCount() - m_curFrameIndex); + math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); while (0 < numberOfFramesRemaining) { DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); if (m_inputBufferOffset >= m_inputBufferLength) { @@ -283,7 +284,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( qWarning() << "Failed to read MP4 input data for sample block" << m_curSampleBlockId << "(" << "min =" - << kMinSampleBlockId << "," << "max =" + << kSampleBlockIdMin << "," << "max =" << m_maxSampleBlockId << ")"; break; // abort } diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 1ac5c57dc0c..e4afb95ea64 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -43,6 +43,9 @@ class AudioSource { static const size_type kFrameCountZero = 0; static const size_type kFrameCountDefault = kFrameCountZero; + // 0-based indexing of sample frames + static const diff_type kFrameIndexMin = 0; + static const sample_type kSampleValueZero; static const sample_type kSampleValuePeak; @@ -128,12 +131,22 @@ class AudioSource { return sampleCount / getChannelCount(); } - // The (sample) frame index only valid in the range - // [0, getFrameCount()]. The value frameIndex = getFrameCount() - // points behind of the audio data stream, but is a valid - // parameter for seeking. + // Index of the first sample frame. + diff_type getFrameIndexMin() const { + return kFrameIndexMin; + } + + // Index of the sample frame following the last + // sample frame. + diff_type getFrameIndexMax() const { + return kFrameIndexMin + getFrameCount(); + } + + // The sample frame index is valid in the range + // [getFrameIndexMin(), getFrameIndexMax()]. inline bool isValidFrameIndex(diff_type frameIndex) const { - return (0 <= frameIndex) && (getFrameCount() >= size_type(frameIndex)); + return (getFrameIndexMin() <= frameIndex) && + (getFrameIndexMax() >= frameIndex); } // Adjusts the current frame seek index: diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 6e6aa10cbac..34fe626d53f 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -125,6 +125,7 @@ void AudioSourceFLAC::close() { Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame( diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + // clear decode buffer before seeking m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 6023b5f5ab8..80c8cd70c8d 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -37,7 +37,8 @@ int decodeFrameHeader( mad_stream* pMadStream, bool skipId3Tag) { const int result = mad_header_decode(pMadHeader, pMadStream); - if ((0 != result) && (MAD_ERROR_LOSTSYNC == pMadStream->error) && skipId3Tag) { + if ((0 != result) && (MAD_ERROR_LOSTSYNC == pMadStream->error) + && skipId3Tag) { long tagsize = id3_tag_query(pMadStream->this_frame, pMadStream->bufend - pMadStream->this_frame); if (0 < tagsize) { @@ -50,13 +51,14 @@ int decodeFrameHeader( } // anonymous namespace -AudioSourceMp3::AudioSourceMp3(QString fileName) : - m_file(fileName), - m_fileSize(0), - m_pFileData(NULL), - m_avgSeekFrameCount(0), - m_curFrameIndex(0), - m_madSynthCount(0) { +AudioSourceMp3::AudioSourceMp3(QString fileName) + : + m_file(fileName), + m_fileSize(0), + m_pFileData(NULL), + m_avgSeekFrameCount(0), + m_curFrameIndex(kFrameIndexMin), + m_madSynthCount(0) { mad_stream_init(&m_madStream); mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); mad_frame_init(&m_madFrame); @@ -101,8 +103,8 @@ Result AudioSourceMp3::open() { mad_timer_t madDuration = mad_timer_zero; unsigned long sumBitrate = 0; - m_curFrameIndex = 0; - + m_curFrameIndex = kFrameIndexMin; + mad_header madHeader; mad_header_init(&madHeader); @@ -128,7 +130,8 @@ Result AudioSourceMp3::open() { setChannelCount(madChannelCount); } else { // check for consistent number of channels - if ((0 < madChannelCount) && (getChannelCount() != madChannelCount)) { + if ((0 < madChannelCount) + && (getChannelCount() != madChannelCount)) { qWarning() << "Differing number of channels in some headers:" << m_file.fileName() << getChannelCount() << "<>" << madChannelCount; @@ -190,9 +193,10 @@ Result AudioSourceMp3::open() { // Accumulate data from the header sumBitrate += madHeader.bitrate; mad_timer_add(&madDuration, madHeader.duration); - + // Update current stream position - m_curFrameIndex = mad_timer_count(madDuration, madUnits); + m_curFrameIndex = kFrameIndexMin + + mad_timer_count(madDuration, madUnits); DEBUG_ASSERT(NULL != m_madStream.this_frame); DEBUG_ASSERT(0 < (m_madStream.this_frame - m_pFileData)); @@ -238,7 +242,8 @@ Result AudioSourceMp3::open() { return OK; } -AudioSource::diff_type AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { +AudioSource::diff_type AudioSourceMp3::restartDecoding( + const SeekFrameType& seekFrame) { qDebug() << "restartDecoding @" << seekFrame.frameIndex; if (0 == seekFrame.frameIndex) { @@ -267,7 +272,7 @@ AudioSource::diff_type AudioSourceMp3::restartDecoding(const SeekFrameType& seek } if ((0 != decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) && - !MAD_RECOVERABLE(m_madStream.error)) { + !MAD_RECOVERABLE(m_madStream.error)) { qWarning() << "Unrecoverable MP3 frame header decoding error:" << mad_stream_errorstr(&m_madStream); // failure @@ -285,7 +290,7 @@ void AudioSourceMp3::close() { m_seekFrameList.clear(); m_avgSeekFrameCount = 0; - m_curFrameIndex = 0; + m_curFrameIndex = kFrameIndexMin; m_file.unmap(m_pFileData); m_fileSize = 0; @@ -314,8 +319,8 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( // Check preconditions DEBUG_ASSERT(0 < m_avgSeekFrameCount); DEBUG_ASSERT(!m_seekFrameList.empty()); - DEBUG_ASSERT(0 == m_seekFrameList.front().frameIndex); - DEBUG_ASSERT(diff_type(getFrameCount()) == m_seekFrameList.back().frameIndex); + DEBUG_ASSERT(kFrameIndexMin == m_seekFrameList.front().frameIndex); + DEBUG_ASSERT(diff_type(kFrameIndexMin + getFrameCount()) == m_seekFrameList.back().frameIndex); SeekFrameList::size_type lowerBound = 0; @@ -362,13 +367,13 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { // some consistency checks DEBUG_ASSERT((curSeekFrameIndex >= seekFrameIndex) || (m_curFrameIndex < frameIndex)); DEBUG_ASSERT((curSeekFrameIndex <= seekFrameIndex) || (m_curFrameIndex > frameIndex)); - if ((diff_type(getFrameCount()) <= m_curFrameIndex) || // out of range - (frameIndex < m_curFrameIndex) || // seek backward - (seekFrameIndex > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jump forward - + if ((getFrameIndexMax() <= m_curFrameIndex) || // out of range + (frameIndex < m_curFrameIndex) || // seek backward + (seekFrameIndex > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jump forward + // Discard decoded output m_madSynthCount = 0; - + // Adjust the seek frame index for prefetching // Implementation note: The type size_type is unsigned so // need to be careful when subtracting! @@ -380,15 +385,15 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { // Restart decoding at the beginnig of the audio stream seekFrameIndex = 0; } - + m_curFrameIndex = restartDecoding(m_seekFrameList[seekFrameIndex]); - if (m_curFrameIndex >= diff_type(getFrameCount())) { + if (getFrameIndexMax() <= m_curFrameIndex) { // out of range -> abort return m_curFrameIndex; } DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == seekFrameIndex); } - + // Decoding starts before the actual target position DEBUG_ASSERT(m_curFrameIndex <= frameIndex); @@ -396,7 +401,7 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { const size_type skipFrameCount = frameIndex - m_curFrameIndex; skipFrameSamples(skipFrameCount); DEBUG_ASSERT(m_curFrameIndex == frameIndex); - + return m_curFrameIndex; } @@ -419,10 +424,10 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); sample_type* pSampleBuffer = sampleBuffer; - size_type numberOfFramesRemaining = - math_min(numberOfFrames, getFrameCount() - m_curFrameIndex); + size_type numberOfFramesRemaining = math_min( + numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); numberOfFramesRemaining = - math_min(numberOfFramesRemaining, samples2frames(sampleBufferSize)); + math_min(numberOfFramesRemaining, samples2frames(sampleBufferSize)); while (0 < numberOfFramesRemaining) { if (0 >= m_madSynthCount) { // When all decoded output data has been consumed... @@ -439,9 +444,9 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { if ((pMadThisFrame != m_madStream.this_frame) && - (NULL != pSampleBuffer)) { - qWarning() << "Recoverable MP3 frame decoding error:" - << mad_stream_errorstr(&m_madStream); + (NULL != pSampleBuffer)) { + qWarning() << "Recoverable MP3 frame decoding error:" + << mad_stream_errorstr(&m_madStream); } // Acknowledge error... m_madStream.error = MAD_ERROR_NONE; @@ -456,8 +461,6 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( } } if (pMadThisFrame == m_madStream.this_frame) { - // Only seems to occur at the beginning of a stream!? - DEBUG_ASSERT(0 == m_curFrameIndex); qDebug() << "Retry decoding MP3 frame @" << m_curFrameIndex; // Retry continue; @@ -473,7 +476,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( } const size_type synthReadCount = math_min( - m_madSynthCount, numberOfFramesRemaining); + m_madSynthCount, numberOfFramesRemaining); if (NULL != pSampleBuffer) { DEBUG_ASSERT(m_madSynthCount <= m_madSynth.pcm.length); const size_type madSynthOffset = diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 5fe91801b20..03be832e044 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -64,6 +64,7 @@ void AudioSourceOggVorbis::close() { AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + const int seekResult = ov_pcm_seek(&m_vf, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggVorbis file:" << seekResult; diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index bcc80d764fe..6b4195f96eb 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -22,7 +22,6 @@ AudioSourcePointer AudioSourceOpus::create(QString fileName) { } Result AudioSourceOpus::open(QString fileName) { - int errorCode = 0; const QByteArray qbaFilename(fileName.toLocal8Bit()); m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); @@ -61,6 +60,7 @@ void AudioSourceOpus::close() { AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggOpus file:" << seekResult; @@ -98,7 +98,7 @@ AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); const size_type numberOfFramesTotal = - math_min(numberOfFrames, samples2frames(sampleBufferSize)); + math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; while (numberOfFramesTotal > numberOfFramesRead) { int readResult = op_read_float_stereo(m_pOggOpusFile, diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index 66bcc6fc93e..aaf34dd7524 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -67,6 +67,7 @@ void AudioSourceSndFile::close() { AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); if (0 <= seekResult) { return seekResult; @@ -79,8 +80,8 @@ AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( AudioSource::size_type AudioSourceSndFile::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { - const sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, - numberOfFrames); + const sf_count_t readCount = + sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); if (0 <= readCount) { return readCount; } else { From 5d4931d03db95d1935678a8636d38b91dca77d34 Mon Sep 17 00:00:00 2001 From: RJ Ryan Date: Thu, 15 Jan 2015 14:26:18 -0800 Subject: [PATCH 102/481] Add workaround for Taglib 1.8.x. Conflicts: src/soundsourcesndfile.cpp --- src/sources/soundsourcesndfile.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index acf9b8ec216..d4a5f999cf1 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -50,18 +50,26 @@ Result SoundSourceSndFile::parseMetadata( if (!readAudioProperties(pMetadata, f)) { return ERR; } - TagLib::ID3v2::Tag *id3v2(f.ID3v2Tag()); + // Taglib 1.8.x doesn't provide an ID3v2Tag method for WAV files. +#if TAGLIB_MAJOR_VERSION == 1 && TAGLIB_MINOR_VERSION == 8 + TagLib::ID3v2::Tag* id3v2(f.tag()); + if (id3v2) { + readID3v2Tag(this, *id3v2); + } +#else + TagLib::ID3v2::Tag* id3v2(f.ID3v2Tag()); if (id3v2) { readID3v2Tag(pMetadata, *id3v2); } else { // fallback - const TagLib::Tag *tag(f.tag()); + const TagLib::Tag* tag(f.tag()); if (tag) { readTag(pMetadata, *tag); } else { return ERR; } } +#endif } else if (getType().startsWith("aif")) { // Try AIFF TagLib::RIFF::AIFF::File f(getFilename().toLocal8Bit().constData()); From 5c9adf0ca7f0d54a23e1e7fb0e8b5281adc463b9 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 16 Jan 2015 16:55:28 +0100 Subject: [PATCH 103/481] Fix open() of AudioSourceM4A --- plugins/soundsourcem4a/audiosourcem4a.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 4b5980203f6..dfda8e49619 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -183,8 +183,12 @@ Result AudioSourceM4A::open(QString fileName) { * frames2samples(kFramesPerSampleBlock); m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); - m_curSampleBlockId = 0; - m_curFrameIndex = kFrameIndexMin; + // Invalidate current position + m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; + m_curFrameIndex = getFrameIndexMax(); + + // Start decoding at the beginning of the file + seekSampleFrame(kFrameIndexMin); return OK; } @@ -214,6 +218,8 @@ bool AudioSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { } void AudioSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { + DEBUG_ASSERT(MP4_INVALID_SAMPLE_ID != sampleBlockId); + NeAACDecPostSeekReset(m_hDecoder, sampleBlockId); m_curSampleBlockId = sampleBlockId; m_curFrameIndex = kFrameIndexMin + @@ -261,10 +267,6 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - if (!isValidSampleBlockId(m_curSampleBlockId)) { - return 0; - } - sample_type* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); From 4d2b0f56270b4defed01ba17f2a02746a314818e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 16 Jan 2015 22:10:00 +0100 Subject: [PATCH 104/481] Fix and align the implementations of various AudioSources --- plugins/soundsourcem4a/audiosourcem4a.cpp | 45 +++++++----- src/sources/audiosource.cpp | 83 +++++++++++++---------- src/sources/audiosource.h | 5 ++ src/sources/audiosourceflac.cpp | 32 ++++++--- src/sources/audiosourceflac.h | 2 + src/sources/audiosourcemp3.cpp | 39 ++++++----- src/sources/audiosourceoggvorbis.cpp | 36 ++++++---- src/sources/audiosourceopus.cpp | 47 +++++++++---- 8 files changed, 187 insertions(+), 102 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index dfda8e49619..cb690aa68b8 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -74,15 +74,15 @@ MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { } -AudioSourceM4A::AudioSourceM4A() : - m_hFile(MP4_INVALID_FILE_HANDLE), - m_trackId(MP4_INVALID_TRACK_ID), - m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), - m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), - m_inputBufferOffset(0), - m_inputBufferLength(0), - m_hDecoder(NULL), - m_curFrameIndex(kFrameIndexMin) { +AudioSourceM4A::AudioSourceM4A() + : m_hFile(MP4_INVALID_FILE_HANDLE), + m_trackId(MP4_INVALID_TRACK_ID), + m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_inputBufferOffset(0), + m_inputBufferLength(0), + m_hDecoder(NULL), + m_curFrameIndex(kFrameIndexMin) { } AudioSourceM4A::~AudioSourceM4A() { @@ -267,9 +267,11 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + const size_type numberOfFramesTotal = math_min(numberOfFrames, + size_type(getFrameIndexMax() - m_curFrameIndex)); + sample_type* pSampleBuffer = sampleBuffer; - size_type numberOfFramesRemaining = - math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); + size_type numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); if (m_inputBufferOffset >= m_inputBufferLength) { @@ -298,21 +300,23 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( // EOF break;// done } - NeAACDecFrameInfo decFrameInfo; - decFrameInfo.bytesconsumed = 0; - decFrameInfo.samples = 0; + // decode samples into sampleBuffer const size_type decodeBufferCapacityInBytes = frames2samples( numberOfFramesRemaining) * sizeof(*sampleBuffer); DEBUG_ASSERT(0 < decodeBufferCapacityInBytes); void* pDecodeBuffer = pSampleBuffer; // in/out parameter + + NeAACDecFrameInfo decFrameInfo; void* pDecodeResult = NeAACDecDecode2(m_hDecoder, &decFrameInfo, &m_inputBuffer[m_inputBufferOffset], m_inputBufferLength - m_inputBufferOffset, &pDecodeBuffer, decodeBufferCapacityInBytes); + // verify our assumptions about the decoding API DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); // verify the in/out parameter DEBUG_ASSERT(pSampleBuffer == pDecodeResult); // verify the result pointer + // verify the decoding result if (0 != decFrameInfo.error) { qWarning() << "AAC decoding error:" @@ -332,17 +336,26 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( << "<>" << getFrameRate(); break; // abort } + // consume input data m_inputBufferOffset += decFrameInfo.bytesconsumed; + // consume decoded output data pSampleBuffer += decFrameInfo.samples; + + // NeAACDecDecode2 always returns a complete sample block. + // As a consequence the last block is padded with 0.0 samples + // and const size_type numberOfFramesDecoded = samples2frames(decFrameInfo.samples); m_curFrameIndex += numberOfFramesDecoded; + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); numberOfFramesRemaining -= numberOfFramesDecoded; } - DEBUG_ASSERT(numberOfFrames >= numberOfFramesRemaining); - return numberOfFrames - numberOfFramesRemaining; + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; } } // namespace Mixxx diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index 5746baa46bc..2b8be0bdeba 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -38,48 +38,61 @@ void AudioSource::reset() { m_bitrate = kBitrateDefault; } +bool AudioSource::isValidSampleBufferSize( + size_type numberOfFrames, + size_type sampleBufferSize, + bool readStereoSamples) const { + if (readStereoSamples) { + return sampleBufferSize >= numberOfFrames * 2; + } else { + return sampleBufferSize >= frames2samples(numberOfFrames); + } +} + AudioSource::size_type AudioSource::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize) { - DEBUG_ASSERT((sampleBufferSize / 2) >= numberOfFrames); + DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, sampleBufferSize, true)); + switch (getChannelCount()) { - case 1: // mono channel - { - const AudioSource::size_type readFrameCount = readSampleFrames( - numberOfFrames, sampleBuffer); - SampleUtil::doubleMonoToDualMono(sampleBuffer, readFrameCount); - return readFrameCount; - } - case 2: // stereo channel(s) - { - return readSampleFrames(numberOfFrames, sampleBuffer); - } - default: // multiple (3 or more) channels - { - const size_type numberOfSamplesToRead = frames2samples(numberOfFrames); - if (numberOfSamplesToRead <= sampleBufferSize) { - // efficient in-place transformation + case 1: // mono channel + { const AudioSource::size_type readFrameCount = readSampleFrames( numberOfFrames, sampleBuffer); - SampleUtil::copyMultiToStereo(sampleBuffer, sampleBuffer, - readFrameCount, getChannelCount()); - return readFrameCount; - } else { - // inefficient transformation through a temporary buffer - qDebug() << "Performance warning:" - << "Allocating a temporary buffer of size" - << numberOfSamplesToRead << "for reading stereo samples." - << "The size of the provided sample buffer is" - << sampleBufferSize; - typedef std::vector SampleBuffer; - SampleBuffer tempBuffer(numberOfSamplesToRead); - const AudioSource::size_type readFrameCount = readSampleFrames( - numberOfFrames, &tempBuffer[0]); - SampleUtil::copyMultiToStereo(sampleBuffer, &tempBuffer[0], - readFrameCount, getChannelCount()); + SampleUtil::doubleMonoToDualMono(sampleBuffer, readFrameCount); return readFrameCount; } - } + case 2: // stereo channel(s) + { + return readSampleFrames(numberOfFrames, sampleBuffer); + } + default: // multiple (3 or more) channels + { + const size_type numberOfSamplesToRead = frames2samples(numberOfFrames); + if (numberOfSamplesToRead <= sampleBufferSize) { + // efficient in-place transformation + const AudioSource::size_type readFrameCount = readSampleFrames( + numberOfFrames, sampleBuffer); + SampleUtil::copyMultiToStereo(sampleBuffer, sampleBuffer, + readFrameCount, getChannelCount()); + return readFrameCount; + } else { + // inefficient transformation through a temporary buffer + qDebug() << "Performance warning:" + << "Allocating a temporary buffer of size" + << numberOfSamplesToRead << "for reading stereo samples." + << "The size of the provided sample buffer is" + << sampleBufferSize; + typedef std::vector SampleBuffer; + SampleBuffer tempBuffer(numberOfSamplesToRead); + const AudioSource::size_type readFrameCount = readSampleFrames( + numberOfFrames, &tempBuffer[0]); + SampleUtil::copyMultiToStereo(sampleBuffer, &tempBuffer[0], + readFrameCount, getChannelCount()); + return readFrameCount; + } + } } } diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index e4afb95ea64..6db27f4ca62 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -220,6 +220,11 @@ class AudioSource { void reset(); + bool isValidSampleBufferSize( + size_type sampleBufferSize, + size_type numberOfFrames, + bool readStereoSamples) const; + private: size_type m_channelCount; size_type m_frameRate; diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 34fe626d53f..40c8f43536d 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -64,7 +64,8 @@ AudioSourceFLAC::AudioSourceFLAC(QString fileName) : m_maxFramesize(0), m_sampleScale(kSampleValueZero), m_decodeSampleBufferReadOffset(0), - m_decodeSampleBufferWriteOffset(0) { + m_decodeSampleBufferWriteOffset(0), + m_curFrameIndex(kFrameIndexMin) { } AudioSourceFLAC::~AudioSourceFLAC() { @@ -108,6 +109,8 @@ Result AudioSourceFLAC::open() { return ERR; } + m_curFrameIndex = kFrameIndexMin; + return OK; } @@ -124,6 +127,7 @@ void AudioSourceFLAC::close() { Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame( diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); // clear decode buffer before seeking @@ -136,7 +140,10 @@ Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame( !FLAC__stream_decoder_flush(m_decoder)) { qWarning() << "SSFLAC: Failed to flush the decoder's input buffer after seeking" << m_file.fileName(); } - return frameIndex; + m_curFrameIndex = frameIndex; + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + return m_curFrameIndex; } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( @@ -155,10 +162,15 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFramesStereo( Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, + sampleBufferSize, readStereoSamples)); + + const size_type numberOfFramesTotal = numberOfFrames; + sample_type* outBuffer = sampleBuffer; - const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); - size_type numberOfFramesRead = 0; - while (numberOfFramesTotal > numberOfFramesRead) { + size_type numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); // if our buffer from libflac is empty (either because we explicitly cleared @@ -182,7 +194,7 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( const size_type decodeBufferFrames = samples2frames( decodeBufferSamples); const size_type framesToCopy = - math_min(decodeBufferFrames, numberOfFramesTotal - numberOfFramesRead); + math_min(decodeBufferFrames, numberOfFramesRemaining); const size_type samplesToCopy = frames2samples(framesToCopy); if (readStereoSamples && !isChannelCountStereo()) { if (isChannelCountMono()) { @@ -202,11 +214,15 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( outBuffer += samplesToCopy; } m_decodeSampleBufferReadOffset += samplesToCopy; - numberOfFramesRead += framesToCopy; + m_curFrameIndex += framesToCopy; + numberOfFramesRemaining -= framesToCopy; DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); } - return numberOfFramesRead; + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; } // flac callback methods diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 4f2b35c92fd..11bf252376d 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -65,6 +65,8 @@ class AudioSourceFLAC: public AudioSource { std::vector m_decodeSampleBuffer; SampleBuffer::size_type m_decodeSampleBufferReadOffset; SampleBuffer::size_type m_decodeSampleBufferWriteOffset; + + diff_type m_curFrameIndex; }; } diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 80c8cd70c8d..dcb879693ec 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -52,13 +52,12 @@ int decodeFrameHeader( } // anonymous namespace AudioSourceMp3::AudioSourceMp3(QString fileName) - : - m_file(fileName), - m_fileSize(0), - m_pFileData(NULL), - m_avgSeekFrameCount(0), - m_curFrameIndex(kFrameIndexMin), - m_madSynthCount(0) { + : m_file(fileName), + m_fileSize(0), + m_pFileData(NULL), + m_avgSeekFrameCount(0), + m_curFrameIndex(kFrameIndexMin), + m_madSynthCount(0) { mad_stream_init(&m_madStream); mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); mad_frame_init(&m_madFrame); @@ -356,6 +355,7 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( } AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); SeekFrameList::size_type seekFrameIndex = findSeekFrameIndex( @@ -402,19 +402,22 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { skipFrameSamples(skipFrameCount); DEBUG_ASSERT(m_curFrameIndex == frameIndex); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); return m_curFrameIndex; } AudioSource::size_type AudioSourceMp3::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, - frames2samples(numberOfFrames), false); + return readSampleFrames(numberOfFrames, + sampleBuffer, frames2samples(numberOfFrames), + false); } AudioSource::size_type AudioSourceMp3::readSampleFramesStereo( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { - return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, + return readSampleFrames(numberOfFrames, + sampleBuffer, sampleBufferSize, true); } @@ -422,12 +425,14 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, + sampleBufferSize, readStereoSamples)); + + const size_type numberOfFramesTotal = math_min(numberOfFrames, + size_type(getFrameIndexMax() - m_curFrameIndex)); sample_type* pSampleBuffer = sampleBuffer; - size_type numberOfFramesRemaining = math_min( - numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); - numberOfFramesRemaining = - math_min(numberOfFramesRemaining, samples2frames(sampleBufferSize)); + size_type numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { if (0 >= m_madSynthCount) { // When all decoded output data has been consumed... @@ -517,8 +522,10 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( m_madSynthCount -= synthReadCount; m_curFrameIndex += synthReadCount; } - DEBUG_ASSERT(numberOfFrames >= numberOfFramesRemaining); - return numberOfFrames - numberOfFramesRemaining; + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; } } // namespace Mixxx diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 03be832e044..8ef982c11c5 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -63,12 +63,15 @@ void AudioSourceOggVorbis::close() { AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); const int seekResult = ov_pcm_seek(&m_vf, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggVorbis file:" << seekResult; } + + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); return getCurrentFrameIndex(); } @@ -89,15 +92,19 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, + sampleBufferSize, readStereoSamples)); - sample_type* nextSample = sampleBuffer; - const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); - size_type numberOfFramesRead = 0; - while (numberOfFramesTotal > numberOfFramesRead) { + const size_type numberOfFramesTotal = math_min(numberOfFrames, + size_type(getFrameIndexMax() - getCurrentFrameIndex())); + + sample_type* pSampleBuffer = sampleBuffer; + size_type numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { float** pcmChannels; int currentSection; const long readResult = ov_read_float(&m_vf, &pcmChannels, - numberOfFramesTotal - numberOfFramesRead, ¤tSection); + numberOfFramesRemaining, ¤tSection); if (0 == readResult) { // EOF break;// done @@ -106,33 +113,36 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( if (isChannelCountMono()) { if (readStereoSamples) { for (long i = 0; i < readResult; ++i) { - *nextSample++ = pcmChannels[0][i]; - *nextSample++ = pcmChannels[0][i]; + *pSampleBuffer++ = pcmChannels[0][i]; + *pSampleBuffer++ = pcmChannels[0][i]; } } else { for (long i = 0; i < readResult; ++i) { - *nextSample++ = pcmChannels[0][i]; + *pSampleBuffer++ = pcmChannels[0][i]; } } } else if (isChannelCountStereo() || readStereoSamples) { for (long i = 0; i < readResult; ++i) { - *nextSample++ = pcmChannels[0][i]; - *nextSample++ = pcmChannels[1][i]; + *pSampleBuffer++ = pcmChannels[0][i]; + *pSampleBuffer++ = pcmChannels[1][i]; } } else { for (long i = 0; i < readResult; ++i) { for (size_type j = 0; j < getChannelCount(); ++j) { - *nextSample++ = pcmChannels[j][i]; + *pSampleBuffer++ = pcmChannels[j][i]; } } } - numberOfFramesRead += readResult; + numberOfFramesRemaining -= readResult; } else { qWarning() << "Failed to read from OggVorbis file:" << readResult; break; // abort } } - return numberOfFramesRead; + + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; } } // namespace Mixxx diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 6b4195f96eb..38e598be8cb 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -59,12 +59,15 @@ void AudioSourceOpus::close() { } AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggOpus file:" << seekResult; } + + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); return getCurrentFrameIndex(); } @@ -72,51 +75,67 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); - size_type readCount = 0; - while (readCount < numberOfFrames) { + const size_type numberOfFramesTotal = math_min(numberOfFrames, + size_type(getFrameIndexMax() - getCurrentFrameIndex())); + + sample_type* pSampleBuffer = sampleBuffer; + size_type numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { int readResult = op_read_float(m_pOggOpusFile, - sampleBuffer + frames2samples(readCount), - frames2samples(numberOfFrames - readCount), NULL); + pSampleBuffer, + frames2samples(numberOfFramesRemaining), NULL); if (0 == readResult) { // EOF break;// done } if (0 < readResult) { - readCount += readResult; + pSampleBuffer += frames2samples(readResult); + numberOfFramesRemaining -= readResult; } else { qWarning() << "Failed to read sample data from OggOpus file:" << readResult; break; // abort } } - return readCount; + + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; } AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, + sampleBufferSize, true)); + + const size_type numberOfFramesTotal = math_min(numberOfFrames, + size_type(getFrameIndexMax() - getCurrentFrameIndex())); - const size_type numberOfFramesTotal = - math_min(numberOfFrames, samples2frames(sampleBufferSize)); - size_type numberOfFramesRead = 0; - while (numberOfFramesTotal > numberOfFramesRead) { + sample_type* pSampleBuffer = sampleBuffer; + size_type numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { int readResult = op_read_float_stereo(m_pOggOpusFile, - sampleBuffer + (numberOfFramesRead * 2), - (numberOfFramesTotal - numberOfFramesRead) * 2); + pSampleBuffer, + numberOfFramesRemaining * 2); if (0 == readResult) { // EOF break;// done } if (0 < readResult) { - numberOfFramesRead += readResult; + pSampleBuffer += readResult * 2; + numberOfFramesRemaining -= readResult; } else { qWarning() << "Failed to read sample data from OggOpus file:" << readResult; break; // abort } } - return numberOfFramesRead; + + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; } } // namespace Mixxx From 64c82ccc7f2f838d1d778acf2c4cf690f52466d2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 16 Jan 2015 22:11:54 +0100 Subject: [PATCH 105/481] Early detection of non-compliant AudioSources in AnalyserQueue --- src/analyserqueue.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 39ef9cbd893..691539ee060 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -173,29 +173,27 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi do { ScopedTimer t("AnalyserQueue::doAnalysis block"); + DEBUG_ASSERT(progressFrameCount < pAudioSource->getFrameCount()); const Mixxx::AudioSource::size_type readFrameCount = pAudioSource->readSampleFramesStereo(kAnalysisFrameCount, &m_sampleBuffer[0], m_sampleBuffer.size()); + progressFrameCount += readFrameCount; + DEBUG_ASSERT(progressFrameCount <= pAudioSource->getFrameCount()); // To compare apples to apples, let's only look at blocks that are the // full block size. - if (readFrameCount < kAnalysisFrameCount) { - // The whole file should have been read now! - DEBUG_ASSERT(pAudioSource->getFrameCount() == (progressFrameCount + readFrameCount)); - break; // done - } - - QListIterator it(m_aq); - while (it.hasNext()) { - Analyser* an = it.next(); - //qDebug() << typeid(*an).name() << ".process()"; - an->process(&m_sampleBuffer[0], readFrameCount * kAnalysisChannels); - //qDebug() << "Done " << typeid(*an).name() << ".process()"; + if (kAnalysisFrameCount == readFrameCount) { + QListIterator it(m_aq); + while (it.hasNext()) { + Analyser* an = it.next(); + //qDebug() << typeid(*an).name() << ".process()"; + an->process(&m_sampleBuffer[0], readFrameCount * kAnalysisChannels); + //qDebug() << "Done " << typeid(*an).name() << ".process()"; + } } // emit progress updates // During the doAnalysis function it goes only to 100% - FINALIZE_PERCENT // because the finalise functions will take also some time //fp div here prevents insane signed overflow - progressFrameCount += readFrameCount; DEBUG_ASSERT(progressFrameCount <= pAudioSource->getFrameCount()); int progress = (int)(((float)progressFrameCount) / pAudioSource->getFrameCount() * (1000 - FINALIZE_PERCENT)); From 11a36e714fbb926f8d80971304b09ad902fcde87 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 17 Jan 2015 01:43:41 +0100 Subject: [PATCH 106/481] Don't try to re-analyse corrupt tracks --- plugins/soundsourcem4a/audiosourcem4a.cpp | 35 ++++++++++++----------- plugins/soundsourcem4a/audiosourcem4a.h | 6 ++-- src/analyserqueue.cpp | 7 +++++ 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index cb690aa68b8..59a92775643 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -74,8 +74,9 @@ MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { } -AudioSourceM4A::AudioSourceM4A() - : m_hFile(MP4_INVALID_FILE_HANDLE), +AudioSourceM4A::AudioSourceM4A(QString fileName) + : m_fileName(fileName), + m_hFile(MP4_INVALID_FILE_HANDLE), m_trackId(MP4_INVALID_TRACK_ID), m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), @@ -90,8 +91,8 @@ AudioSourceM4A::~AudioSourceM4A() { } AudioSourcePointer AudioSourceM4A::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceM4A); - if (OK == pAudioSource->open(fileName)) { + QSharedPointer pAudioSource(new AudioSourceM4A(fileName)); + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -100,27 +101,27 @@ AudioSourcePointer AudioSourceM4A::create(QString fileName) { } } -Result AudioSourceM4A::open(QString fileName) { +Result AudioSourceM4A::open() { /* open MP4 file, check for >= ver 1.9.1 */ #if MP4V2_PROJECT_version_hex <= 0x00010901 - m_hFile = MP4Read(fileName.toLocal8Bit().constData(), 0); + m_hFile = MP4Read(m_fileName.toLocal8Bit().constData(), 0); #else - m_hFile = MP4Read(fileName.toLocal8Bit().constData()); + m_hFile = MP4Read(m_fileName.toLocal8Bit().constData()); #endif if (MP4_INVALID_FILE_HANDLE == m_hFile) { - qWarning() << "Failed to open file for reading:" << fileName; + qWarning() << "Failed to open file for reading:" << m_fileName; return ERR; } m_trackId = findFirstAudioTrackId(m_hFile); if (MP4_INVALID_TRACK_ID == m_trackId) { - qWarning() << "No AAC track found in file:" << fileName; + qWarning() << "No AAC track found in file:" << m_fileName; return ERR; } m_maxSampleBlockId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); if (MP4_INVALID_SAMPLE_ID == m_maxSampleBlockId) { - qWarning() << "Failed to read file structure:" << fileName; + qWarning() << "Failed to read file structure:" << m_fileName; return ERR; } @@ -312,17 +313,19 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( &m_inputBuffer[m_inputBufferOffset], m_inputBufferLength - m_inputBufferOffset, &pDecodeBuffer, decodeBufferCapacityInBytes); - - // verify our assumptions about the decoding API - DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); // verify the in/out parameter - DEBUG_ASSERT(pSampleBuffer == pDecodeResult); // verify the result pointer - // verify the decoding result if (0 != decFrameInfo.error) { qWarning() << "AAC decoding error:" - << NeAACDecGetErrorMessage(decFrameInfo.error); + << decFrameInfo.error + << NeAACDecGetErrorMessage(decFrameInfo.error) + << m_fileName; break; // abort } + + // verify our assumptions about the decoding API + DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); // verify the in/out parameter + DEBUG_ASSERT(pSampleBuffer == pDecodeResult); // verify the result pointer + // verify the decoded sample data for consistency if (getChannelCount() != decFrameInfo.channels) { qWarning() << "Corrupt or unsupported AAC file:" diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 7687e18e9d7..be45ebff386 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -35,9 +35,9 @@ class AudioSourceM4A: public AudioSource { sample_type* sampleBuffer) /*override*/; private: - AudioSourceM4A(); + explicit AudioSourceM4A(QString fileName); - Result open(QString fileName); + Result open(); void close(); @@ -45,6 +45,8 @@ class AudioSourceM4A: public AudioSource { void restartDecoding(MP4SampleId sampleBlockId); + const QString m_fileName; + MP4FileHandle m_hFile; MP4TrackId m_trackId; MP4SampleId m_maxSampleBlockId; diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 691539ee060..de33aea9ee9 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -188,6 +188,13 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi an->process(&m_sampleBuffer[0], readFrameCount * kAnalysisChannels); //qDebug() << "Done " << typeid(*an).name() << ".process()"; } + } else { + if (progressFrameCount < pAudioSource->getFrameCount()) { + qWarning() << "Failed to read sample data from file:" + << tio->getFilename(); + dieflag = true; // abort + cancelled = false; // completed, no retry + } } // emit progress updates From 93e04a4ad58f78f6c8d94b5326547fe85c637943 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 17 Jan 2015 02:10:58 +0100 Subject: [PATCH 107/481] Shut up "Note: nonZeroCount was..." --- vamp-plugins/plugins/BeatTrack.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vamp-plugins/plugins/BeatTrack.cpp b/vamp-plugins/plugins/BeatTrack.cpp index f656ecaac6c..5af68a54a02 100644 --- a/vamp-plugins/plugins/BeatTrack.cpp +++ b/vamp-plugins/plugins/BeatTrack.cpp @@ -453,7 +453,7 @@ BeatTracker::beatTrackNew() --nonZeroCount; } - std::cerr << "Note: nonZeroCount was " << m_d->dfOutput.size() << ", is now " << nonZeroCount << std::endl; + //std::cerr << "Note: nonZeroCount was " << m_d->dfOutput.size() << ", is now " << nonZeroCount << std::endl; for (size_t i = 2; i < nonZeroCount; ++i) { // discard first two elts df.push_back(m_d->dfOutput[i]); From 1d9956829486579bddbd4a8a0fbbadab3d36701f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 18 Jan 2015 22:08:08 +0100 Subject: [PATCH 108/481] Improve readability of sample buffer size assertions --- src/sources/audiosource.cpp | 9 ++++----- src/sources/audiosource.h | 5 ++--- src/sources/audiosourceflac.cpp | 3 +-- src/sources/audiosourcemp3.cpp | 3 +-- src/sources/audiosourceoggvorbis.cpp | 3 +-- src/sources/audiosourceopus.cpp | 3 +-- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index 2b8be0bdeba..0e94aa12314 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -38,14 +38,13 @@ void AudioSource::reset() { m_bitrate = kBitrateDefault; } -bool AudioSource::isValidSampleBufferSize( +AudioSource::size_type AudioSource::getSampleBufferSize( size_type numberOfFrames, - size_type sampleBufferSize, bool readStereoSamples) const { if (readStereoSamples) { - return sampleBufferSize >= numberOfFrames * 2; + return numberOfFrames * 2; } else { - return sampleBufferSize >= frames2samples(numberOfFrames); + return frames2samples(numberOfFrames); } } @@ -53,7 +52,7 @@ AudioSource::size_type AudioSource::readSampleFramesStereo( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { - DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, sampleBufferSize, true)); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) >= sampleBufferSize); switch (getChannelCount()) { case 1: // mono channel diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 6db27f4ca62..0b27aea15dc 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -220,10 +220,9 @@ class AudioSource { void reset(); - bool isValidSampleBufferSize( - size_type sampleBufferSize, + size_type getSampleBufferSize( size_type numberOfFrames, - bool readStereoSamples) const; + bool readStereoSamples = false) const; private: size_type m_channelCount; diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 40c8f43536d..8f64135dad5 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -163,8 +163,7 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, - sampleBufferSize, readStereoSamples)); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) >= sampleBufferSize); const size_type numberOfFramesTotal = numberOfFrames; diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index dcb879693ec..b327c98009a 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -425,8 +425,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, - sampleBufferSize, readStereoSamples)); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) >= sampleBufferSize); const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 8ef982c11c5..7fe075e10a6 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -92,8 +92,7 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); - DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, - sampleBufferSize, readStereoSamples)); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) >= sampleBufferSize); const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - getCurrentFrameIndex())); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 38e598be8cb..87a9fec8347 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -107,8 +107,7 @@ AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); - DEBUG_ASSERT(isValidSampleBufferSize(numberOfFrames, - sampleBufferSize, true)); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) >= sampleBufferSize); const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - getCurrentFrameIndex())); From 8bb1433387a78756242627a258d7b0d618ce8c4a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 18 Jan 2015 22:46:57 +0100 Subject: [PATCH 109/481] Create AudioSources from URLs --- plugins/soundsourcem4a/audiosourcem4a.cpp | 21 +++++----- plugins/soundsourcem4a/audiosourcem4a.h | 6 +-- plugins/soundsourcem4a/soundsourcem4a.cpp | 2 +- .../audiosourcemediafoundation.cpp | 32 ++++++++++----- .../audiosourcemediafoundation.h | 6 +-- .../soundsourcemediafoundation.cpp | 2 +- plugins/soundsourcewv/audiosourcewv.cpp | 16 ++++---- plugins/soundsourcewv/audiosourcewv.h | 6 +-- plugins/soundsourcewv/soundsourcewv.cpp | 2 +- src/sources/audiosource.cpp | 3 +- src/sources/audiosource.h | 9 ++++- src/sources/audiosourcecoreaudio.cpp | 15 ++++--- src/sources/audiosourcecoreaudio.h | 6 +-- src/sources/audiosourceffmpeg.cpp | 40 ++++++++++--------- src/sources/audiosourceffmpeg.h | 6 +-- src/sources/audiosourceflac.cpp | 27 +++++++------ src/sources/audiosourceflac.h | 4 +- src/sources/audiosourcemodplug.cpp | 19 +++++---- src/sources/audiosourcemodplug.h | 6 +-- src/sources/audiosourcemp3.cpp | 9 +++-- src/sources/audiosourcemp3.h | 4 +- src/sources/audiosourceoggvorbis.cpp | 12 +++--- src/sources/audiosourceoggvorbis.h | 6 +-- src/sources/audiosourceopus.cpp | 16 ++++---- src/sources/audiosourceopus.h | 6 +-- src/sources/audiosourcesndfile.cpp | 14 ++++--- src/sources/audiosourcesndfile.h | 6 +-- src/sources/soundsourcecoreaudio.cpp | 2 +- src/sources/soundsourceffmpeg.cpp | 2 +- src/sources/soundsourceflac.cpp | 2 +- src/sources/soundsourcemodplug.cpp | 2 +- src/sources/soundsourcemp3.cpp | 2 +- src/sources/soundsourceoggvorbis.cpp | 2 +- src/sources/soundsourceopus.cpp | 2 +- src/sources/soundsourcesndfile.cpp | 2 +- 35 files changed, 177 insertions(+), 140 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 59a92775643..f1a0188fc5b 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -74,8 +74,8 @@ MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { } -AudioSourceM4A::AudioSourceM4A(QString fileName) - : m_fileName(fileName), +AudioSourceM4A::AudioSourceM4A(QUrl url) + : AudioSource(url), m_hFile(MP4_INVALID_FILE_HANDLE), m_trackId(MP4_INVALID_TRACK_ID), m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), @@ -90,8 +90,8 @@ AudioSourceM4A::~AudioSourceM4A() { close(); } -AudioSourcePointer AudioSourceM4A::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceM4A(fileName)); +AudioSourcePointer AudioSourceM4A::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceM4A(url)); if (OK == pAudioSource->open()) { // success return pAudioSource; @@ -102,26 +102,27 @@ AudioSourcePointer AudioSourceM4A::create(QString fileName) { } Result AudioSourceM4A::open() { + const QString fileName(getUrl().toLocalFile()); /* open MP4 file, check for >= ver 1.9.1 */ #if MP4V2_PROJECT_version_hex <= 0x00010901 - m_hFile = MP4Read(m_fileName.toLocal8Bit().constData(), 0); + m_hFile = MP4Read(fileName.toLocal8Bit().constData(), 0); #else - m_hFile = MP4Read(m_fileName.toLocal8Bit().constData()); + m_hFile = MP4Read(fileName.toLocal8Bit().constData()); #endif if (MP4_INVALID_FILE_HANDLE == m_hFile) { - qWarning() << "Failed to open file for reading:" << m_fileName; + qWarning() << "Failed to open file for reading:" << fileName; return ERR; } m_trackId = findFirstAudioTrackId(m_hFile); if (MP4_INVALID_TRACK_ID == m_trackId) { - qWarning() << "No AAC track found in file:" << m_fileName; + qWarning() << "No AAC track found:" << getUrl(); return ERR; } m_maxSampleBlockId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); if (MP4_INVALID_SAMPLE_ID == m_maxSampleBlockId) { - qWarning() << "Failed to read file structure:" << m_fileName; + qWarning() << "Failed to read file structure:" << getUrl(); return ERR; } @@ -318,7 +319,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( qWarning() << "AAC decoding error:" << decFrameInfo.error << NeAACDecGetErrorMessage(decFrameInfo.error) - << m_fileName; + << getUrl(); break; // abort } diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index be45ebff386..28432bb23fe 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -25,7 +25,7 @@ namespace Mixxx { class AudioSourceM4A: public AudioSource { public: - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceM4A(); @@ -35,7 +35,7 @@ class AudioSourceM4A: public AudioSource { sample_type* sampleBuffer) /*override*/; private: - explicit AudioSourceM4A(QString fileName); + explicit AudioSourceM4A(QUrl url); Result open(); @@ -45,8 +45,6 @@ class AudioSourceM4A: public AudioSource { void restartDecoding(MP4SampleId sampleBlockId); - const QString m_fileName; - MP4FileHandle m_hFile; MP4TrackId m_trackId; MP4SampleId m_maxSampleBlockId; diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 69253eb0adf..03fe4419ddc 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -68,7 +68,7 @@ QImage SoundSourceM4A::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceM4A::open() const { - return Mixxx::AudioSourceM4A::create(getFilename()); + return Mixxx::AudioSourceM4A::create(QUrl::fromLocalFile(getFilename())); } } // namespace Mixxx diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index 3b743d36aa2..789216fc776 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -64,12 +64,22 @@ template static void safeRelease(T **ppT) { } -AudioSourceMediaFoundation::AudioSourceMediaFoundation() - : m_hrCoInitialize(E_FAIL), m_hrMFStartup(E_FAIL), - m_pReader(NULL), m_pAudioType(NULL), m_wcFilename( - NULL), m_nextFrame(0), m_leftoverBuffer(NULL), m_leftoverBufferSize( - 0), m_leftoverBufferLength(0), m_leftoverBufferPosition(0), m_mfDuration( - 0), m_iCurrentPosition(0), m_dead(false), m_seeking(false) { +AudioSourceMediaFoundation::AudioSourceMediaFoundation(QUrl url) + : AudioSource(url), + m_hrCoInitialize(E_FAIL), + m_hrMFStartup(E_FAIL), + m_pReader(NULL), + m_pAudioType(NULL), + m_wcFilename(NULL), + m_nextFrame(0), + m_leftoverBuffer(NULL), + m_leftoverBufferSize(0), + m_leftoverBufferLength(0), + m_leftoverBufferPosition(0), + m_mfDuration(0), + m_iCurrentPosition(0), + m_dead(false), + m_seeking(false) { // these are always the same, might as well just stick them here // -bkgood @@ -87,9 +97,9 @@ AudioSourceMediaFoundation::~AudioSourceMediaFoundation() { close(); } -AudioSourcePointer AudioSourceMediaFoundation::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceMediaFoundation); - if (OK == pAudioSource->open(fileName)) { +AudioSourcePointer AudioSourceMediaFoundation::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceMediaFoundation(url)); + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -98,7 +108,9 @@ AudioSourcePointer AudioSourceMediaFoundation::create(QString fileName) { } } -Result AudioSourceMediaFoundation::open(QString fileName) { +Result AudioSourceMediaFoundation::open() { + const QString fileName(getUrl().toLocalFile()); + if (sDebug) { qDebug() << "open()" << fileName; } diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index 63ea718e66f..717da1fe842 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -26,7 +26,7 @@ namespace Mixxx { class AudioSourceMediaFoundation : public AudioSource { public: - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceMediaFoundation(); @@ -35,9 +35,9 @@ class AudioSourceMediaFoundation : public AudioSource { size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: - AudioSourceMediaFoundation(); + explicit AudioSourceMediaFoundation(QUrl url); - Result open(QString fileName); + Result open(); void close(); diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 7f3736697e0..957c434d528 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -76,7 +76,7 @@ QImage SoundSourceMediaFoundation::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceMediaFoundation::open() const { - return Mixxx::AudioSourceMediaFoundation::create(getFilename()); + return Mixxx::AudioSourceMediaFoundation::create(QUrl::fromLocalFile(getFilename())); } extern "C" MY_EXPORT const char* getMixxxVersion() { diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 00880d96fd3..38e559246d0 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -2,18 +2,19 @@ namespace Mixxx { -AudioSourceWV::AudioSourceWV() : - m_wpc(NULL), - m_sampleScale(0.0f) { +AudioSourceWV::AudioSourceWV(QUrl url) + : AudioSource(url), + m_wpc(NULL), + m_sampleScale(0.0f) { } AudioSourceWV::~AudioSourceWV() { close(); } -AudioSourcePointer AudioSourceWV::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceWV); - if (OK == pAudioSource->open(fileName)) { +AudioSourcePointer AudioSourceWV::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceWV(url)); + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -22,7 +23,8 @@ AudioSourcePointer AudioSourceWV::create(QString fileName) { } } -Result AudioSourceWV::open(QString fileName) { +Result AudioSourceWV::open() { + const QString fileName(getUrl().toLocalFile()); char msg[80]; // hold possible error message m_wpc = WavpackOpenFileInput( fileName.toLocal8Bit().constData(), msg, diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index f959aa206a9..da11f282936 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -16,7 +16,7 @@ namespace Mixxx { class AudioSourceWV: public AudioSource { public: - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceWV(); @@ -26,9 +26,9 @@ class AudioSourceWV: public AudioSource { sample_type* sampleBuffer) /*override*/; private: - AudioSourceWV(); + explicit AudioSourceWV(QUrl url); - Result open(QString fileName); + Result open(); void close(); diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index cc85e710e85..ecb925f2fa6 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -51,7 +51,7 @@ QImage SoundSourceWV::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceWV::open() const { - return Mixxx::AudioSourceWV::create(getFilename()); + return Mixxx::AudioSourceWV::create(QUrl::fromLocalFile(getFilename())); } } // namespace Mixxx diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index 0e94aa12314..c2dfcd0962d 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -11,7 +11,8 @@ namespace Mixxx { /*static*/const AudioSource::sample_type AudioSource::kSampleValuePeak = CSAMPLE_PEAK; -AudioSource::AudioSource() : +AudioSource::AudioSource(QUrl url) : + m_url(url), m_channelCount(kChannelCountDefault), m_frameRate(kFrameRateDefault), m_frameCount(kFrameCountDefault), diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 0b27aea15dc..ba94417ff00 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -5,6 +5,7 @@ #include "util/types.h" // CSAMPLE #include +#include #include // size_t / diff_t @@ -54,6 +55,10 @@ class AudioSource { virtual ~AudioSource(); + const QUrl& getUrl() const { + return m_url; + } + // Returns the number of channels. The number of channels // must be constant over time. inline size_type getChannelCount() const { @@ -208,7 +213,7 @@ class AudioSource { sample_type* sampleBuffer, size_type sampleBufferSize); protected: - AudioSource(); + explicit AudioSource(QUrl url); void setChannelCount(size_type channelCount); void setFrameRate(size_type frameRate); @@ -225,6 +230,8 @@ class AudioSource { bool readStereoSamples = false) const; private: + const QUrl m_url; + size_type m_channelCount; size_type m_frameRate; size_type m_frameCount; diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index 192799fff62..a29c7fc2c7a 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -10,17 +10,18 @@ namespace { AudioSource::size_type kChannelCount = 2; } -AudioSourceCoreAudio::AudioSourceCoreAudio() : - m_headerFrames(0) { +AudioSourceCoreAudio::AudioSourceCoreAudio(QUrl url) + : AudioSource(url), + m_headerFrames(0) { } AudioSourceCoreAudio::~AudioSourceCoreAudio() { close(); } -AudioSourcePointer AudioSourceCoreAudio::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceCoreAudio); - if (OK == pAudioSource->open(fileName)) { +AudioSourcePointer AudioSourceCoreAudio::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceCoreAudio(url)); + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -30,7 +31,9 @@ AudioSourcePointer AudioSourceCoreAudio::create(QString fileName) { } // soundsource overrides -Result AudioSourceCoreAudio::open(QString fileName) { +Result AudioSourceCoreAudio::open() { + const QString fileName(getUrl().toLocalFile()); + //Open the audio file. OSStatus err; diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index ae1fbb9c849..4489f42ef23 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -23,7 +23,7 @@ namespace Mixxx { class AudioSourceCoreAudio: public AudioSource { public: - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceCoreAudio(); @@ -32,9 +32,9 @@ class AudioSourceCoreAudio: public AudioSource { sample_type* sampleBuffer) /*override*/; private: - AudioSourceCoreAudio(); + explicit AudioSourceCoreAudio(QUrl url); - Result open(QString fileName); + Result open(); void close(); diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index dc77850ede2..08c32b9c269 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -9,29 +9,30 @@ namespace Mixxx { -AudioSourceFFmpeg::AudioSourceFFmpeg() - : m_pFormatCtx(NULL) - , m_iAudioStream(-1) - , m_pCodecCtx(NULL) - , m_pCodec(NULL) - , m_pResample(NULL) - , m_iCurrentMixxTs(0) - , m_bIsSeeked(false) - , m_lCacheBytePos(0) - , m_lCacheStartByte(0) - , m_lCacheEndByte(0) - , m_lCacheLastPos(0) - , m_lLastStoredPos(0) - , m_lStoredSeekPoint(-1) { +AudioSourceFFmpeg::AudioSourceFFmpeg(QUrl url) + : AudioSource(url), + m_pFormatCtx(NULL), + m_iAudioStream(-1), + m_pCodecCtx(NULL), + m_pCodec(NULL), + m_pResample(NULL), + m_iCurrentMixxTs(0), + m_bIsSeeked(false), + m_lCacheBytePos(0), + m_lCacheStartByte(0), + m_lCacheEndByte(0), + m_lCacheLastPos(0), + m_lLastStoredPos(0), + m_lStoredSeekPoint(-1) { } AudioSourceFFmpeg::~AudioSourceFFmpeg() { close(); } -AudioSourcePointer AudioSourceFFmpeg::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceFFmpeg); - if (OK == pAudioSource->open(fileName)) { +AudioSourcePointer AudioSourceFFmpeg::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceFFmpeg(url)); + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -40,11 +41,12 @@ AudioSourcePointer AudioSourceFFmpeg::create(QString fileName) { } } -Result AudioSourceFFmpeg::open(QString fileName) { +Result AudioSourceFFmpeg::open() { unsigned int i; AVDictionary *l_iFormatOpts = NULL; - QByteArray qBAFilename = fileName.toLocal8Bit(); + const QString fileName(getUrl().toLocalFile()); + const QByteArray qBAFilename = fileName.toLocal8Bit(); qDebug() << "New AudioSourceFFmpeg :" << qBAFilename; diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index ab85b792999..ba94880285c 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -43,7 +43,7 @@ struct ffmpegCacheObject { class AudioSourceFFmpeg : public AudioSource { public: - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceFFmpeg(); @@ -52,9 +52,9 @@ class AudioSourceFFmpeg : public AudioSource { size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: - AudioSourceFFmpeg(); + explicit AudioSourceFFmpeg(QUrl url); - Result open(QString fileName); + Result open(); void close(); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 8f64135dad5..d0736a94c31 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -55,25 +55,26 @@ void FLAC_error_cb(const FLAC__StreamDecoder*, // end callbacks } -AudioSourceFLAC::AudioSourceFLAC(QString fileName) : - m_file(fileName), - m_decoder(NULL), - m_minBlocksize(0), - m_maxBlocksize(0), - m_minFramesize(0), - m_maxFramesize(0), - m_sampleScale(kSampleValueZero), - m_decodeSampleBufferReadOffset(0), - m_decodeSampleBufferWriteOffset(0), - m_curFrameIndex(kFrameIndexMin) { +AudioSourceFLAC::AudioSourceFLAC(QUrl url) + : AudioSource(url), + m_file(getUrl().toLocalFile()), + m_decoder(NULL), + m_minBlocksize(0), + m_maxBlocksize(0), + m_minFramesize(0), + m_maxFramesize(0), + m_sampleScale(kSampleValueZero), + m_decodeSampleBufferReadOffset(0), + m_decodeSampleBufferWriteOffset(0), + m_curFrameIndex(kFrameIndexMin) { } AudioSourceFLAC::~AudioSourceFLAC() { close(); } -AudioSourcePointer AudioSourceFLAC::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceFLAC(fileName)); +AudioSourcePointer AudioSourceFLAC::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceFLAC(url)); if (OK == pAudioSource->open()) { // success return pAudioSource; diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 11bf252376d..fc3ed958319 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -14,7 +14,7 @@ namespace Mixxx { class AudioSourceFLAC: public AudioSource { public: - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceFLAC(); @@ -37,7 +37,7 @@ class AudioSourceFLAC: public AudioSource { void flacError(FLAC__StreamDecoderErrorStatus status); private: - explicit AudioSourceFLAC(QString fileName); + explicit AudioSourceFLAC(QUrl url); Result open(); diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index 7499a15e4dd..aa21afeeb78 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -36,19 +36,20 @@ void AudioSourceModPlug::configure(unsigned int bufferSizeLimit, ModPlug::ModPlug_SetSettings(&settings); } -AudioSourceModPlug::AudioSourceModPlug() : - m_pModFile(NULL), - m_fileLength(0), - m_seekPos(0) { +AudioSourceModPlug::AudioSourceModPlug(QUrl url) + : AudioSource(url), + m_pModFile(NULL), + m_fileLength(0), + m_seekPos(0) { } AudioSourceModPlug::~AudioSourceModPlug() { close(); } -AudioSourcePointer AudioSourceModPlug::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceModPlug); - if (OK == pAudioSource->open(fileName)) { +AudioSourcePointer AudioSourceModPlug::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceModPlug(url)); + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -57,7 +58,9 @@ AudioSourcePointer AudioSourceModPlug::create(QString fileName) { } } -Result AudioSourceModPlug::open(QString fileName) { +Result AudioSourceModPlug::open() { + const QString fileName(getUrl().toLocalFile()); + ScopedTimer t("AudioSourceModPlug::open()"); qDebug() << "[ModPlug] Loading ModPlug module " << fileName; diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index 04dd22c20c6..e310f2f046e 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -24,7 +24,7 @@ class AudioSourceModPlug: public AudioSource { static void configure(unsigned int bufferSizeLimit, const ModPlug::ModPlug_Settings &settings); - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceModPlug(); @@ -36,9 +36,9 @@ class AudioSourceModPlug: public AudioSource { private: static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) - AudioSourceModPlug(); + explicit AudioSourceModPlug(QUrl url); - Result open(QString fileName); + Result open(); void close(); diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index b327c98009a..351cc60eecd 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -51,8 +51,9 @@ int decodeFrameHeader( } // anonymous namespace -AudioSourceMp3::AudioSourceMp3(QString fileName) - : m_file(fileName), +AudioSourceMp3::AudioSourceMp3(QUrl url) + : AudioSource(url), + m_file(getUrl().toLocalFile()), m_fileSize(0), m_pFileData(NULL), m_avgSeekFrameCount(0), @@ -69,8 +70,8 @@ AudioSourceMp3::~AudioSourceMp3() { close(); } -AudioSourcePointer AudioSourceMp3::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceMp3(fileName)); +AudioSourcePointer AudioSourceMp3::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceMp3(url)); if (OK == pAudioSource->open()) { // success return pAudioSource; diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index f8590982d22..d3658a28a3e 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -20,7 +20,7 @@ namespace Mixxx { class AudioSourceMp3: public AudioSource { public: - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceMp3(); @@ -32,7 +32,7 @@ class AudioSourceMp3: public AudioSource { sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; private: - explicit AudioSourceMp3(QString fileName); + explicit AudioSourceMp3(QUrl url); Result open(); diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 7fe075e10a6..15858128c0b 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -2,7 +2,8 @@ namespace Mixxx { -AudioSourceOggVorbis::AudioSourceOggVorbis() { +AudioSourceOggVorbis::AudioSourceOggVorbis(QUrl url) + : AudioSource(url) { memset(&m_vf, 0, sizeof(m_vf)); } @@ -10,9 +11,9 @@ AudioSourceOggVorbis::~AudioSourceOggVorbis() { close(); } -AudioSourcePointer AudioSourceOggVorbis::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceOggVorbis); - if (OK == pAudioSource->open(fileName)) { +AudioSourcePointer AudioSourceOggVorbis::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceOggVorbis(url)); + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -21,7 +22,8 @@ AudioSourcePointer AudioSourceOggVorbis::create(QString fileName) { } } -Result AudioSourceOggVorbis::open(QString fileName) { +Result AudioSourceOggVorbis::open() { + const QString fileName(getUrl().toLocalFile()); const QByteArray qbaFilename(fileName.toLocal8Bit()); if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { qWarning() << "Failed to open OggVorbis file:" << fileName; diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index 394d7c6ef0a..e41f99796c3 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -11,7 +11,7 @@ namespace Mixxx { class AudioSourceOggVorbis: public AudioSource { public: - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceOggVorbis(); @@ -23,9 +23,9 @@ class AudioSourceOggVorbis: public AudioSource { sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; private: - AudioSourceOggVorbis(); + explicit AudioSourceOggVorbis(QUrl url); - Result open(QString fileName); + Result open(); void close(); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 87a9fec8347..868ee151524 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -2,17 +2,18 @@ namespace Mixxx { -AudioSourceOpus::AudioSourceOpus() : - m_pOggOpusFile(NULL) { +AudioSourceOpus::AudioSourceOpus(QUrl url) + : AudioSource(url), + m_pOggOpusFile(NULL) { } AudioSourceOpus::~AudioSourceOpus() { close(); } -AudioSourcePointer AudioSourceOpus::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceOpus); - if (OK == pAudioSource->open(fileName)) { +AudioSourcePointer AudioSourceOpus::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceOpus(url)); + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -21,9 +22,10 @@ AudioSourcePointer AudioSourceOpus::create(QString fileName) { } } -Result AudioSourceOpus::open(QString fileName) { - int errorCode = 0; +Result AudioSourceOpus::open() { + const QString fileName(getUrl().toLocalFile()); const QByteArray qbaFilename(fileName.toLocal8Bit()); + int errorCode = 0; m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); if (!m_pOggOpusFile) { qDebug() << "Failed to open OggOpus file:" << fileName << "errorCode" diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index d52de9ff4de..f301c94a60c 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -14,7 +14,7 @@ class AudioSourceOpus: public AudioSource { // All Opus audio is encoded at 48 kHz static const size_type kFrameRate = 48000; - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceOpus(); @@ -26,9 +26,9 @@ class AudioSourceOpus: public AudioSource { sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; private: - AudioSourceOpus(); + explicit AudioSourceOpus(QUrl url); - Result open(QString fileName); + Result open(); void close(); diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index aaf34dd7524..c6138c7a963 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -2,8 +2,9 @@ namespace Mixxx { -AudioSourceSndFile::AudioSourceSndFile() : - m_pSndFile(NULL) { +AudioSourceSndFile::AudioSourceSndFile(QUrl url) + : AudioSource(url), + m_pSndFile(NULL) { memset(&m_sfInfo, 0, sizeof(m_sfInfo)); } @@ -11,9 +12,9 @@ AudioSourceSndFile::~AudioSourceSndFile() { close(); } -AudioSourcePointer AudioSourceSndFile::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceSndFile); - if (OK == pAudioSource->open(fileName)) { +AudioSourcePointer AudioSourceSndFile::create(QUrl url) { + QSharedPointer pAudioSource(new AudioSourceSndFile(url)); + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -22,7 +23,8 @@ AudioSourcePointer AudioSourceSndFile::create(QString fileName) { } } -Result AudioSourceSndFile::open(QString fileName) { +Result AudioSourceSndFile::open() { + const QString fileName(getUrl().toLocalFile()); #ifdef __WINDOWS__ // Pointer valid until string changed LPCWSTR lpcwFilename = (LPCWSTR)fileName.utf16(); diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 0f34c25f5ec..2f822dc1663 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -16,7 +16,7 @@ namespace Mixxx { class AudioSourceSndFile: public AudioSource { public: - static AudioSourcePointer create(QString fileName); + static AudioSourcePointer create(QUrl url); ~AudioSourceSndFile(); @@ -26,9 +26,9 @@ class AudioSourceSndFile: public AudioSource { sample_type* sampleBuffer) /*override*/; private: - AudioSourceSndFile(); + explicit AudioSourceSndFile(QUrl url); - Result open(QString fileName); + Result open(); void close(); diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index c6a329452c0..fbb74512835 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -117,5 +117,5 @@ QImage SoundSourceCoreAudio::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceCoreAudio::open() const { - return Mixxx::AudioSourceCoreAudio::create(getFilename()); + return Mixxx::AudioSourceCoreAudio::create(QUrl::fromLocalFile(getFilename())); } diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index a5ed7c930df..47bbbb76702 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -189,5 +189,5 @@ QImage SoundSourceFFmpeg::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceFFmpeg::open() const { - return Mixxx::AudioSourceFFmpeg::create(getFilename()); + return Mixxx::AudioSourceFFmpeg::create(QUrl::fromLocalFile(getFilename())); } diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index c8e3322757a..052bb267435 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -86,5 +86,5 @@ QImage SoundSourceFLAC::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceFLAC::open() const { - return Mixxx::AudioSourceFLAC::create(getFilename()); + return Mixxx::AudioSourceFLAC::create(QUrl::fromLocalFile(getFilename())); } diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index 4fd1444f843..2a24cae8c49 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -77,5 +77,5 @@ QImage SoundSourceModPlug::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceModPlug::open() const { - return Mixxx::AudioSourceModPlug::create(getFilename()); + return Mixxx::AudioSourceModPlug::create(QUrl::fromLocalFile(getFilename())); } diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 5bf286664b5..87fbbe075c2 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -77,5 +77,5 @@ QImage SoundSourceMp3::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceMp3::open() const { - return Mixxx::AudioSourceMp3::create(getFilename()); + return Mixxx::AudioSourceMp3::create(QUrl::fromLocalFile(getFilename())); } diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index 1abefcdf2a5..30ea034ea4f 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -69,5 +69,5 @@ QImage SoundSourceOggVorbis::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceOggVorbis::open() const { - return Mixxx::AudioSourceOggVorbis::create(getFilename()); + return Mixxx::AudioSourceOggVorbis::create(QUrl::fromLocalFile(getFilename())); } diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index d425dd36717..5f07408275d 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -131,5 +131,5 @@ QImage SoundSourceOpus::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceOpus::open() const { - return Mixxx::AudioSourceOpus::create(getFilename()); + return Mixxx::AudioSourceOpus::create(QUrl::fromLocalFile(getFilename())); } diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index d4a5f999cf1..36d68b1f4c6 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -132,5 +132,5 @@ QImage SoundSourceSndFile::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceSndFile::open() const { - return Mixxx::AudioSourceSndFile::create(getFilename()); + return Mixxx::AudioSourceSndFile::create(QUrl::fromLocalFile(getFilename())); } From 9eed118ceaceef621fadee2964f81a30d5c42d3c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 19 Jan 2015 12:55:24 +0100 Subject: [PATCH 110/481] Create SoundSource from URL --- plugins/soundsourcem4a/soundsourcem4a.cpp | 10 ++-- plugins/soundsourcem4a/soundsourcem4a.h | 2 +- .../soundsourcemediafoundation.cpp | 10 ++-- .../soundsourcemediafoundation.h | 2 +- plugins/soundsourcewv/soundsourcewv.cpp | 10 ++-- plugins/soundsourcewv/soundsourcewv.h | 2 +- src/soundsourceproxy.cpp | 53 ++++++++++--------- src/sources/soundsource.cpp | 18 ++++--- src/sources/soundsource.h | 21 +++++--- src/sources/soundsourcecoreaudio.cpp | 14 ++--- src/sources/soundsourcecoreaudio.h | 2 +- src/sources/soundsourceffmpeg.cpp | 8 +-- src/sources/soundsourceffmpeg.h | 2 +- src/sources/soundsourceflac.cpp | 10 ++-- src/sources/soundsourceflac.h | 2 +- src/sources/soundsourcemodplug.cpp | 28 +++++----- src/sources/soundsourcemodplug.h | 4 +- src/sources/soundsourcemp3.cpp | 10 ++-- src/sources/soundsourcemp3.h | 2 +- src/sources/soundsourceoggvorbis.cpp | 10 ++-- src/sources/soundsourceoggvorbis.h | 2 +- src/sources/soundsourceopus.cpp | 10 ++-- src/sources/soundsourceopus.h | 2 +- src/sources/soundsourceplugin.h | 8 +-- src/sources/soundsourcesndfile.cpp | 18 +++---- src/sources/soundsourcesndfile.h | 2 +- 26 files changed, 138 insertions(+), 124 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 03fe4419ddc..de5fcb8d69f 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -30,12 +30,12 @@ QList SoundSourceM4A::supportedFileExtensions() { return list; } -SoundSourceM4A::SoundSourceM4A(QString fileName) : - SoundSourcePlugin(fileName, "m4a") { +SoundSourceM4A::SoundSourceM4A(QUrl url) + : SoundSourcePlugin(url, "m4a") { } Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); + TagLib::MP4::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -58,7 +58,7 @@ Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } QImage SoundSourceM4A::parseCoverArt() const { - TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); + TagLib::MP4::File f(getLocalFilePath().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { return readMP4TagCover(*mp4); @@ -68,7 +68,7 @@ QImage SoundSourceM4A::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceM4A::open() const { - return Mixxx::AudioSourceM4A::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceM4A::create(getUrl()); } } // namespace Mixxx diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index bf7af6e2f9f..61c34b9a7b8 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -33,7 +33,7 @@ class SoundSourceM4A: public SoundSourcePlugin { public: static QList supportedFileExtensions(); - explicit SoundSourceM4A(QString fileName); + explicit SoundSourceM4A(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 957c434d528..78c59b2969b 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -37,13 +37,13 @@ QList SoundSourceMediaFoundation::supportedFileExtensions() { return list; } -SoundSourceMediaFoundation::SoundSourceMediaFoundation(QString fileName) - : SoundSourcePlugin(fileName, "m4a") { +SoundSourceMediaFoundation::SoundSourceMediaFoundation(QUrl url) + : SoundSourcePlugin(url, "m4a") { } Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { // Must be toLocal8Bit since Windows fopen does not do UTF-8 - TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); + TagLib::MP4::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -66,7 +66,7 @@ Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata } QImage SoundSourceMediaFoundation::parseCoverArt() const { - TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); + TagLib::MP4::File f(getLocalFilePath().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { return Mixxx::readMP4TagCover(*mp4); @@ -76,7 +76,7 @@ QImage SoundSourceMediaFoundation::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceMediaFoundation::open() const { - return Mixxx::AudioSourceMediaFoundation::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceMediaFoundation::create(getUrl()); } extern "C" MY_EXPORT const char* getMixxxVersion() { diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 301fac4d924..5f17bca4658 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -33,7 +33,7 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { public: static QList supportedFileExtensions(); - explicit SoundSourceMediaFoundation(QString fileName); + explicit SoundSourceMediaFoundation(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index ecb925f2fa6..558b13d6e68 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -13,12 +13,12 @@ QList SoundSourceWV::supportedFileExtensions() { return list; } -SoundSourceWV::SoundSourceWV(QString fileName) : - SoundSourcePlugin(fileName, "wv") { +SoundSourceWV::SoundSourceWV(QUrl url) + : SoundSourcePlugin(url, "wv") { } Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::WavPack::File f(getFilename().toLocal8Bit().constData()); + TagLib::WavPack::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -41,7 +41,7 @@ Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } QImage SoundSourceWV::parseCoverArt() const { - TagLib::WavPack::File f(getFilename().toLocal8Bit().constData()); + TagLib::WavPack::File f(getLocalFilePath().constData()); TagLib::APE::Tag *ape = f.APETag(); if (ape) { return Mixxx::readAPETagCover(*ape); @@ -51,7 +51,7 @@ QImage SoundSourceWV::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceWV::open() const { - return Mixxx::AudioSourceWV::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceWV::create(getUrl()); } } // namespace Mixxx diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index f2d5ffbc048..8ebe9fb822d 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -16,7 +16,7 @@ class SoundSourceWV: public SoundSourcePlugin { public: static QList supportedFileExtensions(); - explicit SoundSourceWV(QString fileName); + explicit SoundSourceWV(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index bf96ef10bfd..be3c8addbf2 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -164,51 +164,52 @@ void SoundSourceProxy::loadPlugins() { // static Mixxx::SoundSourcePointer SoundSourceProxy::initialize(const QString& qFilename) { - QString extension(qFilename); - extension = extension.remove(0, (qFilename.lastIndexOf(".") + 1)).toLower().trimmed(); - if (extension.isEmpty()) { - qWarning() << "Missing file extension:" << qFilename; + const QUrl url(QUrl::fromLocalFile(qFilename)); + + const QString type(Mixxx::SoundSource::getTypeFromUrl(url)); + if (type.isEmpty()) { + qWarning() << "Unknown file type:" << qFilename; return Mixxx::SoundSourcePointer(); } #ifdef __FFMPEGFILE__ - return Mixxx::SoundSourcePointer(new SoundSourceFFmpeg(qFilename)); + return Mixxx::SoundSourcePointer(new SoundSourceFFmpeg(url)); #endif - if (SoundSourceOggVorbis::supportedFileExtensions().contains(extension)) { - return Mixxx::SoundSourcePointer(new SoundSourceOggVorbis(qFilename)); + if (SoundSourceOggVorbis::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new SoundSourceOggVorbis(url)); #ifdef __OPUS__ - } else if (SoundSourceOpus::supportedFileExtensions().contains(extension)) { - return Mixxx::SoundSourcePointer(new SoundSourceOpus(qFilename)); + } else if (SoundSourceOpus::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new SoundSourceOpus(url)); #endif #ifdef __MAD__ - } else if (SoundSourceMp3::supportedFileExtensions().contains(extension)) { - return Mixxx::SoundSourcePointer(new SoundSourceMp3(qFilename)); + } else if (SoundSourceMp3::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new SoundSourceMp3(url)); #endif - } else if (SoundSourceFLAC::supportedFileExtensions().contains(extension)) { - return Mixxx::SoundSourcePointer(new SoundSourceFLAC(qFilename)); + } else if (SoundSourceFLAC::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new SoundSourceFLAC(url)); #ifdef __COREAUDIO__ - } else if (SoundSourceCoreAudio::supportedFileExtensions().contains(extension)) { - return Mixxx::SoundSourcePointer(new SoundSourceCoreAudio(qFilename)); + } else if (SoundSourceCoreAudio::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new SoundSourceCoreAudio(url)); #endif #ifdef __MODPLUG__ - } else if (SoundSourceModPlug::supportedFileExtensions().contains(extension)) { - return Mixxx::SoundSourcePointer(new SoundSourceModPlug(qFilename)); + } else if (SoundSourceModPlug::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new SoundSourceModPlug(url)); #endif - } else if (m_extensionsSupportedByPlugins.contains(extension)) { - getSoundSourceFunc getter = m_extensionsSupportedByPlugins.value(extension); + } else if (m_extensionsSupportedByPlugins.contains(type)) { + getSoundSourceFunc getter = m_extensionsSupportedByPlugins.value(type); if (getter) { - qDebug() << "Getting SoundSource plugin object for" << extension; - return Mixxx::SoundSourcePointer(getter(qFilename)); + qDebug() << "Getting SoundSource plugin object for" << type; + return Mixxx::SoundSourcePointer(getter(url.toLocalFile())); } else { qDebug() << "Failed to resolve getSoundSource in plugin for" << - extension; + type; return Mixxx::SoundSourcePointer(); //Failed to load plugin } #ifdef __SNDFILE__ - } else if (SoundSourceSndFile::supportedFileExtensions().contains(extension)) { - return Mixxx::SoundSourcePointer(new SoundSourceSndFile(qFilename)); + } else if (SoundSourceSndFile::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new SoundSourceSndFile(url)); #endif } else { //Unsupported filetype return Mixxx::SoundSourcePointer(); @@ -315,13 +316,13 @@ Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource() const { } if (!pAudioSource->isValid()) { - qWarning() << "Invalid file:" << m_pSoundSource->getFilename() + qWarning() << "Invalid file:" << m_pSoundSource->getUrl() << "channels" << pAudioSource->getChannelCount() << "frame rate" << pAudioSource->getChannelCount(); return Mixxx::AudioSourcePointer(); } if (pAudioSource->isEmpty()) { - qWarning() << "Empty file:" << m_pSoundSource->getFilename(); + qWarning() << "Empty file:" << m_pSoundSource->getUrl(); return Mixxx::AudioSourcePointer(); } diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index dd2100d182f..a81f7c1172c 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -20,14 +20,20 @@ namespace Mixxx { -SoundSource::SoundSource(QString sFilename) : - m_sFilename(sFilename), - m_sType(m_sFilename.section(".", -1).toLower()) { +/*static*/ QString SoundSource::getTypeFromUrl(QUrl url) { + return url.toString().section(".", -1).toLower().trimmed(); } -SoundSource::SoundSource(QString sFilename, QString sType) : - m_sFilename(sFilename), - m_sType(sType) { +SoundSource::SoundSource(QUrl url) + : m_url(url), + m_type(getTypeFromUrl(url)) { + DEBUG_ASSERT(getUrl().isValid()); +} + +SoundSource::SoundSource(QUrl url, QString type) + : m_url(url), + m_type(type) { + DEBUG_ASSERT(getUrl().isValid()); } SoundSource::~SoundSource() { diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index 00fd863c746..a5a41b209f7 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -55,11 +55,13 @@ class SoundSource { public: virtual ~SoundSource(); - inline const QString& getFilename() const { - return m_sFilename; + static QString getTypeFromUrl(QUrl url); + + inline const QUrl& getUrl() const { + return m_url; } inline const QString& getType() const { - return m_sType; + return m_type; } // Parses metadata before opening the SoundSource for reading. @@ -77,12 +79,17 @@ class SoundSource { virtual AudioSourcePointer open() const = 0; protected: - explicit SoundSource(QString sFilename); - SoundSource(QString sFilename, QString sType); + explicit SoundSource(QUrl url); + SoundSource(QUrl url, QString type); + + inline QByteArray getLocalFilePath() const { + DEBUG_ASSERT(getUrl().isLocalFile()); + return getUrl().toLocalFile().toLocal8Bit(); + } private: - const QString m_sFilename; - const QString m_sType; + const QUrl m_url; + const QString m_type; }; typedef QSharedPointer SoundSourcePointer; diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index fbb74512835..440ed65236c 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -36,13 +36,13 @@ QList SoundSourceCoreAudio::supportedFileExtensions() { return list; } -SoundSourceCoreAudio::SoundSourceCoreAudio(QString fileName) - : SoundSource(fileName) { +SoundSourceCoreAudio::SoundSourceCoreAudio(QUrl url) + : SoundSource(url) { } Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { if (getType() == "m4a") { - TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); + TagLib::MP4::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -59,7 +59,7 @@ Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) cons } } } else if (getType() == "mp3") { - TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); + TagLib::MPEG::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -92,7 +92,7 @@ Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) cons QImage SoundSourceCoreAudio::parseCoverArt() const { QImage coverArt; if (getType() == "m4a") { - TagLib::MP4::File f(getFilename().toLocal8Bit().constData()); + TagLib::MP4::File f(getLocalFilePath().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { return Mixxx::readMP4TagCover(*mp4); @@ -100,7 +100,7 @@ QImage SoundSourceCoreAudio::parseCoverArt() const { return QImage(); } } else if (getType() == "mp3") { - TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); + TagLib::MPEG::File f(getLocalFilePath().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); @@ -117,5 +117,5 @@ QImage SoundSourceCoreAudio::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceCoreAudio::open() const { - return Mixxx::AudioSourceCoreAudio::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceCoreAudio::create(getUrl()); } diff --git a/src/sources/soundsourcecoreaudio.h b/src/sources/soundsourcecoreaudio.h index 4850cdd0197..2cebcbc2976 100644 --- a/src/sources/soundsourcecoreaudio.h +++ b/src/sources/soundsourcecoreaudio.h @@ -25,7 +25,7 @@ class SoundSourceCoreAudio : public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - explicit SoundSourceCoreAudio(QString fileName); + explicit SoundSourceCoreAudio(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index 47bbbb76702..4ad62d88170 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -60,8 +60,8 @@ QList SoundSourceFFmpeg::supportedFileExtensions() { return list; } -SoundSourceFFmpeg::SoundSourceFFmpeg(QString fileName) - : Mixxx::SoundSource(fileName) { +SoundSourceFFmpeg::SoundSourceFFmpeg(QUrl url) + : SoundSource(url) { } Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { @@ -73,7 +73,7 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { unsigned int i; AVDictionary *l_iFormatOpts = NULL; - if (avformat_open_input(&FmtCtx, getFilename().toLocal8Bit().constData(), NULL, + if (avformat_open_input(&FmtCtx, getLocalFilePath().constData(), NULL, &l_iFormatOpts) !=0) { qDebug() << "av_open_input_file: cannot open" << getFilename(); return ERR; @@ -189,5 +189,5 @@ QImage SoundSourceFFmpeg::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceFFmpeg::open() const { - return Mixxx::AudioSourceFFmpeg::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceFFmpeg::create(getUrl()); } diff --git a/src/sources/soundsourceffmpeg.h b/src/sources/soundsourceffmpeg.h index 2056323f62f..60ea3431cb3 100644 --- a/src/sources/soundsourceffmpeg.h +++ b/src/sources/soundsourceffmpeg.h @@ -24,7 +24,7 @@ class SoundSourceFFmpeg : public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - explicit SoundSourceFFmpeg(QString qFilename); + explicit SoundSourceFFmpeg(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 052bb267435..07d4387835d 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -28,12 +28,12 @@ QList SoundSourceFLAC::supportedFileExtensions() { return list; } -SoundSourceFLAC::SoundSourceFLAC(QString fileName) : - SoundSource(fileName, "flac") { +SoundSourceFLAC::SoundSourceFLAC(QUrl url) + : SoundSource(url, "flac") { } Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); + TagLib::FLAC::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -61,7 +61,7 @@ Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } QImage SoundSourceFLAC::parseCoverArt() const { - TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); + TagLib::FLAC::File f(getLocalFilePath().constData()); QImage coverArt; TagLib::Ogg::XiphComment *xiph(f.xiphComment()); if (xiph) { @@ -86,5 +86,5 @@ QImage SoundSourceFLAC::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceFLAC::open() const { - return Mixxx::AudioSourceFLAC::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceFLAC::create(getUrl()); } diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index 45e1f6ba510..01c5d02c03f 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -24,7 +24,7 @@ class SoundSourceFLAC: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - explicit SoundSourceFLAC(QString fileName); + explicit SoundSourceFLAC(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index 2a24cae8c49..421a4c98d9e 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -9,28 +9,26 @@ #include -namespace { -QString getTypeFromFilename(QString fileName) { - const QString fileExt(fileName.section(".", -1).toLower()); - if (fileExt == "mod") { +QString SoundSourceModPlug::getTypeFromUrl(QUrl url) { + const QString type(SoundSource::getTypeFromUrl(url)); + if (type == "mod") { return "Protracker"; - } else if (fileExt == "med") { + } else if (type == "med") { return "OctaMed"; - } else if (fileExt == "okt") { + } else if (type == "okt") { return "Oktalyzer"; - } else if (fileExt == "s3m") { + } else if (type == "s3m") { return "Scream Tracker 3"; - } else if (fileExt == "stm") { + } else if (type == "stm") { return "Scream Tracker"; - } else if (fileExt == "xm") { + } else if (type == "xm") { return "FastTracker2"; - } else if (fileExt == "it") { + } else if (type == "it") { return "Impulse Tracker"; } else { return "Module"; } } -} QList SoundSourceModPlug::supportedFileExtensions() { QList list; @@ -46,13 +44,13 @@ QList SoundSourceModPlug::supportedFileExtensions() { return list; } -SoundSourceModPlug::SoundSourceModPlug(QString fileName) : - SoundSource(fileName, getTypeFromFilename(fileName)) { +SoundSourceModPlug::SoundSourceModPlug(QUrl url) : + SoundSource(url, getTypeFromUrl(url)) { } Result SoundSourceModPlug::parseMetadata( Mixxx::TrackMetadata* pMetadata) const { - QFile modFile(getFilename()); + QFile modFile(getLocalFilePath()); modFile.open(QIODevice::ReadOnly); const QByteArray fileBuf(modFile.readAll()); modFile.close(); @@ -77,5 +75,5 @@ QImage SoundSourceModPlug::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceModPlug::open() const { - return Mixxx::AudioSourceModPlug::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceModPlug::create(getUrl()); } diff --git a/src/sources/soundsourcemodplug.h b/src/sources/soundsourcemodplug.h index 0219147d08c..102d78d07a1 100644 --- a/src/sources/soundsourcemodplug.h +++ b/src/sources/soundsourcemodplug.h @@ -10,7 +10,9 @@ class SoundSourceModPlug: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - explicit SoundSourceModPlug(QString fileName); + static QString getTypeFromUrl(QUrl url); + + explicit SoundSourceModPlug(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 87fbbe075c2..f932d99c64a 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -27,12 +27,12 @@ QList SoundSourceMp3::supportedFileExtensions() { return list; } -SoundSourceMp3::SoundSourceMp3(QString qFilename) : - SoundSource(qFilename, "mp3") { +SoundSourceMp3::SoundSourceMp3(QUrl url) + : SoundSource(url, "mp3") { } Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); + TagLib::MPEG::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -62,7 +62,7 @@ Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { QImage SoundSourceMp3::parseCoverArt() const { QImage coverArt; - TagLib::MPEG::File f(getFilename().toLocal8Bit().constData()); + TagLib::MPEG::File f(getLocalFilePath().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); @@ -77,5 +77,5 @@ QImage SoundSourceMp3::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceMp3::open() const { - return Mixxx::AudioSourceMp3::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceMp3::create(getUrl()); } diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h index b45aeb7828d..47056bf20fb 100644 --- a/src/sources/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -28,7 +28,7 @@ class SoundSourceMp3: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - explicit SoundSourceMp3(QString qFilename); + explicit SoundSourceMp3(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index 30ea034ea4f..c1cd08dd28b 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -27,8 +27,8 @@ QList SoundSourceOggVorbis::supportedFileExtensions() { return list; } -SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) : - SoundSource(qFilename, "ogg") { +SoundSourceOggVorbis::SoundSourceOggVorbis(QUrl url) : + SoundSource(url, "ogg") { } /* @@ -36,7 +36,7 @@ SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) : */ Result SoundSourceOggVorbis::parseMetadata( Mixxx::TrackMetadata* pMetadata) const { - TagLib::Ogg::Vorbis::File f(getFilename().toLocal8Bit().constData()); + TagLib::Ogg::Vorbis::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -59,7 +59,7 @@ Result SoundSourceOggVorbis::parseMetadata( } QImage SoundSourceOggVorbis::parseCoverArt() const { - TagLib::Ogg::Vorbis::File f(getFilename().toLocal8Bit().constData()); + TagLib::Ogg::Vorbis::File f(getLocalFilePath().constData()); TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { return Mixxx::readXiphCommentCover(*xiph); @@ -69,5 +69,5 @@ QImage SoundSourceOggVorbis::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceOggVorbis::open() const { - return Mixxx::AudioSourceOggVorbis::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceOggVorbis::create(getUrl()); } diff --git a/src/sources/soundsourceoggvorbis.h b/src/sources/soundsourceoggvorbis.h index 5a60a89a8d0..3eda6b960e2 100644 --- a/src/sources/soundsourceoggvorbis.h +++ b/src/sources/soundsourceoggvorbis.h @@ -23,7 +23,7 @@ class SoundSourceOggVorbis: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - explicit SoundSourceOggVorbis(QString qFilename); + explicit SoundSourceOggVorbis(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 5f07408275d..869913a761c 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -14,8 +14,8 @@ QList SoundSourceOpus::supportedFileExtensions() { return list; } -SoundSourceOpus::SoundSourceOpus(QString qFilename) : - SoundSource(qFilename, "opus") { +SoundSourceOpus::SoundSourceOpus(QUrl url) : + SoundSource(url, "opus") { } namespace { @@ -40,7 +40,7 @@ class OggOpusFileOwner { Parse the the file to get metadata */ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - const QByteArray qbaFilename(getFilename().toLocal8Bit()); + const QByteArray qbaFilename(getLocalFilePath()); int error = 0; OggOpusFileOwner l_ptrOpusFile( @@ -121,7 +121,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { QImage SoundSourceOpus::parseCoverArt() const { #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) - TagLib::Ogg::Opus::File f(getFilename().toLocal8Bit().constData()); + TagLib::Ogg::Opus::File f(getLocalFilePath().constData()); TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { return Mixxx::readXiphCommentCover(*xiph); @@ -131,5 +131,5 @@ QImage SoundSourceOpus::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceOpus::open() const { - return Mixxx::AudioSourceOpus::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceOpus::create(getUrl()); } diff --git a/src/sources/soundsourceopus.h b/src/sources/soundsourceopus.h index 150eeb8027b..5be30ea8f33 100644 --- a/src/sources/soundsourceopus.h +++ b/src/sources/soundsourceopus.h @@ -7,7 +7,7 @@ class SoundSourceOpus: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - explicit SoundSourceOpus(QString qFilename); + explicit SoundSourceOpus(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; diff --git a/src/sources/soundsourceplugin.h b/src/sources/soundsourceplugin.h index 14d51ba177a..8dbc66c4ce9 100644 --- a/src/sources/soundsourceplugin.h +++ b/src/sources/soundsourceplugin.h @@ -13,11 +13,11 @@ class SoundSourcePlugin: public SoundSource { static void freeFileExtensions(char** fileExtensions); protected: - inline explicit SoundSourcePlugin(QString sFilename) : - SoundSource(sFilename) { + inline explicit SoundSourcePlugin(QUrl url) + : SoundSource(url) { } - inline SoundSourcePlugin(QString sFilename, QString sType) : - SoundSource(sFilename, sType) { + inline SoundSourcePlugin(QUrl url, QString type) + : SoundSource(url, type) { } }; diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 36d68b1f4c6..56226ab9f66 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -17,14 +17,14 @@ QList SoundSourceSndFile::supportedFileExtensions() { return list; } -SoundSourceSndFile::SoundSourceSndFile(QString qFilename) : - SoundSource(qFilename) { +SoundSourceSndFile::SoundSourceSndFile(QUrl url) + : SoundSource(url) { } Result SoundSourceSndFile::parseMetadata( Mixxx::TrackMetadata* pMetadata) const { if (getType() == "flac") { - TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); + TagLib::FLAC::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -46,7 +46,7 @@ Result SoundSourceSndFile::parseMetadata( } } } else if (getType() == "wav") { - TagLib::RIFF::WAV::File f(getFilename().toLocal8Bit().constData()); + TagLib::RIFF::WAV::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -72,7 +72,7 @@ Result SoundSourceSndFile::parseMetadata( #endif } else if (getType().startsWith("aif")) { // Try AIFF - TagLib::RIFF::AIFF::File f(getFilename().toLocal8Bit().constData()); + TagLib::RIFF::AIFF::File f(getLocalFilePath().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -93,7 +93,7 @@ QImage SoundSourceSndFile::parseCoverArt() const { QImage coverArt; if (getType() == "flac") { - TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); + TagLib::FLAC::File f(getLocalFilePath().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); @@ -114,14 +114,14 @@ QImage SoundSourceSndFile::parseCoverArt() const { } } } else if (getType() == "wav") { - TagLib::RIFF::WAV::File f(getFilename().toLocal8Bit().constData()); + TagLib::RIFF::WAV::File f(getLocalFilePath().constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); } } else if (getType().startsWith("aif")) { // Try AIFF - TagLib::RIFF::AIFF::File f(getFilename().toLocal8Bit().constData()); + TagLib::RIFF::AIFF::File f(getLocalFilePath().constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); @@ -132,5 +132,5 @@ QImage SoundSourceSndFile::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceSndFile::open() const { - return Mixxx::AudioSourceSndFile::create(QUrl::fromLocalFile(getFilename())); + return Mixxx::AudioSourceSndFile::create(getUrl()); } diff --git a/src/sources/soundsourcesndfile.h b/src/sources/soundsourcesndfile.h index e027bc896a5..c05ebc4075c 100644 --- a/src/sources/soundsourcesndfile.h +++ b/src/sources/soundsourcesndfile.h @@ -7,7 +7,7 @@ class SoundSourceSndFile: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - explicit SoundSourceSndFile(QString qFilename); + explicit SoundSourceSndFile(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; From fa8446423e04d4f4764d4643c03a3fae88cacfae Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 19 Jan 2015 22:17:56 +0100 Subject: [PATCH 111/481] MP3: Suppress "lost sync" warnings --- src/sources/audiosourcemp3.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 351cc60eecd..fc6b4e2fec2 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -111,8 +111,11 @@ Result AudioSourceMp3::open() { do { if (0 != decodeFrameHeader(&madHeader, &m_madStream, true)) { if (MAD_RECOVERABLE(m_madStream.error)) { - qWarning() << "Recoverable MP3 header decoding error:" - << mad_stream_errorstr(&m_madStream); + // Suppress "lost synchronization" warnings + if (MAD_ERROR_LOSTSYNC != m_madStream.error) { + qWarning() << "Recoverable MP3 header decoding error:" + << mad_stream_errorstr(&m_madStream); + } continue; } else { if (MAD_ERROR_BUFLEN != m_madStream.error) { @@ -449,7 +452,8 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { if ((pMadThisFrame != m_madStream.this_frame) && - (NULL != pSampleBuffer)) { + // Suppress "lost synchronization" warnings + (MAD_ERROR_LOSTSYNC != m_madStream.error)) { qWarning() << "Recoverable MP3 frame decoding error:" << mad_stream_errorstr(&m_madStream); } @@ -481,7 +485,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( } const size_type synthReadCount = math_min( - m_madSynthCount, numberOfFramesRemaining); + m_madSynthCount, numberOfFramesRemaining); if (NULL != pSampleBuffer) { DEBUG_ASSERT(m_madSynthCount <= m_madSynth.pcm.length); const size_type madSynthOffset = From 82a9f8370134ee52e128bc83c0cd49508371fa5c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 20 Jan 2015 17:34:33 +0100 Subject: [PATCH 112/481] MP3: Log "lost synchronization" warnings for debugging purposes --- src/sources/audiosourcemp3.cpp | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index fc6b4e2fec2..548c89b66c1 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -111,8 +111,13 @@ Result AudioSourceMp3::open() { do { if (0 != decodeFrameHeader(&madHeader, &m_madStream, true)) { if (MAD_RECOVERABLE(m_madStream.error)) { - // Suppress "lost synchronization" warnings - if (MAD_ERROR_LOSTSYNC != m_madStream.error) { + if (MAD_ERROR_LOSTSYNC == m_madStream.error) { + // When seeking through the file "lost synchronization" + // warnings are expected. Just log them for debugging + // purposes. + qDebug() << "Recoverable MP3 header decoding error:" + << mad_stream_errorstr(&m_madStream); + } else { qWarning() << "Recoverable MP3 header decoding error:" << mad_stream_errorstr(&m_madStream); } @@ -451,11 +456,18 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( unsigned char const* pMadThisFrame = m_madStream.this_frame; if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { - if ((pMadThisFrame != m_madStream.this_frame) && - // Suppress "lost synchronization" warnings - (MAD_ERROR_LOSTSYNC != m_madStream.error)) { - qWarning() << "Recoverable MP3 frame decoding error:" + if (pMadThisFrame != m_madStream.this_frame) { + // Suppress "lost synchronization" warnings + if (MAD_ERROR_LOSTSYNC == m_madStream.error) { + // When seeking through the file "lost synchronization" + // warnings are expected. Just log them for debugging + // purposes. + qDebug() << "Recoverable MP3 frame decoding error:" << mad_stream_errorstr(&m_madStream); + } else { + qWarning() << "Recoverable MP3 frame decoding error:" + << mad_stream_errorstr(&m_madStream); + } } // Acknowledge error... m_madStream.error = MAD_ERROR_NONE; From 6c8c9ad54283ca06d38e93826189d56bc925505b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 21 Jan 2015 13:21:35 +0100 Subject: [PATCH 113/481] Fix naming in AnalyserQueue --- src/analyserqueue.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index de33aea9ee9..b04f96cbd6e 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -20,11 +20,11 @@ #include "util/event.h" #include "util/trace.h" -// Measured in 0.1%, +// Measured in 0.1% // 0 for no progress during finalize // 1 to display the text "finalizing" // 100 for 10% step after finalize -#define FINALIZE_PERCENT 1 +#define FINALIZE_PROMILLE 1 namespace { @@ -203,7 +203,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi //fp div here prevents insane signed overflow DEBUG_ASSERT(progressFrameCount <= pAudioSource->getFrameCount()); int progress = (int)(((float)progressFrameCount) / pAudioSource->getFrameCount() * - (1000 - FINALIZE_PERCENT)); + (1000 - FINALIZE_PROMILLE)); if (m_progressInfo.track_progress != progress) { if (progressUpdateInhibitTimer.elapsed() > 60) { @@ -321,7 +321,7 @@ void AnalyserQueue::run() { emitUpdateProgress(nextTrack, 0); } else { // 100% - FINALIZE_PERCENT finished - emitUpdateProgress(nextTrack, 1000 - FINALIZE_PERCENT); + emitUpdateProgress(nextTrack, 1000 - FINALIZE_PROMILLE); // This takes around 3 sec on a Atom Netbook QListIterator itf(m_aq); while (itf.hasNext()) { @@ -352,7 +352,7 @@ void AnalyserQueue::emitUpdateProgress(TrackPointer tio, int progress) { // The following tries will success if the previous signal was processed in the GUI Thread // This prevent the AnalysisQueue from filling up the GUI Thread event Queue // 100 % is emitted in any case - if (progress < 1000 - FINALIZE_PERCENT && progress > 0) { + if (progress < 1000 - FINALIZE_PROMILLE && progress > 0) { // Signals during processing are not required in any case if (!m_progressInfo.sema.tryAcquire()) { return; From a09755e120e1aa8bb73e1849a090f6bc4b49ccac Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 21 Jan 2015 13:22:41 +0100 Subject: [PATCH 114/481] Improve documenation in AnalyserQueue --- src/analyserqueue.cpp | 65 ++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index b04f96cbd6e..135ff26fd7d 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -26,23 +26,22 @@ // 100 for 10% step after finalize #define FINALIZE_PROMILLE 1 -namespace -{ +namespace { + // Analysis is done in blocks. + // We need to use a smaller block size, because on Linux the AnalyserQueue + // can starve the CPU of its resources, resulting in xruns. A block size + // of 4096 frames per block seems to do fine. const Mixxx::AudioSource::size_type kAnalysisChannels = 2; // stereo - - // We need to use a smaller block size because on Linux, the AnalyserQueue - // can starve the CPU of its resources, resulting in xruns. A block size of - // 4096 samples per channel seems to do fine. - const Mixxx::AudioSource::size_type kAnalysisFrameCount = 4096; - - const Mixxx::AudioSource::size_type kAnalysisSampleCount = kAnalysisFrameCount * kAnalysisChannels; -} + const Mixxx::AudioSource::size_type kAnalysisFramesPerBlock = 4096; + const Mixxx::AudioSource::size_type kAnalysisSamplesPerBlock = + kAnalysisFramesPerBlock * kAnalysisChannels; +} // anonymous namespace AnalyserQueue::AnalyserQueue(TrackCollection* pTrackCollection) : m_aq(), m_exit(false), m_aiCheckPriorities(false), - m_sampleBuffer(kAnalysisSampleCount), + m_sampleBuffer(kAnalysisSamplesPerBlock), m_tioq(), m_qm(), m_qwait(), @@ -167,29 +166,45 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi QTime progressUpdateInhibitTimer; progressUpdateInhibitTimer.start(); // Inhibit Updates for 60 milliseconds - Mixxx::AudioSource::size_type progressFrameCount = 0; + Mixxx::AudioSource::size_type frameIndex = 0; bool dieflag = false; bool cancelled = false; do { ScopedTimer t("AnalyserQueue::doAnalysis block"); - DEBUG_ASSERT(progressFrameCount < pAudioSource->getFrameCount()); - const Mixxx::AudioSource::size_type readFrameCount = pAudioSource->readSampleFramesStereo(kAnalysisFrameCount, &m_sampleBuffer[0], m_sampleBuffer.size()); - progressFrameCount += readFrameCount; - DEBUG_ASSERT(progressFrameCount <= pAudioSource->getFrameCount()); - - // To compare apples to apples, let's only look at blocks that are the - // full block size. - if (kAnalysisFrameCount == readFrameCount) { + DEBUG_ASSERT(frameIndex < pAudioSource->getFrameCount()); + const Mixxx::AudioSource::size_type framesToRead = + math_min(kAnalysisFramesPerBlock, + pAudioSource->getFrameCount() - frameIndex); + DEBUG_ASSERT(0 < framesToRead); + + const Mixxx::AudioSource::size_type framesRead = + pAudioSource->readSampleFramesStereo( + kAnalysisFramesPerBlock, + &m_sampleBuffer[0], + m_sampleBuffer.size()); + frameIndex += framesRead; + DEBUG_ASSERT(pAudioSource->isValidFrameIndex(frameIndex)); + + // To compare apples to apples, let's only look at blocks that are + // the full block size. + DEBUG_ASSERT(kAnalysisFramesPerBlock >= framesRead); + if (kAnalysisFramesPerBlock == framesRead) { + // A complete block of audio samples has been read QListIterator it(m_aq); while (it.hasNext()) { Analyser* an = it.next(); //qDebug() << typeid(*an).name() << ".process()"; - an->process(&m_sampleBuffer[0], readFrameCount * kAnalysisChannels); + an->process(&m_sampleBuffer[0], framesRead * kAnalysisChannels); //qDebug() << "Done " << typeid(*an).name() << ".process()"; } } else { - if (progressFrameCount < pAudioSource->getFrameCount()) { + // a partial block of audio samples has been read + if (frameIndex < pAudioSource->getFrameCount()) { + // Fewer frames than actually expected have been read + // from the AudioSource. This indicates an error while + // decoding the audio stream and the analysis should + // stop now. qWarning() << "Failed to read sample data from file:" << tio->getFilename(); dieflag = true; // abort @@ -201,8 +216,8 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi // During the doAnalysis function it goes only to 100% - FINALIZE_PERCENT // because the finalise functions will take also some time //fp div here prevents insane signed overflow - DEBUG_ASSERT(progressFrameCount <= pAudioSource->getFrameCount()); - int progress = (int)(((float)progressFrameCount) / pAudioSource->getFrameCount() * + DEBUG_ASSERT(frameIndex <= pAudioSource->getFrameCount()); + int progress = (int)(((float)frameIndex) / pAudioSource->getFrameCount() * (1000 - FINALIZE_PROMILLE)); if (m_progressInfo.track_progress != progress) { @@ -239,7 +254,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi if (dieflag || cancelled) { t.cancel(); } - } while (!dieflag && (progressFrameCount < pAudioSource->getFrameCount())); + } while (!dieflag && (frameIndex < pAudioSource->getFrameCount())); return !cancelled; //don't return !dieflag or we might reanalyze over and over } From ac0a6d9f784ad4f082d270b8f599daa910fde331 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 21 Jan 2015 13:24:09 +0100 Subject: [PATCH 115/481] Improve detection of read errors and documentation in CachingReaderWorker --- src/cachingreaderworker.cpp | 80 ++++++++++++++++++++++--------------- src/cachingreaderworker.h | 3 +- 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index dc96b1fc87f..9d69a8d5b7a 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -11,20 +11,19 @@ #include "util/event.h" #include "util/math.h" -// There's a little math to this, but not much: 48khz stereo audio is 384kb/sec -// if using float samples. We want the chunk size to be a power of 2 so it's -// easier to memory align, and roughly 1/2 - 1/4th of a second of audio. 2**17 -// and 2**16 are nice candidates. 2**16 is 170ms of audio, which is well above -// (hopefully) the latencies people are seeing. at 10ms latency, one chunk is -// enough for 17 callbacks. We may need to tweak this later. - -// Must be divisible by 8, 4, and 2. Just pick a power of 2. -#define CHUNK_LENGTH 65536 - -const Mixxx::AudioSource::size_type CachingReaderWorker::kChunkLength = CHUNK_LENGTH; -const Mixxx::AudioSource::size_type CachingReaderWorker::kSamplesPerChunk = CHUNK_LENGTH / sizeof(Mixxx::AudioSource::sample_type); -const Mixxx::AudioSource::size_type CachingReaderWorker::kFramesPerChunk = kSamplesPerChunk / kChunkChannels; +// One chunk should contain 1/2 - 1/4th of a second of audio. +// 8192 frames contain about 170 ms of audio at 48 kHz, which +// is well above (hopefully) the latencies people are seeing. +// At 10 ms latency one chunk is enough for 17 callbacks. +// Additionally the chunk size should be a power of 2 for +// easier memory alignment. +// TODO(XXX): The optimum value of the "constant" kFramesPerChunk +// depends on the properties of the AudioSource as the remarks +// above suggest! +const Mixxx::AudioSource::size_type CachingReaderWorker::kChunkChannels = 2; // stereo +const Mixxx::AudioSource::size_type CachingReaderWorker::kFramesPerChunk = 8192; // ~ 170 ms at 48 kHz +const Mixxx::AudioSource::size_type CachingReaderWorker::kSamplesPerChunk = kFramesPerChunk * kChunkChannels; CachingReaderWorker::CachingReaderWorker(QString group, FIFO* pChunkReadRequestFIFO, @@ -39,45 +38,60 @@ CachingReaderWorker::CachingReaderWorker(QString group, CachingReaderWorker::~CachingReaderWorker() { } -void CachingReaderWorker::processChunkReadRequest(ChunkReadRequest* request, +void CachingReaderWorker::processChunkReadRequest( + ChunkReadRequest* request, ReaderStatusUpdate* update) { - int chunk_number = request->chunk->chunk_number; //qDebug() << "Processing ChunkReadRequest for" << chunk_number; + + // Initialize the output parameter update->chunk = request->chunk; update->chunk->frameCount = 0; + const int chunk_number = request->chunk->chunk_number; if (!m_pAudioSource || chunk_number < 0) { update->status = CHUNK_READ_INVALID; return; } - // Stereo samples - Mixxx::AudioSource::size_type frame_position = frameForChunk(chunk_number); - Mixxx::AudioSource::size_type frames_remaining = m_pAudioSource->getFrameCount() - frame_position; - Mixxx::AudioSource::size_type frames_to_read = math_min(kFramesPerChunk, frames_remaining); + const Mixxx::AudioSource::size_type chunkFrameIndex = + frameForChunk(chunk_number); + if (!m_pAudioSource->isValidFrameIndex(chunkFrameIndex)) { + // Frame index out of range + update->status = CHUNK_READ_INVALID; + return; + } - // Bogus chunk number - if (frames_to_read <= 0) { + const Mixxx::AudioSource::size_type seekFrameIndex = + m_pAudioSource->seekSampleFrame(chunkFrameIndex); + DEBUG_ASSERT(m_pAudioSource->isValidFrameIndex(seekFrameIndex)); + const Mixxx::AudioSource::size_type framesRemaining = + m_pAudioSource->getFrameCount() - seekFrameIndex; + const Mixxx::AudioSource::size_type framesToRead = + math_min(kFramesPerChunk, framesRemaining); + if (0 >= framesToRead) { update->status = CHUNK_READ_EOF; return; } - frame_position = m_pAudioSource->seekSampleFrame(frame_position); - - const Mixxx::AudioSource::size_type frames_read = + const Mixxx::AudioSource::size_type framesRead = m_pAudioSource->readSampleFramesStereo( - frames_to_read, request->chunk->stereoSamples, kSamplesPerChunk); + framesToRead, request->chunk->stereoSamples, kSamplesPerChunk); - // If we've run out of music, the AudioSource can return 0 frames/samples. - // Remember that AudioSource->getFrameCount() can lie to us about - // the length of the song! - if (frames_read <= 0) { - update->status = CHUNK_READ_EOF; + // If the AudioSource does not return any samples although + // there should still be some available at this position + // a read error must have occurred! + DEBUG_ASSERT(framesRead <= framesToRead); + if (0 >= framesRead) { + update->status = CHUNK_READ_INVALID; return; } + // Otherwise all requested samples should have been + // read entirely (according to the frame calculations + // above) + DEBUG_ASSERT(framesRead == framesToRead); update->status = CHUNK_READ_SUCCESS; - update->chunk->frameCount = frames_read; + update->chunk->frameCount = framesRead; } // WARNING: Always called from a different thread (GUI) @@ -177,7 +191,9 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { } // Emit that the track is loaded. - emit(trackLoaded(pTrack, m_pAudioSource->getFrameRate(), m_pAudioSource->getFrameCount() * kChunkChannels)); + const Mixxx::AudioSource::size_type sampleCount = + m_pAudioSource->getFrameCount() * kChunkChannels; + emit(trackLoaded(pTrack, m_pAudioSource->getFrameRate(), sampleCount)); } void CachingReaderWorker::quitWait() { diff --git a/src/cachingreaderworker.h b/src/cachingreaderworker.h index 26b48e2232b..df5feb1adad 100644 --- a/src/cachingreaderworker.h +++ b/src/cachingreaderworker.h @@ -82,8 +82,7 @@ class CachingReaderWorker : public EngineWorker { // A Chunk is a memory-resident section of audio that has been cached. Each // chunk holds a fixed number of stereo frames given by kFramesPerChunk. - static const Mixxx::AudioSource::size_type kChunkLength; - static const Mixxx::AudioSource::size_type kChunkChannels = 2; // stereo + static const Mixxx::AudioSource::size_type kChunkChannels; static const Mixxx::AudioSource::size_type kFramesPerChunk; static const Mixxx::AudioSource::size_type kSamplesPerChunk; // = kFramesPerChunk * kChunkChannels From 454826179467e8f2bc4c478da8a181f814d774ae Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 21 Jan 2015 13:52:48 +0100 Subject: [PATCH 116/481] Move creation logic for AudioSources into base class --- plugins/soundsourcem4a/audiosourcem4a.cpp | 11 ++--------- plugins/soundsourcem4a/audiosourcem4a.h | 3 +-- .../audiosourcemediafoundation.cpp | 11 ++--------- .../audiosourcemediafoundation.h | 3 +-- plugins/soundsourcewv/audiosourcewv.cpp | 11 ++--------- plugins/soundsourcewv/audiosourcewv.h | 3 +-- src/sources/audiosource.cpp | 11 +++++++++++ src/sources/audiosource.h | 10 ++++++++-- src/sources/audiosourcecoreaudio.cpp | 11 ++--------- src/sources/audiosourcecoreaudio.h | 3 +-- src/sources/audiosourceffmpeg.cpp | 11 ++--------- src/sources/audiosourceffmpeg.h | 3 +-- src/sources/audiosourceflac.cpp | 11 ++--------- src/sources/audiosourceflac.h | 3 +-- src/sources/audiosourcemodplug.cpp | 11 ++--------- src/sources/audiosourcemodplug.h | 3 +-- src/sources/audiosourcemp3.cpp | 11 ++--------- src/sources/audiosourcemp3.h | 3 +-- src/sources/audiosourceoggvorbis.cpp | 11 ++--------- src/sources/audiosourceoggvorbis.h | 3 +-- src/sources/audiosourceopus.cpp | 11 ++--------- src/sources/audiosourceopus.h | 3 +-- src/sources/audiosourcesndfile.cpp | 11 ++--------- src/sources/audiosourcesndfile.h | 3 +-- src/sources/soundsource.h | 1 - 25 files changed, 52 insertions(+), 124 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index f1a0188fc5b..13825bdcb8f 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -91,17 +91,10 @@ AudioSourceM4A::~AudioSourceM4A() { } AudioSourcePointer AudioSourceM4A::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceM4A(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceM4A(url)); } -Result AudioSourceM4A::open() { +Result AudioSourceM4A::postConstruct() { const QString fileName(getUrl().toLocalFile()); /* open MP4 file, check for >= ver 1.9.1 */ #if MP4V2_PROJECT_version_hex <= 0x00010901 diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 28432bb23fe..fdc15a38ae2 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -2,7 +2,6 @@ #define AUDIOSOURCEM4A_H #include "sources/audiosource.h" -#include "util/defs.h" #ifdef __MP4V2__ #include @@ -37,7 +36,7 @@ class AudioSourceM4A: public AudioSource { private: explicit AudioSourceM4A(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index 789216fc776..8929574521c 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -98,17 +98,10 @@ AudioSourceMediaFoundation::~AudioSourceMediaFoundation() { } AudioSourcePointer AudioSourceMediaFoundation::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceMediaFoundation(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceMediaFoundation(url)); } -Result AudioSourceMediaFoundation::open() { +Result AudioSourceMediaFoundation::postConstruct() { const QString fileName(getUrl().toLocalFile()); if (sDebug) { diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index 717da1fe842..60a7edf22c2 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -8,7 +8,6 @@ #define AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H #include "sources/audiosource.h" -#include "util/defs.h" #include @@ -37,7 +36,7 @@ class AudioSourceMediaFoundation : public AudioSource { private: explicit AudioSourceMediaFoundation(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 38e559246d0..f3debd85d79 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -13,17 +13,10 @@ AudioSourceWV::~AudioSourceWV() { } AudioSourcePointer AudioSourceWV::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceWV(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceWV(url)); } -Result AudioSourceWV::open() { +Result AudioSourceWV::postConstruct() { const QString fileName(getUrl().toLocalFile()); char msg[80]; // hold possible error message m_wpc = WavpackOpenFileInput( diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index da11f282936..74196180575 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -2,7 +2,6 @@ #define AUDIOSOURCEWV_H #include "sources/audiosource.h" -#include "util/defs.h" #include "wavpack/wavpack.h" @@ -28,7 +27,7 @@ class AudioSourceWV: public AudioSource { private: explicit AudioSourceWV(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index c2dfcd0962d..b2abd6718bd 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -11,6 +11,17 @@ namespace Mixxx { /*static*/const AudioSource::sample_type AudioSource::kSampleValuePeak = CSAMPLE_PEAK; +AudioSourcePointer AudioSource::onCreate(AudioSource* pNewAudioSource) { + AudioSourcePointer pAudioSource(pNewAudioSource); // take ownership + if (OK == pAudioSource->postConstruct()) { + // success + return pAudioSource; + } else { + // failure + return AudioSourcePointer(); + } +} + AudioSource::AudioSource(QUrl url) : m_url(url), m_channelCount(kChannelCountDefault), diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index ba94417ff00..8e07148154f 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -3,6 +3,7 @@ #include "util/assert.h" #include "util/types.h" // CSAMPLE +#include "util/defs.h" // Result #include #include @@ -11,6 +12,9 @@ namespace Mixxx { +class AudioSource; +typedef QSharedPointer AudioSourcePointer; + // Common interface and base class for audio sources. // // Both the number of channels and the frame rate must @@ -215,6 +219,10 @@ class AudioSource { protected: explicit AudioSource(QUrl url); + static AudioSourcePointer onCreate(AudioSource* pNewAudioSource); + + virtual Result postConstruct() /*override*/ = 0; + void setChannelCount(size_type channelCount); void setFrameRate(size_type frameRate); void setFrameCount(size_type frameCount); @@ -239,8 +247,6 @@ class AudioSource { size_type m_bitrate; }; -typedef QSharedPointer AudioSourcePointer; - } // namespace Mixxx #endif // MIXXX_AUDIOSOURCE_H diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index a29c7fc2c7a..701b9c4055a 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -20,18 +20,11 @@ AudioSourceCoreAudio::~AudioSourceCoreAudio() { } AudioSourcePointer AudioSourceCoreAudio::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceCoreAudio(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceCoreAudio(url)); } // soundsource overrides -Result AudioSourceCoreAudio::open() { +Result AudioSourceCoreAudio::postConstruct() { const QString fileName(getUrl().toLocalFile()); //Open the audio file. diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index 4489f42ef23..d525cce1eec 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -2,7 +2,6 @@ #define AUDIOSOURCECOREAUDIO_H #include "sources/audiosource.h" -#include "util/defs.h" #include //In our tree at lib/apple/ @@ -34,7 +33,7 @@ class AudioSourceCoreAudio: public AudioSource { private: explicit AudioSourceCoreAudio(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 08c32b9c269..d231c297551 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -31,17 +31,10 @@ AudioSourceFFmpeg::~AudioSourceFFmpeg() { } AudioSourcePointer AudioSourceFFmpeg::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceFFmpeg(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceFFmpeg(url)); } -Result AudioSourceFFmpeg::open() { +Result AudioSourceFFmpeg::postConstruct() { unsigned int i; AVDictionary *l_iFormatOpts = NULL; diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index ba94880285c..64177f8c86e 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -2,7 +2,6 @@ #define AUDIOSOURCEFFMPEG_H #include "sources/audiosource.h" -#include "util/defs.h" #include @@ -54,7 +53,7 @@ class AudioSourceFFmpeg : public AudioSource { private: explicit AudioSourceFFmpeg(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index d0736a94c31..9fc65691b77 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -74,17 +74,10 @@ AudioSourceFLAC::~AudioSourceFLAC() { } AudioSourcePointer AudioSourceFLAC::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceFLAC(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceFLAC(url)); } -Result AudioSourceFLAC::open() { +Result AudioSourceFLAC::postConstruct() { if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "SSFLAC: Could not read file!"; return ERR; diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index fc3ed958319..c2a41f1e3df 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -2,7 +2,6 @@ #define AUDIOSOURCEFLAC_H #include "sources/audiosource.h" -#include "util/defs.h" #include @@ -39,7 +38,7 @@ class AudioSourceFLAC: public AudioSource { private: explicit AudioSourceFLAC(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index aa21afeeb78..bf6200153f3 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -48,17 +48,10 @@ AudioSourceModPlug::~AudioSourceModPlug() { } AudioSourcePointer AudioSourceModPlug::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceModPlug(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceModPlug(url)); } -Result AudioSourceModPlug::open() { +Result AudioSourceModPlug::postConstruct() { const QString fileName(getUrl().toLocalFile()); ScopedTimer t("AudioSourceModPlug::open()"); diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index e310f2f046e..5f45cf25467 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -2,7 +2,6 @@ #define AUDIOSOURCEMODPLUG_H #include "sources/audiosource.h" -#include "util/defs.h" namespace ModPlug { #include @@ -38,7 +37,7 @@ class AudioSourceModPlug: public AudioSource { explicit AudioSourceModPlug(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 548c89b66c1..8d6e3609ceb 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -71,17 +71,10 @@ AudioSourceMp3::~AudioSourceMp3() { } AudioSourcePointer AudioSourceMp3::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceMp3(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceMp3(url)); } -Result AudioSourceMp3::open() { +Result AudioSourceMp3::postConstruct() { if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file:" << m_file.fileName(); return ERR; diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index d3658a28a3e..7ced7a822e7 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -2,7 +2,6 @@ #define AUDIOSOURCEMP3_H #include "sources/audiosource.h" -#include "util/defs.h" #ifdef _MSC_VER // So mad.h doesn't try to use inline assembly which MSVC doesn't support. @@ -34,7 +33,7 @@ class AudioSourceMp3: public AudioSource { private: explicit AudioSourceMp3(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 15858128c0b..f994c95b8a7 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -12,17 +12,10 @@ AudioSourceOggVorbis::~AudioSourceOggVorbis() { } AudioSourcePointer AudioSourceOggVorbis::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceOggVorbis(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceOggVorbis(url)); } -Result AudioSourceOggVorbis::open() { +Result AudioSourceOggVorbis::postConstruct() { const QString fileName(getUrl().toLocalFile()); const QByteArray qbaFilename(fileName.toLocal8Bit()); if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index e41f99796c3..ceb9273a9c6 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -2,7 +2,6 @@ #define AUDIOSOURCEOGGVORBIS_H #include "sources/audiosource.h" -#include "util/defs.h" #define OV_EXCLUDE_STATIC_CALLBACKS #include @@ -25,7 +24,7 @@ class AudioSourceOggVorbis: public AudioSource { private: explicit AudioSourceOggVorbis(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 868ee151524..1dc5347e4b9 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -12,17 +12,10 @@ AudioSourceOpus::~AudioSourceOpus() { } AudioSourcePointer AudioSourceOpus::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceOpus(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceOpus(url)); } -Result AudioSourceOpus::open() { +Result AudioSourceOpus::postConstruct() { const QString fileName(getUrl().toLocalFile()); const QByteArray qbaFilename(fileName.toLocal8Bit()); int errorCode = 0; diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index f301c94a60c..c9bca3df9ae 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -2,7 +2,6 @@ #define AUDIOSOURCEOPUS_H #include "sources/audiosource.h" -#include "util/defs.h" #define OV_EXCLUDE_STATIC_CALLBACKS #include @@ -28,7 +27,7 @@ class AudioSourceOpus: public AudioSource { private: explicit AudioSourceOpus(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index c6138c7a963..2a2b51fa36e 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -13,17 +13,10 @@ AudioSourceSndFile::~AudioSourceSndFile() { } AudioSourcePointer AudioSourceSndFile::create(QUrl url) { - QSharedPointer pAudioSource(new AudioSourceSndFile(url)); - if (OK == pAudioSource->open()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } + return onCreate(new AudioSourceSndFile(url)); } -Result AudioSourceSndFile::open() { +Result AudioSourceSndFile::postConstruct() { const QString fileName(getUrl().toLocalFile()); #ifdef __WINDOWS__ // Pointer valid until string changed diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 2f822dc1663..1a3ae1b2845 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -2,7 +2,6 @@ #define AUDIOSOURCESNDFILE_H #include "sources/audiosource.h" -#include "util/defs.h" #ifdef Q_OS_WIN //Enable unicode in libsndfile on Windows @@ -28,7 +27,7 @@ class AudioSourceSndFile: public AudioSource { private: explicit AudioSourceSndFile(QUrl url); - Result open(); + Result postConstruct() /*override*/; void close(); diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index a5a41b209f7..dbac01cdddc 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -30,7 +30,6 @@ */ #include "sources/audiosource.h" -#include "util/defs.h" #include From a66ccde0f60cef36dc4673f1404a901b9cbbb0e2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 21 Jan 2015 14:03:23 +0100 Subject: [PATCH 117/481] Avoid duplicate initialization of member variables --- plugins/soundsourcem4a/audiosourcem4a.cpp | 9 +++------ plugins/soundsourcewv/audiosourcewv.cpp | 2 +- src/sources/audiosourceflac.cpp | 2 -- src/sources/audiosourcemp3.cpp | 4 +--- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 13825bdcb8f..0d8ae983006 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -124,9 +124,6 @@ Result AudioSourceM4A::postConstruct() { const u_int32_t maxSampleBlockInputSize = MP4GetTrackMaxSampleSize(m_hFile, m_trackId); m_inputBuffer.resize(maxSampleBlockInputSize, 0); - // Initially the input buffer is empty - m_inputBufferOffset = 0; - m_inputBufferLength = 0; DEBUG_ASSERT(NULL == m_hDecoder); // not already opened m_hDecoder = NeAACDecOpen(); @@ -178,11 +175,11 @@ Result AudioSourceM4A::postConstruct() { * frames2samples(kFramesPerSampleBlock); m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); - // Invalidate current position - m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; + // Invalidate current position to enforce the following + // seek operation m_curFrameIndex = getFrameIndexMax(); - // Start decoding at the beginning of the file + // (Re-)Start decoding at the beginning of the file seekSampleFrame(kFrameIndexMin); return OK; diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index f3debd85d79..d4b48bfbb36 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -5,7 +5,7 @@ namespace Mixxx { AudioSourceWV::AudioSourceWV(QUrl url) : AudioSource(url), m_wpc(NULL), - m_sampleScale(0.0f) { + m_sampleScale(kSampleValueZero) { } AudioSourceWV::~AudioSourceWV() { diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 9fc65691b77..49d2019c3ff 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -103,8 +103,6 @@ Result AudioSourceFLAC::postConstruct() { return ERR; } - m_curFrameIndex = kFrameIndexMin; - return OK; } diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 8d6e3609ceb..6dbb6b8a0d1 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -96,8 +96,6 @@ Result AudioSourceMp3::postConstruct() { mad_timer_t madDuration = mad_timer_zero; unsigned long sumBitrate = 0; - m_curFrameIndex = kFrameIndexMin; - mad_header madHeader; mad_header_init(&madHeader); @@ -222,7 +220,7 @@ Result AudioSourceMp3::postConstruct() { return ERR; // abort } - // Initialize audio stream length + // Initialize the audio stream length setFrameCount(m_curFrameIndex); // Calculate average values From e65fdf46a119ae231900ea1e16b279ffbddd0ef1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 22 Jan 2015 08:13:26 +0100 Subject: [PATCH 118/481] Simplify destruction of AudioSources - Free/deallocate unmanaged resources in preDestroy() -- After postConstruct() fails -- In destructor - Prevent double free/deallocation - No need to reset member variables to initial/default values --- plugins/soundsourcem4a/audiosourcem4a.cpp | 12 ++--------- plugins/soundsourcem4a/audiosourcem4a.h | 2 +- .../audiosourcemediafoundation.cpp | 6 ++---- .../audiosourcemediafoundation.h | 2 +- plugins/soundsourcewv/audiosourcewv.cpp | 5 ++--- plugins/soundsourcewv/audiosourcewv.h | 2 +- src/sources/audiosource.cpp | 7 ------- src/sources/audiosource.h | 2 -- src/sources/audiosourcecoreaudio.cpp | 2 +- src/sources/audiosourcecoreaudio.h | 2 +- src/sources/audiosourceffmpeg.cpp | 8 +++++--- src/sources/audiosourceffmpeg.h | 4 ++-- src/sources/audiosourceflac.cpp | 6 ++---- src/sources/audiosourceflac.h | 2 +- src/sources/audiosourcemodplug.cpp | 5 ++--- src/sources/audiosourcemodplug.h | 2 +- src/sources/audiosourcemp3.cpp | 20 +++++++------------ src/sources/audiosourcemp3.h | 2 +- src/sources/audiosourceoggvorbis.cpp | 5 ++--- src/sources/audiosourceoggvorbis.h | 2 +- src/sources/audiosourceopus.cpp | 5 ++--- src/sources/audiosourceopus.h | 2 +- src/sources/audiosourcesndfile.cpp | 11 +++++----- src/sources/audiosourcesndfile.h | 2 +- 24 files changed, 44 insertions(+), 74 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 0d8ae983006..56aad171f7c 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -87,7 +87,7 @@ AudioSourceM4A::AudioSourceM4A(QUrl url) } AudioSourceM4A::~AudioSourceM4A() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceM4A::create(QUrl url) { @@ -185,7 +185,7 @@ Result AudioSourceM4A::postConstruct() { return OK; } -void AudioSourceM4A::close() { +void AudioSourceM4A::preDestroy() { if (m_hDecoder) { NeAACDecClose(m_hDecoder); m_hDecoder = NULL; @@ -194,14 +194,6 @@ void AudioSourceM4A::close() { MP4Close(m_hFile); m_hFile = MP4_INVALID_FILE_HANDLE; } - m_trackId = MP4_INVALID_TRACK_ID; - m_maxSampleBlockId = MP4_INVALID_SAMPLE_ID; - m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; - m_inputBuffer.clear(); - m_inputBufferOffset = 0; - m_inputBufferLength = 0; - m_curFrameIndex = kFrameIndexMin; - reset(); } bool AudioSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index fdc15a38ae2..5e10b3f8588 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -38,7 +38,7 @@ class AudioSourceM4A: public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); bool isValidSampleBlockId(MP4SampleId sampleBlockId) const; diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index 8929574521c..b54d153ff97 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -94,7 +94,7 @@ AudioSourceMediaFoundation::AudioSourceMediaFoundation(QUrl url) } AudioSourceMediaFoundation::~AudioSourceMediaFoundation() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceMediaFoundation::create(QUrl url) { @@ -150,7 +150,7 @@ Result AudioSourceMediaFoundation::postConstruct() { return OK; } -void AudioSourceMediaFoundation::close() { +void AudioSourceMediaFoundation::preDestroy() { delete[] m_wcFilename; m_wcFilename = NULL; delete[] m_leftoverBuffer; @@ -167,8 +167,6 @@ void AudioSourceMediaFoundation::close() { CoUninitialize(); m_hrCoInitialize = E_FAIL; } - - reset(); } Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekSampleFrame( diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index 60a7edf22c2..cff63d7688b 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -38,7 +38,7 @@ class AudioSourceMediaFoundation : public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); bool configureAudioStream(); diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index d4b48bfbb36..3efcd04596e 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -9,7 +9,7 @@ AudioSourceWV::AudioSourceWV(QUrl url) } AudioSourceWV::~AudioSourceWV() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceWV::create(QUrl url) { @@ -43,12 +43,11 @@ Result AudioSourceWV::postConstruct() { return OK; } -void AudioSourceWV::close() { +void AudioSourceWV::preDestroy() { if (m_wpc) { WavpackCloseFile(m_wpc); m_wpc = NULL; } - reset(); } AudioSource::diff_type AudioSourceWV::seekSampleFrame(diff_type frameIndex) { diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index 74196180575..4f9fb109312 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -29,7 +29,7 @@ class AudioSourceWV: public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); WavpackContext* m_wpc; diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index b2abd6718bd..a44a84e50dd 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -43,13 +43,6 @@ void AudioSource::setFrameCount(size_type frameCount) { m_frameCount = frameCount; } -void AudioSource::reset() { - m_channelCount = kChannelCountDefault; - m_frameRate = kFrameRateDefault; - m_frameCount = kFrameCountDefault; - m_bitrate = kBitrateDefault; -} - AudioSource::size_type AudioSource::getSampleBufferSize( size_type numberOfFrames, bool readStereoSamples) const { diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 8e07148154f..0795ddc8451 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -231,8 +231,6 @@ class AudioSource { m_bitrate = bitrate; } - void reset(); - size_type getSampleBufferSize( size_type numberOfFrames, bool readStereoSamples = false) const; diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index 701b9c4055a..f9ed7b0db31 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -119,7 +119,7 @@ Result AudioSourceCoreAudio::postConstruct() { return OK; } -void AudioSourceCoreAudio::close() { +void AudioSourceCoreAudio::preDestroy() { ExtAudioFileDispose(m_audioFile); } diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index d525cce1eec..afa329626c4 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -35,7 +35,7 @@ class AudioSourceCoreAudio: public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); ExtAudioFileRef m_audioFile; CAStreamBasicDescription m_inputFormat; diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index d231c297551..1652e243e5d 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -27,7 +27,7 @@ AudioSourceFFmpeg::AudioSourceFFmpeg(QUrl url) } AudioSourceFFmpeg::~AudioSourceFFmpeg() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceFFmpeg::create(QUrl url) { @@ -114,7 +114,7 @@ Result AudioSourceFFmpeg::postConstruct() { return OK; } -void AudioSourceFFmpeg::close() { +void AudioSourceFFmpeg::preDestroy() { clearCache(); if (m_pCodecCtx != NULL) { @@ -122,11 +122,13 @@ void AudioSourceFFmpeg::close() { avcodec_close(m_pCodecCtx); avformat_close_input(&m_pFormatCtx); av_free(m_pFormatCtx); + m_pFormatCtx = NULL; } if (m_pResample != NULL) { qDebug() << "~AudioSourceFFmpeg(): Delete FFMPEG Resampler"; delete m_pResample; + m_pResample = NULL; } while (m_SJumpPoints.size() > 0) { @@ -136,7 +138,7 @@ void AudioSourceFFmpeg::close() { } } -void AudioSourceFFmpeg::clearCache() throw() { +void AudioSourceFFmpeg::clearCache() { while (m_SCache.size() > 0) { struct ffmpegCacheObject* l_SRmObj = m_SCache[0]; m_SCache.remove(0); diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index 64177f8c86e..6f877318b82 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -55,12 +55,12 @@ class AudioSourceFFmpeg : public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); bool readFramesToCache(unsigned int count, qint64 offset); bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); quint64 getSizeofCache(); - void clearCache() throw(); + void clearCache(); unsigned int read(unsigned long size, SAMPLE*); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 49d2019c3ff..041cf9f66e2 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -70,7 +70,7 @@ AudioSourceFLAC::AudioSourceFLAC(QUrl url) } AudioSourceFLAC::~AudioSourceFLAC() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceFLAC::create(QUrl url) { @@ -106,15 +106,13 @@ Result AudioSourceFLAC::postConstruct() { return OK; } -void AudioSourceFLAC::close() { +void AudioSourceFLAC::preDestroy() { if (m_decoder) { FLAC__stream_decoder_finish(m_decoder); FLAC__stream_decoder_delete(m_decoder); // frees memory m_decoder = NULL; } - m_decodeSampleBuffer.clear(); m_file.close(); - reset(); } Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame( diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index c2a41f1e3df..0b3f9e7e8aa 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -40,7 +40,7 @@ class AudioSourceFLAC: public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index bf6200153f3..c2fe4732194 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -44,7 +44,7 @@ AudioSourceModPlug::AudioSourceModPlug(QUrl url) } AudioSourceModPlug::~AudioSourceModPlug() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceModPlug::create(QUrl url) { @@ -120,12 +120,11 @@ Result AudioSourceModPlug::postConstruct() { return OK; } -void AudioSourceModPlug::close() { +void AudioSourceModPlug::preDestroy() { if (m_pModFile) { ModPlug::ModPlug_Unload(m_pModFile); m_pModFile = NULL; } - reset(); } AudioSource::diff_type AudioSourceModPlug::seekSampleFrame( diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index 5f45cf25467..692b203f533 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -39,7 +39,7 @@ class AudioSourceModPlug: public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor unsigned long m_fileLength; // length of file in samples diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 6dbb6b8a0d1..c8224732d60 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -67,7 +67,7 @@ AudioSourceMp3::AudioSourceMp3(QUrl url) } AudioSourceMp3::~AudioSourceMp3() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceMp3::create(QUrl url) { @@ -281,22 +281,16 @@ AudioSource::diff_type AudioSourceMp3::restartDecoding( return seekFrame.frameIndex; } -void AudioSourceMp3::close() { +void AudioSourceMp3::preDestroy() { mad_synth_finish(&m_madSynth); mad_frame_finish(&m_madFrame); mad_stream_finish(&m_madStream); - m_madSynthCount = 0; - m_seekFrameList.clear(); - m_avgSeekFrameCount = 0; - m_curFrameIndex = kFrameIndexMin; - - m_file.unmap(m_pFileData); - m_fileSize = 0; - m_pFileData = NULL; - m_file.close(); - - reset(); + if (NULL != m_pFileData) { + m_file.unmap(m_pFileData); + m_file.close(); + m_pFileData = NULL; + } } void AudioSourceMp3::addSeekFrame( diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index 7ced7a822e7..ec604a611f5 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -35,7 +35,7 @@ class AudioSourceMp3: public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); inline size_type skipFrameSamples(size_type numberOfFrames) { return readSampleFrames(numberOfFrames, NULL); diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index f994c95b8a7..451b1d0817f 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -8,7 +8,7 @@ AudioSourceOggVorbis::AudioSourceOggVorbis(QUrl url) } AudioSourceOggVorbis::~AudioSourceOggVorbis() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceOggVorbis::create(QUrl url) { @@ -48,12 +48,11 @@ Result AudioSourceOggVorbis::postConstruct() { return OK; } -void AudioSourceOggVorbis::close() { +void AudioSourceOggVorbis::preDestroy() { const int clearResult = ov_clear(&m_vf); if (0 != clearResult) { qWarning() << "Failed to close OggVorbis file" << clearResult; } - reset(); } AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index ceb9273a9c6..55ffda5fdfb 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -26,7 +26,7 @@ class AudioSourceOggVorbis: public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); inline diff_type getCurrentFrameIndex() { return ov_pcm_tell(&m_vf); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 1dc5347e4b9..3e4a3e3a1d8 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -8,7 +8,7 @@ AudioSourceOpus::AudioSourceOpus(QUrl url) } AudioSourceOpus::~AudioSourceOpus() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceOpus::create(QUrl url) { @@ -45,12 +45,11 @@ Result AudioSourceOpus::postConstruct() { return OK; } -void AudioSourceOpus::close() { +void AudioSourceOpus::preDestroy() { if (m_pOggOpusFile) { op_free(m_pOggOpusFile); m_pOggOpusFile = NULL; } - reset(); } AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index c9bca3df9ae..516be108132 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -29,7 +29,7 @@ class AudioSourceOpus: public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); inline diff_type getCurrentFrameIndex() const { DEBUG_ASSERT(NULL != m_pOggOpusFile); diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index 2a2b51fa36e..4ee258320fc 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -9,7 +9,7 @@ AudioSourceSndFile::AudioSourceSndFile(QUrl url) } AudioSourceSndFile::~AudioSourceSndFile() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceSndFile::create(QUrl url) { @@ -46,17 +46,16 @@ Result AudioSourceSndFile::postConstruct() { return OK; } -void AudioSourceSndFile::close() { +void AudioSourceSndFile::preDestroy() { if (m_pSndFile) { const int closeResult = sf_close(m_pSndFile); - if (0 != closeResult) { + if (0 == closeResult) { + m_pSndFile = NULL; + } else { qWarning() << "Failed to close libsnd file:" << closeResult << sf_strerror(m_pSndFile); } - m_pSndFile = NULL; - memset(&m_sfInfo, 0, sizeof(m_sfInfo)); } - reset(); } AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 1a3ae1b2845..6eb830ba8d3 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -29,7 +29,7 @@ class AudioSourceSndFile: public AudioSource { Result postConstruct() /*override*/; - void close(); + void preDestroy(); SNDFILE* m_pSndFile; SF_INFO m_sfInfo; From 2f31aaf0dbb3dfd66156050c068493805a93b206 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 23 Jan 2015 00:30:31 +0100 Subject: [PATCH 119/481] Minor changes --- src/sources/audiosourcemp3.cpp | 42 +++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index c8224732d60..8aa8c206a67 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -241,6 +241,18 @@ Result AudioSourceMp3::postConstruct() { return OK; } +void AudioSourceMp3::preDestroy() { + mad_synth_finish(&m_madSynth); + mad_frame_finish(&m_madFrame); + mad_stream_finish(&m_madStream); + + if (NULL != m_pFileData) { + m_file.unmap(m_pFileData); + m_file.close(); + m_pFileData = NULL; + } +} + AudioSource::diff_type AudioSourceMp3::restartDecoding( const SeekFrameType& seekFrame) { qDebug() << "restartDecoding @" << seekFrame.frameIndex; @@ -270,29 +282,21 @@ AudioSource::diff_type AudioSourceMp3::restartDecoding( mad_synth_mute(&m_madSynth); } - if ((0 != decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) && - !MAD_RECOVERABLE(m_madStream.error)) { - qWarning() << "Unrecoverable MP3 frame header decoding error:" - << mad_stream_errorstr(&m_madStream); - // failure - return getFrameCount(); + if (0 != decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) { + if (MAD_RECOVERABLE(m_madStream.error)) { + qDebug() << "Recoverable MP3 frame header decoding error after restart:" + << mad_stream_errorstr(&m_madStream); + } else { + qWarning() << "Unrecoverable MP3 frame header decoding error after restart:" + << mad_stream_errorstr(&m_madStream); + // failure + return getFrameCount(); + } } return seekFrame.frameIndex; } -void AudioSourceMp3::preDestroy() { - mad_synth_finish(&m_madSynth); - mad_frame_finish(&m_madFrame); - mad_stream_finish(&m_madStream); - - if (NULL != m_pFileData) { - m_file.unmap(m_pFileData); - m_file.close(); - m_pFileData = NULL; - } -} - void AudioSourceMp3::addSeekFrame( diff_type frameIndex, const unsigned char* pInputData) { @@ -519,9 +523,9 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( } } // consume decoded output data - numberOfFramesRemaining -= synthReadCount; m_madSynthCount -= synthReadCount; m_curFrameIndex += synthReadCount; + numberOfFramesRemaining -= synthReadCount; } DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); From 831237d33a8ccd90d321644a85e789809aa2c7b6 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 23 Jan 2015 10:53:35 +0100 Subject: [PATCH 120/481] MP3: Clarification why "lost sync" warnings are suppressed --- src/sources/audiosourcemp3.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 8aa8c206a67..a9226fd8e8c 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -105,7 +105,7 @@ Result AudioSourceMp3::postConstruct() { if (MAD_ERROR_LOSTSYNC == m_madStream.error) { // When seeking through the file "lost synchronization" // warnings are expected. Just log them for debugging - // purposes. + // purposes and suppress them in the release version. qDebug() << "Recoverable MP3 header decoding error:" << mad_stream_errorstr(&m_madStream); } else { @@ -446,11 +446,10 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { if (pMadThisFrame != m_madStream.this_frame) { - // Suppress "lost synchronization" warnings if (MAD_ERROR_LOSTSYNC == m_madStream.error) { // When seeking through the file "lost synchronization" // warnings are expected. Just log them for debugging - // purposes. + // purposes and suppress them in the release version. qDebug() << "Recoverable MP3 frame decoding error:" << mad_stream_errorstr(&m_madStream); } else { From a8d4cc24251d1a12c4c042f5cb43094773407904 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 12:45:11 +0100 Subject: [PATCH 121/481] Fine-tune logging of MP3 decoding errors --- src/sources/audiosourcemp3.cpp | 92 +++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index a9226fd8e8c..cd61dd35614 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -37,13 +37,25 @@ int decodeFrameHeader( mad_stream* pMadStream, bool skipId3Tag) { const int result = mad_header_decode(pMadHeader, pMadStream); - if ((0 != result) && (MAD_ERROR_LOSTSYNC == pMadStream->error) - && skipId3Tag) { - long tagsize = id3_tag_query(pMadStream->this_frame, - pMadStream->bufend - pMadStream->this_frame); - if (0 < tagsize) { - // Skip ID3 tag data - mad_stream_skip(pMadStream, tagsize); + if (0 != result) { + if (MAD_RECOVERABLE(pMadStream->error)) { + if ((MAD_ERROR_LOSTSYNC == pMadStream->error) && skipId3Tag) { + long tagsize = id3_tag_query(pMadStream->this_frame, + pMadStream->bufend - pMadStream->this_frame); + if (0 < tagsize) { + // Skip ID3 tag data + mad_stream_skip(pMadStream, tagsize); + // Suppress lost synchronization warnings + return result; + } + } + qWarning() << "Recoverable MP3 header decoding error:" + << mad_stream_errorstr(pMadStream); + } else { + if (MAD_ERROR_BUFLEN != pMadStream->error) { // EOF + qWarning() << "Unrecoverable MP3 header decoding error:" + << mad_stream_errorstr(pMadStream); + } } } return result; @@ -102,23 +114,16 @@ Result AudioSourceMp3::postConstruct() { do { if (0 != decodeFrameHeader(&madHeader, &m_madStream, true)) { if (MAD_RECOVERABLE(m_madStream.error)) { - if (MAD_ERROR_LOSTSYNC == m_madStream.error) { - // When seeking through the file "lost synchronization" - // warnings are expected. Just log them for debugging - // purposes and suppress them in the release version. - qDebug() << "Recoverable MP3 header decoding error:" - << mad_stream_errorstr(&m_madStream); - } else { - qWarning() << "Recoverable MP3 header decoding error:" - << mad_stream_errorstr(&m_madStream); - } continue; } else { - if (MAD_ERROR_BUFLEN != m_madStream.error) { - qWarning() << "Unrecoverable MP3 header decoding error:" - << mad_stream_errorstr(&m_madStream); + if (MAD_ERROR_BUFLEN == m_madStream.error) { + // EOF + break; + } else { + // Abort + mad_header_finish(&madHeader); + return ERR; } - break; } } @@ -134,8 +139,9 @@ Result AudioSourceMp3::postConstruct() { qWarning() << "Differing number of channels in some headers:" << m_file.fileName() << getChannelCount() << "<>" << madChannelCount; + // Abort mad_header_finish(&madHeader); - return ERR; // abort + return ERR; } } const size_type madSampleRate = madHeader.samplerate; @@ -173,8 +179,9 @@ Result AudioSourceMp3::postConstruct() { default: qWarning() << "Invalid sample rate:" << m_file.fileName() << madSampleRate; + // Abort mad_header_finish(&madHeader); - return ERR; // abort + return ERR; } } else { // check for consistent frame/sample rate @@ -182,8 +189,9 @@ Result AudioSourceMp3::postConstruct() { qWarning() << "Differing sample rate in some headers:" << m_file.fileName() << getFrameRate() << "<>" << madSampleRate; + // Abort mad_header_finish(&madHeader); - return ERR; // abort + return ERR; } } @@ -209,7 +217,8 @@ Result AudioSourceMp3::postConstruct() { if (MAD_ERROR_BUFLEN != m_madStream.error) { qWarning() << "Unrecoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); - return ERR; // abort + // Abort + return ERR; } } @@ -217,7 +226,8 @@ Result AudioSourceMp3::postConstruct() { // This is not a working MP3 file. qWarning() << "SSMP3: This is not a working MP3 file:" << m_file.fileName(); - return ERR; // abort + // Abort + return ERR; } // Initialize the audio stream length @@ -235,6 +245,7 @@ Result AudioSourceMp3::postConstruct() { m_curFrameIndex = restartDecoding(m_seekFrameList.front()); if (m_curFrameIndex != m_seekFrameList.front().frameIndex) { qWarning() << "Failed to start decoding:" << m_file.fileName(); + // Abort return ERR; } @@ -283,13 +294,8 @@ AudioSource::diff_type AudioSourceMp3::restartDecoding( } if (0 != decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) { - if (MAD_RECOVERABLE(m_madStream.error)) { - qDebug() << "Recoverable MP3 frame header decoding error after restart:" - << mad_stream_errorstr(&m_madStream); - } else { - qWarning() << "Unrecoverable MP3 frame header decoding error after restart:" - << mad_stream_errorstr(&m_madStream); - // failure + if (!MAD_RECOVERABLE(m_madStream.error)) { + // Failure -> Seek to EOF return getFrameCount(); } } @@ -323,12 +329,15 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( 0; SeekFrameList::size_type upperBound = m_seekFrameList.size(); + DEBUG_ASSERT(lowerBound < upperBound); + // Initial guess based on average frame size SeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; - if (seekFrameIndex >= m_seekFrameList.size()) { - seekFrameIndex = m_seekFrameList.size() - 1; + if (seekFrameIndex >= upperBound) { + seekFrameIndex = upperBound - 1; } + while ((upperBound - lowerBound) > 1) { DEBUG_ASSERT(seekFrameIndex >= lowerBound); DEBUG_ASSERT(seekFrameIndex < upperBound); @@ -380,7 +389,7 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { // before the expected sync position seekFrameIndex -= kSeekFramePrefetchCount; } else { - // Restart decoding at the beginnig of the audio stream + // Restart decoding at the beginning of the audio stream seekFrameIndex = 0; } @@ -446,11 +455,12 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { if (pMadThisFrame != m_madStream.this_frame) { - if (MAD_ERROR_LOSTSYNC == m_madStream.error) { - // When seeking through the file "lost synchronization" - // warnings are expected. Just log them for debugging - // purposes and suppress them in the release version. - qDebug() << "Recoverable MP3 frame decoding error:" + // Suppress common cases of "lost synchronization" + // warnings. When seeking through the file those + // warnings are expected while skipping over + // prefetched frames. + if ((MAD_ERROR_LOSTSYNC == m_madStream.error) && (NULL == pSampleBuffer)) { + qDebug() << "Recoverable MP3 frame decoding error while skipping:" << mad_stream_errorstr(&m_madStream); } else { qWarning() << "Recoverable MP3 frame decoding error:" From 168ddd5a14df07abb769bbd01f2539a19e303923 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 17:18:10 +0100 Subject: [PATCH 122/481] Keep track of current seek/read position in Ogg and Opus sources --- src/sources/audiosourceoggvorbis.cpp | 35 ++++++++++++----- src/sources/audiosourceoggvorbis.h | 6 +-- src/sources/audiosourceopus.cpp | 58 +++++++++++++++++++++------- src/sources/audiosourceopus.h | 10 ++--- 4 files changed, 73 insertions(+), 36 deletions(-) diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 451b1d0817f..365d52beacd 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -2,8 +2,15 @@ namespace Mixxx { +namespace { + +const int kLogicalBitstreamIndex = -1; // whole stream/file + +} // anonymous namespace + AudioSourceOggVorbis::AudioSourceOggVorbis(QUrl url) - : AudioSource(url) { + : AudioSource(url), + m_curFrameIndex(0) { memset(&m_vf, 0, sizeof(m_vf)); } @@ -28,8 +35,8 @@ Result AudioSourceOggVorbis::postConstruct() { return ERR; } - // lookup the ogg's channels and samplerate - const vorbis_info* vi = ov_info(&m_vf, -1); + // lookup the ogg's channels and sample rate + const vorbis_info* vi = ov_info(&m_vf, kLogicalBitstreamIndex); if (!vi) { qWarning() << "Failed to read OggVorbis file:" << fileName; return ERR; @@ -37,7 +44,7 @@ Result AudioSourceOggVorbis::postConstruct() { setChannelCount(vi->channels); setFrameRate(vi->rate); - ogg_int64_t frameCount = ov_pcm_total(&m_vf, -1); + ogg_int64_t frameCount = ov_pcm_total(&m_vf, kLogicalBitstreamIndex); if (0 <= frameCount) { setFrameCount(frameCount); } else { @@ -57,16 +64,23 @@ void AudioSourceOggVorbis::preDestroy() { AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( diff_type frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); const int seekResult = ov_pcm_seek(&m_vf, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggVorbis file:" << seekResult; + const ogg_int64_t pcmOffset = ov_pcm_tell(&m_vf); + if (0 <= pcmOffset) { + m_curFrameIndex = pcmOffset; + } else { + // Reset to EOF + m_curFrameIndex = getFrameIndexMax(); + } } - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); - return getCurrentFrameIndex(); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + return m_curFrameIndex; } AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( @@ -85,11 +99,11 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFramesStereo( AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) >= sampleBufferSize); const size_type numberOfFramesTotal = math_min(numberOfFrames, - size_type(getFrameIndexMax() - getCurrentFrameIndex())); + size_type(getFrameIndexMax() - m_curFrameIndex)); sample_type* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; @@ -103,6 +117,7 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( break;// done } if (0 < readResult) { + m_curFrameIndex += readResult; if (isChannelCountMono()) { if (readStereoSamples) { for (long i = 0; i < readResult; ++i) { @@ -133,7 +148,7 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( } } - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); return numberOfFramesTotal - numberOfFramesRemaining; } diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index 55ffda5fdfb..bc0f63e500b 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -28,15 +28,13 @@ class AudioSourceOggVorbis: public AudioSource { void preDestroy(); - inline diff_type getCurrentFrameIndex() { - return ov_pcm_tell(&m_vf); - } - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); OggVorbis_File m_vf; + + diff_type m_curFrameIndex; }; } diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 3e4a3e3a1d8..7aaf16f4acb 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -2,9 +2,19 @@ namespace Mixxx { +namespace { + +const int kLogicalBitstreamIndex = -1; // whole stream/file + +} // anonymous namespace + +// Decoded output of opusfile has a fixed sample rate of 48 kHz +const AudioSource::size_type AudioSourceOpus::kFrameRate = 48000; + AudioSourceOpus::AudioSourceOpus(QUrl url) : AudioSource(url), - m_pOggOpusFile(NULL) { + m_pOggOpusFile(NULL), + m_curFrameIndex(0) { } AudioSourceOpus::~AudioSourceOpus() { @@ -31,17 +41,24 @@ Result AudioSourceOpus::postConstruct() { return ERR; } - setChannelCount(op_channel_count(m_pOggOpusFile, -1)); - setFrameRate(kFrameRate); + const int channelCount = op_channel_count(m_pOggOpusFile, kLogicalBitstreamIndex); + if (0 < channelCount) { + setChannelCount(channelCount); + } else { + qWarning() << "Failed to read channel configuration of OggOpus file:" << fileName; + return ERR; + } - ogg_int64_t frameCount = op_pcm_total(m_pOggOpusFile, -1); + ogg_int64_t frameCount = op_pcm_total(m_pOggOpusFile, kLogicalBitstreamIndex); if (0 <= frameCount) { setFrameCount(frameCount); } else { - qWarning() << "Failed to read OggOpus file:" << fileName; + qWarning() << "Failed to read total length of OggOpus file:" << fileName; return ERR; } + setFrameRate(kFrameRate); + return OK; } @@ -53,24 +70,33 @@ void AudioSourceOpus::preDestroy() { } AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggOpus file:" << seekResult; + const ogg_int64_t pcmOffset = op_pcm_tell(m_pOggOpusFile); + if (0 <= pcmOffset) { + m_curFrameIndex = pcmOffset; + } else { + // Reset to EOF + m_curFrameIndex = getFrameIndexMax(); + } + } else { + m_curFrameIndex = frameIndex; } - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); - return getCurrentFrameIndex(); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + return m_curFrameIndex; } AudioSource::size_type AudioSourceOpus::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); const size_type numberOfFramesTotal = math_min(numberOfFrames, - size_type(getFrameIndexMax() - getCurrentFrameIndex())); + size_type(getFrameIndexMax() - m_curFrameIndex)); sample_type* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; @@ -83,6 +109,7 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( break;// done } if (0 < readResult) { + m_curFrameIndex += readResult; pSampleBuffer += frames2samples(readResult); numberOfFramesRemaining -= readResult; } else { @@ -92,7 +119,7 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( } } - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); return numberOfFramesTotal - numberOfFramesRemaining; } @@ -100,11 +127,11 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) >= sampleBufferSize); const size_type numberOfFramesTotal = math_min(numberOfFrames, - size_type(getFrameIndexMax() - getCurrentFrameIndex())); + size_type(getFrameIndexMax() - m_curFrameIndex)); sample_type* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; @@ -117,7 +144,8 @@ AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( break;// done } if (0 < readResult) { - pSampleBuffer += readResult * 2; + m_curFrameIndex += readResult; + pSampleBuffer += readResult * 2; // stereo numberOfFramesRemaining -= readResult; } else { qWarning() << "Failed to read sample data from OggOpus file:" @@ -126,7 +154,7 @@ AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( } } - DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); return numberOfFramesTotal - numberOfFramesRemaining; } diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 516be108132..59e3bc362ae 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -10,8 +10,7 @@ namespace Mixxx { class AudioSourceOpus: public AudioSource { public: - // All Opus audio is encoded at 48 kHz - static const size_type kFrameRate = 48000; + static const size_type kFrameRate; static AudioSourcePointer create(QUrl url); @@ -31,12 +30,9 @@ class AudioSourceOpus: public AudioSource { void preDestroy(); - inline diff_type getCurrentFrameIndex() const { - DEBUG_ASSERT(NULL != m_pOggOpusFile); - return op_pcm_tell(m_pOggOpusFile); - } - OggOpusFile *m_pOggOpusFile; + + diff_type m_curFrameIndex; }; } From fac9babc4ee1fd3d6cfa69a3d841a1834366b98c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 17:18:30 +0100 Subject: [PATCH 123/481] Determine bitrate of Ogg and Opus files --- src/sources/audiosourceoggvorbis.cpp | 7 +++++++ src/sources/audiosourceopus.cpp | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 365d52beacd..5df6224b552 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -43,6 +43,13 @@ Result AudioSourceOggVorbis::postConstruct() { } setChannelCount(vi->channels); setFrameRate(vi->rate); + if (0 < vi->bitrate_nominal) { + setBitrate(vi->bitrate_nominal / 1000); + } else { + if ((0 < vi->bitrate_lower) && (vi->bitrate_lower == vi->bitrate_upper)) { + setBitrate(vi->bitrate_lower / 1000); + } + } ogg_int64_t frameCount = ov_pcm_total(&m_vf, kLogicalBitstreamIndex); if (0 <= frameCount) { diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 7aaf16f4acb..c5c017584fa 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -57,6 +57,14 @@ Result AudioSourceOpus::postConstruct() { return ERR; } + const opus_int32 bitrate = op_bitrate(m_pOggOpusFile, kLogicalBitstreamIndex); + if (0 < bitrate) { + setBitrate(bitrate / 1000); + } else { + qWarning() << "Failed to calculate bitrate of OggOpus file:" << fileName; + return ERR; + } + setFrameRate(kFrameRate); return OK; From c0c94c7a109b50ee15ac0d523c413d527f3efa88 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 16:56:08 +0100 Subject: [PATCH 124/481] Cleanup SoundSourceOpus --- src/sources/soundsourceopus.cpp | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 869913a761c..477f7ebf938 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -3,8 +3,11 @@ #include "sources/audiosourceopus.h" #include "metadata/trackmetadatataglib.h" -// Include this if taglib if new enough (version 1.9.1 have opusfile) -#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) +// TagLib has support for the Ogg Opus file format since version 1.9 +#define TAGLIB_HAS_OPUSFILE \ + ((TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9))) + +#if TAGLIB_HAS_OPUSFILE #include #endif @@ -37,23 +40,13 @@ class OggOpusFileOwner { } /* - Parse the the file to get metadata + Parse the file to get metadata */ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { const QByteArray qbaFilename(getLocalFilePath()); - int error = 0; - OggOpusFileOwner l_ptrOpusFile( - op_open_file(qbaFilename.constData(), &error)); - - pMetadata->setChannels(op_channel_count(l_ptrOpusFile, -1)); - pMetadata->setSampleRate(Mixxx::AudioSourceOpus::kFrameRate); - pMetadata->setBitrate(op_bitrate(l_ptrOpusFile, -1) / 1000); - pMetadata->setDuration( - op_pcm_total(l_ptrOpusFile, -1) / pMetadata->getSampleRate()); - // If we don't have new enough Taglib we use libopusfile parser! -#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) +#if TAGLIB_HAS_OPUSFILE TagLib::Ogg::Opus::File f(qbaFilename.constData()); if (!readAudioProperties(pMetadata, f)) { @@ -73,11 +66,21 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } } #else + int error = 0; + OggOpusFileOwner l_ptrOpusFile( + op_open_file(qbaFilename.constData(), &error)); + // From Taglib 1.9.x Opus is supported // Before that we have parse tags by this code int i = 0; const OpusTags *l_ptrOpusTags = op_tags(l_ptrOpusFile, -1); + pMetadata->setChannels(op_channel_count(l_ptrOpusFile, -1)); + pMetadata->setSampleRate(Mixxx::AudioSourceOpus::kFrameRate); + pMetadata->setBitrate(op_bitrate(l_ptrOpusFile, -1) / 1000); + pMetadata->setDuration( + op_pcm_total(l_ptrOpusFile, -1) / pMetadata->getSampleRate()); + // This is left for debug reasons !! // qDebug() << "opus: We have " << l_ptrOpusTags->comments; for (i = 0; i < l_ptrOpusTags->comments; ++i) { @@ -120,7 +123,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } QImage SoundSourceOpus::parseCoverArt() const { -#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) +#if TAGLIB_HAS_OPUSFILE TagLib::Ogg::Opus::File f(getLocalFilePath().constData()); TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { From aba177e5f69b49d3ae57e5bc8bb602aef552af6e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 17:13:36 +0100 Subject: [PATCH 125/481] Remove unreachable code: Decoding past EOF is not possible --- plugins/soundsourcem4a/audiosourcem4a.cpp | 5 +---- src/sources/audiosourceoggvorbis.cpp | 4 ---- src/sources/audiosourceopus.cpp | 8 -------- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 56aad171f7c..bd85909d49e 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -279,11 +279,8 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( ++m_curSampleBlockId; m_inputBufferLength = inputBufferLength; } - } DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); - if (m_inputBufferOffset >= m_inputBufferLength) { - // EOF - break;// done } + DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); // decode samples into sampleBuffer const size_type decodeBufferCapacityInBytes = frames2samples( diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 5df6224b552..c5d67ca0810 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -119,10 +119,6 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( int currentSection; const long readResult = ov_read_float(&m_vf, &pcmChannels, numberOfFramesRemaining, ¤tSection); - if (0 == readResult) { - // EOF - break;// done - } if (0 < readResult) { m_curFrameIndex += readResult; if (isChannelCountMono()) { diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index c5c017584fa..1b5103ba4e7 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -112,10 +112,6 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( int readResult = op_read_float(m_pOggOpusFile, pSampleBuffer, frames2samples(numberOfFramesRemaining), NULL); - if (0 == readResult) { - // EOF - break;// done - } if (0 < readResult) { m_curFrameIndex += readResult; pSampleBuffer += frames2samples(readResult); @@ -147,10 +143,6 @@ AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( int readResult = op_read_float_stereo(m_pOggOpusFile, pSampleBuffer, numberOfFramesRemaining * 2); - if (0 == readResult) { - // EOF - break;// done - } if (0 < readResult) { m_curFrameIndex += readResult; pSampleBuffer += readResult * 2; // stereo From d3e03999ac44d72a5f5392631d699203265fcbfa Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sat, 24 Jan 2015 17:53:30 +0100 Subject: [PATCH 126/481] cleaning + remove buffersize from period count --- src/effects/native/autopaneffect.cpp | 48 +++++++++------------------- src/effects/native/autopaneffect.h | 8 ++--- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index 00412450b26..858c256002d 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -5,6 +5,9 @@ #include "sampleutil.h" +const float positionRampingThreshold = 0.016f; + + // static QString AutoPanEffect::getId() { return "org.mixxx.effects.autopan"; @@ -24,8 +27,8 @@ EffectManifest AutoPanEffect::getManifest() { // my mixer. Any suggestion? // This parameter controls the easing of the sound from a side to another. EffectManifestParameter* strength = manifest.addParameter(); - strength->setId("strength"); - strength->setName(QObject::tr("Strength")); + strength->setId("curve"); + strength->setName(QObject::tr("Curve")); strength->setDescription( QObject::tr("How fast the signal goes from a channel to an other")); strength->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); @@ -49,21 +52,6 @@ EffectManifest AutoPanEffect::getManifest() { period->setMaximum(500.0); period->setDefault(50.0); - // This only extists for testing and finding the best value - // TODO(jclaveau) : remove when a satisfying value is chosen - // The current value is taken with a simple laptop soundcard with a sample - // rate of 44100 - EffectManifestParameter* ramping = manifest.addParameter(); - ramping->setId("ramping_threshold"); - ramping->setName(QObject::tr("Ramping")); - ramping->setDescription(""); - ramping->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); - ramping->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); - ramping->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - ramping->setMinimum(0.000000000001f); - ramping->setMaximum(0.015978f); // max good value with my soundcard - ramping->setDefault(0.003f); // best value - // This control is hidden for the moment in Deere (replace by the previous // one wich would be useless soon) EffectManifestParameter* depth = manifest.addParameter(); @@ -83,9 +71,8 @@ EffectManifest AutoPanEffect::getManifest() { AutoPanEffect::AutoPanEffect(EngineEffect* pEffect, const EffectManifest& manifest) : m_pDepthParameter(pEffect->getParameterById("depth")), - m_pStrengthParameter(pEffect->getParameterById("strength")), - m_pPeriodParameter(pEffect->getParameterById("period")), - m_pRampingParameter(pEffect->getParameterById("ramping_threshold")) + m_pCurveParameter(pEffect->getParameterById("curve")), + m_pPeriodParameter(pEffect->getParameterById("period")) { Q_UNUSED(manifest); } @@ -115,12 +102,12 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat double beats = pow(2, floor(period * 9 / 500) - 3); period = groupFeatures.beat_length * beats; } else { - period *= (float)numSamples / 2.0f; + // 500 * 2048 as max period as number of samples + period *= 2048.0f; } - CSAMPLE stepFrac = m_pStrengthParameter->value(); + CSAMPLE stepFrac = m_pCurveParameter->value(); CSAMPLE depth = m_pDepthParameter->value(); - float rampingTreshold = m_pRampingParameter->value(); if (gs.time > period || enableState == EffectProcessor::ENABLING) { gs.time = 0; @@ -144,10 +131,9 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat // size of a segment of slope (controled by the "strength" parameter) float u = (0.5f - stepFrac) / 2.0f; - gs.frac.setRamping(rampingTreshold); + gs.frac.setRampingThreshold(positionRampingThreshold); gs.frac.ramped = false; // just for debug - for (unsigned int i = 0; i + 1 < numSamples; i += 2) { CSAMPLE periodFraction = CSAMPLE(gs.time) / period; @@ -171,26 +157,22 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat } // transform the position into a sinusoid (but between 0 and 1) - gs.frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; - - pOutput[i] = pInput[i] * (1 - depth) - + pInput[i] * gs.frac * lawCoef * depth; + gs.frac.setWithRampingApplied((sin(M_PI * 2.0f * position) + 1.0f) / 2.0f); - pOutput[i+1] = pInput[i+1] * (1 - depth) - + pInput[i+1] * (1.0f - gs.frac) * lawCoef * depth; + pOutput[i] = pInput[i] * (1 - depth + gs.frac * lawCoef * depth); + pOutput[i+1] = pInput[i+1] * (1 - depth + (1.0f - gs.frac) * lawCoef * depth); gs.time++; } qDebug() - // << "| ramped :" << gs.frac.ramped + << "| ramped :" << gs.frac.ramped // << "| beat_length :" << groupFeatures.beat_length // << "| period :" << period << "| frac :" << gs.frac << "| time :" << gs.time - << "| rampingTreshold :" << rampingTreshold ; } diff --git a/src/effects/native/autopaneffect.h b/src/effects/native/autopaneffect.h index f63b527ffae..62c96ea710a 100644 --- a/src/effects/native/autopaneffect.h +++ b/src/effects/native/autopaneffect.h @@ -25,11 +25,11 @@ class RampedSample { virtual ~RampedSample(){}; - inline void setRamping(const float newMaxDifference) { + inline void setRampingThreshold(const float newMaxDifference) { maxDifference = newMaxDifference; } - inline RampedSample & operator=(const float newValue) { + inline void setWithRampingApplied(const float newValue) { if (!initialized) { currentValue = newValue; initialized = true; @@ -42,7 +42,6 @@ class RampedSample { currentValue = newValue; } } - return *this; } inline operator float() { @@ -94,9 +93,8 @@ class AutoPanEffect : public GroupEffectProcessor { } EngineEffectParameter* m_pDepthParameter; - EngineEffectParameter* m_pStrengthParameter; + EngineEffectParameter* m_pCurveParameter; EngineEffectParameter* m_pPeriodParameter; - EngineEffectParameter* m_pRampingParameter; DISALLOW_COPY_AND_ASSIGN(AutoPanEffect); }; From 884cf398f6ade0bd5f032455dfb82c39edeee033 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sat, 24 Jan 2015 18:10:42 +0100 Subject: [PATCH 127/481] removing useless depth + renaming position as angleFraction + setting good ramping threshold value --- src/effects/native/autopaneffect.cpp | 33 ++++++++-------------------- src/effects/native/autopaneffect.h | 1 - 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index 858c256002d..6c563749aa2 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -5,7 +5,7 @@ #include "sampleutil.h" -const float positionRampingThreshold = 0.016f; +const float positionRampingThreshold = 0.005f; // static @@ -52,25 +52,11 @@ EffectManifest AutoPanEffect::getManifest() { period->setMaximum(500.0); period->setDefault(50.0); - // This control is hidden for the moment in Deere (replace by the previous - // one wich would be useless soon) - EffectManifestParameter* depth = manifest.addParameter(); - depth->setId("depth"); - depth->setName(QObject::tr("Depth")); - depth->setDescription("Controls the intensity of the effect."); - depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); - depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); - depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - depth->setMinimum(0.0); - depth->setMaximum(1.0); - depth->setDefault(1.0); - return manifest; } AutoPanEffect::AutoPanEffect(EngineEffect* pEffect, const EffectManifest& manifest) : - m_pDepthParameter(pEffect->getParameterById("depth")), m_pCurveParameter(pEffect->getParameterById("curve")), m_pPeriodParameter(pEffect->getParameterById("period")) { @@ -107,7 +93,6 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat } CSAMPLE stepFrac = m_pCurveParameter->value(); - CSAMPLE depth = m_pDepthParameter->value(); if (gs.time > period || enableState == EffectProcessor::ENABLING) { gs.time = 0; @@ -147,21 +132,21 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat // float inInterval = fmod( periodFraction, (period / 2.0) ); float inStepInterval = fmod(periodFraction, 0.5f); - CSAMPLE position; + CSAMPLE angleFraction; if (inStepInterval > u && inStepInterval < (u + stepFrac)) { // at full left or full right - position = quarter < 2.0f ? 0.25f : 0.75f; + angleFraction = quarter < 2.0f ? 0.25f : 0.75f; } else { // in the slope (linear function) - position = (periodFraction - stepsFractionPart) * a; + angleFraction = (periodFraction - stepsFractionPart) * a; } - // transform the position into a sinusoid (but between 0 and 1) - gs.frac.setWithRampingApplied((sin(M_PI * 2.0f * position) + 1.0f) / 2.0f); - - pOutput[i] = pInput[i] * (1 - depth + gs.frac * lawCoef * depth); + // transform the angleFraction into a sinusoid (but between 0 and 1) + gs.frac.setWithRampingApplied( + (sin(M_PI * 2.0f * angleFraction) + 1.0f) / 2.0f); - pOutput[i+1] = pInput[i+1] * (1 - depth + (1.0f - gs.frac) * lawCoef * depth); + pOutput[i] = pInput[i] * gs.frac * lawCoef; + pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; gs.time++; } diff --git a/src/effects/native/autopaneffect.h b/src/effects/native/autopaneffect.h index 62c96ea710a..0421bb213c7 100644 --- a/src/effects/native/autopaneffect.h +++ b/src/effects/native/autopaneffect.h @@ -92,7 +92,6 @@ class AutoPanEffect : public GroupEffectProcessor { return getId(); } - EngineEffectParameter* m_pDepthParameter; EngineEffectParameter* m_pCurveParameter; EngineEffectParameter* m_pPeriodParameter; From 8d9d1d6b18add257ee31f01d03899bcb212808c6 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 18:50:08 +0100 Subject: [PATCH 128/481] Fix analysis of non-stereo audio sources --- src/analyserqueue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 135ff26fd7d..0832247a5c9 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -314,7 +314,7 @@ void AnalyserQueue::run() { bool processTrack = false; while (it.hasNext()) { // Make sure not to short-circuit initialise(...) - if (it.next()->initialise(nextTrack, pAudioSource->getFrameRate(), pAudioSource->frames2samples(pAudioSource->getFrameCount()))) { + if (it.next()->initialise(nextTrack, pAudioSource->getFrameRate(), pAudioSource->getFrameCount() * kAnalysisChannels)) { processTrack = true; } } From 64a24cf0acd2e57ca1796763b539a5d3a1f1b5cb Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 18:51:14 +0100 Subject: [PATCH 129/481] Minor cleanup --- src/analyserqueue.cpp | 24 +++++++++++++----------- src/sources/audiosourceoggvorbis.cpp | 8 ++++---- src/sources/audiosourceopus.cpp | 10 +++++----- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 0832247a5c9..81ce43fb3f1 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -166,16 +166,17 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi QTime progressUpdateInhibitTimer; progressUpdateInhibitTimer.start(); // Inhibit Updates for 60 milliseconds - Mixxx::AudioSource::size_type frameIndex = 0; + Mixxx::AudioSource::diff_type frameIndex = pAudioSource->getFrameIndexMin(); bool dieflag = false; bool cancelled = false; do { ScopedTimer t("AnalyserQueue::doAnalysis block"); - DEBUG_ASSERT(frameIndex < pAudioSource->getFrameCount()); + DEBUG_ASSERT(frameIndex < pAudioSource->getFrameIndexMax()); + const Mixxx::AudioSource::size_type framesRemaining = + pAudioSource->getFrameIndexMax() - frameIndex; const Mixxx::AudioSource::size_type framesToRead = - math_min(kAnalysisFramesPerBlock, - pAudioSource->getFrameCount() - frameIndex); + math_min(kAnalysisFramesPerBlock, framesRemaining); DEBUG_ASSERT(0 < framesToRead); const Mixxx::AudioSource::size_type framesRead = @@ -200,7 +201,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi } } else { // a partial block of audio samples has been read - if (frameIndex < pAudioSource->getFrameCount()) { + if (frameIndex < pAudioSource->getFrameIndexMax()) { // Fewer frames than actually expected have been read // from the AudioSource. This indicates an error while // decoding the audio stream and the analysis should @@ -216,14 +217,15 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi // During the doAnalysis function it goes only to 100% - FINALIZE_PERCENT // because the finalise functions will take also some time //fp div here prevents insane signed overflow - DEBUG_ASSERT(frameIndex <= pAudioSource->getFrameCount()); - int progress = (int)(((float)frameIndex) / pAudioSource->getFrameCount() * - (1000 - FINALIZE_PROMILLE)); + DEBUG_ASSERT(pAudioSource->isValidFrameIndex(frameIndex)); + const double frameProgress = + double(frameIndex) / double(pAudioSource->getFrameIndexMax()); + int progressPromille = frameProgress * (1000 - FINALIZE_PROMILLE); - if (m_progressInfo.track_progress != progress) { + if (m_progressInfo.track_progress != progressPromille) { if (progressUpdateInhibitTimer.elapsed() > 60) { // Inhibit Updates for 60 milliseconds - emitUpdateProgress(tio, progress); + emitUpdateProgress(tio, progressPromille); progressUpdateInhibitTimer.start(); } } @@ -254,7 +256,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi if (dieflag || cancelled) { t.cancel(); } - } while (!dieflag && (frameIndex < pAudioSource->getFrameCount())); + } while (!dieflag && (frameIndex < pAudioSource->getFrameIndexMax())); return !cancelled; //don't return !dieflag or we might reanalyze over and over } diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index c5d67ca0810..91c676d8022 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -51,11 +51,11 @@ Result AudioSourceOggVorbis::postConstruct() { } } - ogg_int64_t frameCount = ov_pcm_total(&m_vf, kLogicalBitstreamIndex); - if (0 <= frameCount) { - setFrameCount(frameCount); + ogg_int64_t pcmTotal = ov_pcm_total(&m_vf, kLogicalBitstreamIndex); + if (0 <= pcmTotal) { + setFrameCount(pcmTotal); } else { - qWarning() << "Failed to read OggVorbis file:" << fileName; + qWarning() << "Failed to read total length of OggVorbis file:" << fileName; return ERR; } diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 1b5103ba4e7..1562a92bd8b 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -49,9 +49,9 @@ Result AudioSourceOpus::postConstruct() { return ERR; } - ogg_int64_t frameCount = op_pcm_total(m_pOggOpusFile, kLogicalBitstreamIndex); - if (0 <= frameCount) { - setFrameCount(frameCount); + const ogg_int64_t pcmTotal = op_pcm_total(m_pOggOpusFile, kLogicalBitstreamIndex); + if (0 <= pcmTotal) { + setFrameCount(pcmTotal); } else { qWarning() << "Failed to read total length of OggOpus file:" << fileName; return ERR; @@ -61,7 +61,7 @@ Result AudioSourceOpus::postConstruct() { if (0 < bitrate) { setBitrate(bitrate / 1000); } else { - qWarning() << "Failed to calculate bitrate of OggOpus file:" << fileName; + qWarning() << "Failed to determine bitrate of OggOpus file:" << fileName; return ERR; } @@ -142,7 +142,7 @@ AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( while (0 < numberOfFramesRemaining) { int readResult = op_read_float_stereo(m_pOggOpusFile, pSampleBuffer, - numberOfFramesRemaining * 2); + numberOfFramesRemaining * 2); // stereo if (0 < readResult) { m_curFrameIndex += readResult; pSampleBuffer += readResult * 2; // stereo From 8cbf1bdbcc2f00e64b8bb62cd1a2ffaf995fb829 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 21:23:28 +0100 Subject: [PATCH 130/481] Delete wrong comment --- plugins/soundsourcem4a/audiosourcem4a.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index bd85909d49e..bd873df3c9a 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -326,9 +326,6 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( // consume decoded output data pSampleBuffer += decFrameInfo.samples; - // NeAACDecDecode2 always returns a complete sample block. - // As a consequence the last block is padded with 0.0 samples - // and const size_type numberOfFramesDecoded = samples2frames(decFrameInfo.samples); m_curFrameIndex += numberOfFramesDecoded; From ba324d3c72831d32effba0b2e30d76ea6119aa3f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 21:24:48 +0100 Subject: [PATCH 131/481] Minor index calculation cleanup --- src/cachingreaderworker.cpp | 2 +- src/sources/audiosourcemp3.cpp | 2 +- src/test/soundproxy_test.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 9d69a8d5b7a..61b4556a873 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -65,7 +65,7 @@ void CachingReaderWorker::processChunkReadRequest( m_pAudioSource->seekSampleFrame(chunkFrameIndex); DEBUG_ASSERT(m_pAudioSource->isValidFrameIndex(seekFrameIndex)); const Mixxx::AudioSource::size_type framesRemaining = - m_pAudioSource->getFrameCount() - seekFrameIndex; + m_pAudioSource->getFrameIndexMax() - seekFrameIndex; const Mixxx::AudioSource::size_type framesToRead = math_min(kFramesPerChunk, framesRemaining); if (0 >= framesToRead) { diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index cd61dd35614..a62ddbabe79 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -323,7 +323,7 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( DEBUG_ASSERT(0 < m_avgSeekFrameCount); DEBUG_ASSERT(!m_seekFrameList.empty()); DEBUG_ASSERT(kFrameIndexMin == m_seekFrameList.front().frameIndex); - DEBUG_ASSERT(diff_type(kFrameIndexMin + getFrameCount()) == m_seekFrameList.back().frameIndex); + DEBUG_ASSERT(diff_type(kFrameIndexMin + getFrameIndexMax()) == m_seekFrameList.back().frameIndex); SeekFrameList::size_type lowerBound = 0; diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 426dbcf8749..a1f3b2385a9 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -79,7 +79,7 @@ TEST_F(SoundSourceProxyTest, seekForward) { Mixxx::AudioSourcePointer pAudioSource1( openAudioSource(filePath)); EXPECT_FALSE(pAudioSource1.isNull()); - if ((seekFrameIndex + kTestFrameCount) > pAudioSource1->getFrameCount()) { + if ((seekFrameIndex + kTestFrameCount) > pAudioSource1->getFrameIndexMax()) { break; // finished } const unsigned int sampleCount1 = pAudioSource1->frames2samples(kTestFrameCount); @@ -97,7 +97,7 @@ TEST_F(SoundSourceProxyTest, seekForward) { Mixxx::AudioSourcePointer pAudioSource2( openAudioSource(filePath)); EXPECT_FALSE(pAudioSource2.isNull()); - if ((seekFrameIndex + kTestFrameCount) > pAudioSource2->getFrameCount()) { + if ((seekFrameIndex + kTestFrameCount) > pAudioSource2->getFrameIndexMax()) { break; // finished } const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); From d310018d229b8eb8b132b185a1444ee89cafa2ab Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 21:25:55 +0100 Subject: [PATCH 132/481] Remove chatty debug logging --- src/sources/audiosourceflac.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 041cf9f66e2..5f01cc8bd8d 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -332,17 +332,10 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata *metadata) { / sample_type( FLAC__int32(1) << metadata->data.stream_info.bits_per_sample); - qDebug() << "FLAC file " << m_file.fileName(); - qDebug() << getChannelCount() << " ch," << getFrameRate() << " Hz," - << getFrameCount() << " total," << "bit depth" - << metadata->data.stream_info.bits_per_sample; m_minBlocksize = metadata->data.stream_info.min_blocksize; m_maxBlocksize = metadata->data.stream_info.max_blocksize; m_minFramesize = metadata->data.stream_info.min_framesize; m_maxFramesize = metadata->data.stream_info.max_framesize; - qDebug() << "Blocksize in [" << m_minBlocksize << "," << m_maxBlocksize - << "], Framesize in [" << m_minFramesize << "," - << m_maxFramesize << "]"; m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; m_decodeSampleBuffer.resize(m_maxBlocksize * getChannelCount()); From 057c93fc36b37334ca670285e45d6e32db2c43c0 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 21:42:01 +0100 Subject: [PATCH 133/481] Improve decoding and logging for corrupt files --- plugins/soundsourcem4a/audiosourcem4a.cpp | 14 +++++++++++--- src/analyserqueue.cpp | 23 +++++++++++++---------- src/cachingreaderworker.cpp | 21 ++++++++++++--------- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index bd873df3c9a..c18fce49ab5 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -242,9 +242,17 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; // prefetch (decode and discard) all samples up to the target position DEBUG_ASSERT(frames2samples(prefetchFrameCount) <= m_prefetchSampleBuffer.size()); - readSampleFrames(prefetchFrameCount, &m_prefetchSampleBuffer[0]); - } DEBUG_ASSERT(m_curFrameIndex == frameIndex); - return frameIndex; + const size_type skipFrameCount = + readSampleFrames(prefetchFrameCount, &m_prefetchSampleBuffer[0]); + DEBUG_ASSERT(skipFrameCount <= prefetchFrameCount); + if (skipFrameCount != prefetchFrameCount) { + qWarning() << "Failed to skip over prefetched sample frames after seeking @" << m_curFrameIndex; + // Abort + return m_curFrameIndex; + } + } + DEBUG_ASSERT(m_curFrameIndex == frameIndex); + return m_curFrameIndex; } AudioSource::size_type AudioSourceM4A::readSampleFrames( diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 81ce43fb3f1..ac0abfcf2dd 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -184,14 +184,14 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi kAnalysisFramesPerBlock, &m_sampleBuffer[0], m_sampleBuffer.size()); + DEBUG_ASSERT(framesRead <= framesToRead); frameIndex += framesRead; DEBUG_ASSERT(pAudioSource->isValidFrameIndex(frameIndex)); // To compare apples to apples, let's only look at blocks that are // the full block size. - DEBUG_ASSERT(kAnalysisFramesPerBlock >= framesRead); if (kAnalysisFramesPerBlock == framesRead) { - // A complete block of audio samples has been read + // Complete analysis block of audio samples has been read. QListIterator it(m_aq); while (it.hasNext()) { Analyser* an = it.next(); @@ -200,16 +200,19 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi //qDebug() << "Done " << typeid(*an).name() << ".process()"; } } else { - // a partial block of audio samples has been read + // Partial analysis block of audio samples has been read. + // This should only happen at the end of an audio stream, + // otherwise a decoding error must have occurred. if (frameIndex < pAudioSource->getFrameIndexMax()) { - // Fewer frames than actually expected have been read - // from the AudioSource. This indicates an error while - // decoding the audio stream and the analysis should - // stop now. + // EOF not reached qWarning() << "Failed to read sample data from file:" - << tio->getFilename(); - dieflag = true; // abort - cancelled = false; // completed, no retry + << tio->getFilename() + << "@" << frameIndex; + if (0 >= framesRead) { + // If no frames have been read, abort the analysis + dieflag = true; // abort + cancelled = false; // completed, no retry + } } } diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 61b4556a873..641eca8c6a8 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -63,12 +63,20 @@ void CachingReaderWorker::processChunkReadRequest( const Mixxx::AudioSource::size_type seekFrameIndex = m_pAudioSource->seekSampleFrame(chunkFrameIndex); - DEBUG_ASSERT(m_pAudioSource->isValidFrameIndex(seekFrameIndex)); + if (seekFrameIndex != chunkFrameIndex) { + // Failed to seek to the requested index. + // Corrupt file? -> Stop reading! + qWarning() << "Failed to seek chunk position"; + update->status = CHUNK_READ_INVALID; + return; + } + const Mixxx::AudioSource::size_type framesRemaining = m_pAudioSource->getFrameIndexMax() - seekFrameIndex; const Mixxx::AudioSource::size_type framesToRead = math_min(kFramesPerChunk, framesRemaining); if (0 >= framesToRead) { + // No more data available for reading update->status = CHUNK_READ_EOF; return; } @@ -76,18 +84,13 @@ void CachingReaderWorker::processChunkReadRequest( const Mixxx::AudioSource::size_type framesRead = m_pAudioSource->readSampleFramesStereo( framesToRead, request->chunk->stereoSamples, kSamplesPerChunk); - - // If the AudioSource does not return any samples although - // there should still be some available at this position - // a read error must have occurred! DEBUG_ASSERT(framesRead <= framesToRead); - if (0 >= framesRead) { + if (framesRead < framesToRead) { + // Failed to read data! Corrupt file? + qWarning() << "Failed to read chunk samples"; update->status = CHUNK_READ_INVALID; return; } - // Otherwise all requested samples should have been - // read entirely (according to the frame calculations - // above) DEBUG_ASSERT(framesRead == framesToRead); update->status = CHUNK_READ_SUCCESS; From dbfea8aa10037226a8b9aeb37e2fe668d8029296 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 22:06:29 +0100 Subject: [PATCH 134/481] Fix wrong assertions --- src/sources/audiosource.cpp | 2 +- src/sources/audiosourceflac.cpp | 2 +- src/sources/audiosourcemp3.cpp | 2 +- src/sources/audiosourceoggvorbis.cpp | 2 +- src/sources/audiosourceopus.cpp | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index a44a84e50dd..efe0356e248 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -57,7 +57,7 @@ AudioSource::size_type AudioSource::readSampleFramesStereo( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { - DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) >= sampleBufferSize); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) <= sampleBufferSize); switch (getChannelCount()) { case 1: // mono channel diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 5f01cc8bd8d..ec76a471fd3 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -153,7 +153,7 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) >= sampleBufferSize); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); const size_type numberOfFramesTotal = numberOfFrames; diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index a62ddbabe79..85fe83f21b5 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -432,7 +432,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) >= sampleBufferSize); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 91c676d8022..c4b08f5bd50 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -107,7 +107,7 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) >= sampleBufferSize); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 1562a92bd8b..5a2f1f96da5 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -132,7 +132,7 @@ AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) >= sampleBufferSize); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) <= sampleBufferSize); const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); From f2929c4591a05d13428861a59edfe52d66cd34cd Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 24 Jan 2015 22:16:36 +0100 Subject: [PATCH 135/481] Fix seeking in Ogg files --- src/sources/audiosourceoggvorbis.cpp | 4 +++- src/sources/audiosourceopus.cpp | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index c4b08f5bd50..fb0d9813c70 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -75,7 +75,9 @@ AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( DEBUG_ASSERT(isValidFrameIndex(frameIndex)); const int seekResult = ov_pcm_seek(&m_vf, frameIndex); - if (0 != seekResult) { + if (0 == seekResult) { + m_curFrameIndex = frameIndex; + } else { qWarning() << "Failed to seek OggVorbis file:" << seekResult; const ogg_int64_t pcmOffset = ov_pcm_tell(&m_vf); if (0 <= pcmOffset) { diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 5a2f1f96da5..0679817927c 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -82,7 +82,9 @@ AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); - if (0 != seekResult) { + if (0 == seekResult) { + m_curFrameIndex = frameIndex; + } else { qWarning() << "Failed to seek OggOpus file:" << seekResult; const ogg_int64_t pcmOffset = op_pcm_tell(m_pOggOpusFile); if (0 <= pcmOffset) { @@ -91,8 +93,6 @@ AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { // Reset to EOF m_curFrameIndex = getFrameIndexMax(); } - } else { - m_curFrameIndex = frameIndex; } DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); From 823c9bfddc4efa214e5bc87f9a03297a5f7a12ba Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 25 Jan 2015 17:32:29 +0100 Subject: [PATCH 136/481] Delete obsolete functions from SampleUtil --- src/sampleutil.cpp | 10 ---------- src/sampleutil.h | 5 ----- 2 files changed, 15 deletions(-) diff --git a/src/sampleutil.cpp b/src/sampleutil.cpp index 61c33adef62..d793c45d79a 100644 --- a/src/sampleutil.cpp +++ b/src/sampleutil.cpp @@ -287,16 +287,6 @@ void SampleUtil::mixStereoToMono(CSAMPLE* pDest, const CSAMPLE* pSrc, } } -// static -void SampleUtil::doubleMonoToDualMono(SAMPLE* pBuffer, unsigned int numFrames) { - // backward loop - unsigned int i = numFrames; - while (0 < i--) { - pBuffer[i * 2] = pBuffer[i]; - pBuffer[i * 2 + 1] = pBuffer[i]; - } -} - // static void SampleUtil::doubleMonoToDualMono(CSAMPLE* pBuffer, unsigned int numFrames) { // backward loop diff --git a/src/sampleutil.h b/src/sampleutil.h index 804e8259806..cc99fe9295a 100644 --- a/src/sampleutil.h +++ b/src/sampleutil.h @@ -176,11 +176,6 @@ class SampleUtil { static void mixStereoToMono(CSAMPLE* pDest, const CSAMPLE* pSrc, unsigned int iNumSamples); - // In-place doubles the mono samples in pBuffer to dual mono samples. - // (numFrames) samples will be read from pBuffer - // (numFrames * 2) samples will be written into pBuffer - static void doubleMonoToDualMono(SAMPLE* pBuffer, unsigned int numFrames); - // In-place doubles the mono samples in pBuffer to dual mono samples. // (numFrames) samples will be read from pBuffer // (numFrames * 2) samples will be written into pBuffer From c25265fc37d45b92bd0fbbd9c9ff4a83d59efcd0 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 26 Jan 2015 12:11:53 +0100 Subject: [PATCH 137/481] Harmonize handling of URL (re-)sources --- plugins/soundsourcem4a/audiosourcem4a.cpp | 7 ++-- plugins/soundsourcem4a/soundsourcem4a.cpp | 4 +-- .../audiosourcemediafoundation.cpp | 2 +- .../soundsourcemediafoundation.cpp | 4 +-- plugins/soundsourcewv/audiosourcewv.cpp | 3 +- plugins/soundsourcewv/soundsourcewv.cpp | 4 +-- src/sources/audiosource.cpp | 15 ++++---- src/sources/audiosource.h | 13 ++----- src/sources/audiosourcecoreaudio.cpp | 2 +- src/sources/audiosourceffmpeg.cpp | 3 +- src/sources/audiosourceflac.cpp | 2 +- src/sources/audiosourcemodplug.cpp | 6 ++-- src/sources/audiosourcemp3.cpp | 2 +- src/sources/audiosourceoggvorbis.cpp | 11 +++--- src/sources/audiosourceopus.cpp | 13 ++++--- src/sources/audiosourcesndfile.cpp | 10 +++--- src/sources/soundsource.cpp | 7 ++-- src/sources/soundsource.h | 13 +------ src/sources/soundsourcecoreaudio.cpp | 8 ++--- src/sources/soundsourceffmpeg.cpp | 10 +++--- src/sources/soundsourceflac.cpp | 4 +-- src/sources/soundsourcemodplug.cpp | 2 +- src/sources/soundsourcemp3.cpp | 4 +-- src/sources/soundsourceoggvorbis.cpp | 4 +-- src/sources/soundsourceopus.cpp | 4 +-- src/sources/soundsourcesndfile.cpp | 12 +++---- src/sources/urlresource.h | 36 +++++++++++++++++++ 27 files changed, 105 insertions(+), 100 deletions(-) create mode 100644 src/sources/urlresource.h diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index c18fce49ab5..41345802d3f 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -95,15 +95,14 @@ AudioSourcePointer AudioSourceM4A::create(QUrl url) { } Result AudioSourceM4A::postConstruct() { - const QString fileName(getUrl().toLocalFile()); /* open MP4 file, check for >= ver 1.9.1 */ #if MP4V2_PROJECT_version_hex <= 0x00010901 - m_hFile = MP4Read(fileName.toLocal8Bit().constData(), 0); + m_hFile = MP4Read(getLocalFileNameBytes().constData(), 0); #else - m_hFile = MP4Read(fileName.toLocal8Bit().constData()); + m_hFile = MP4Read(getLocalFileNameBytes().constData()); #endif if (MP4_INVALID_FILE_HANDLE == m_hFile) { - qWarning() << "Failed to open file for reading:" << fileName; + qWarning() << "Failed to open file for reading:" << getUrl(); return ERR; } diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index de5fcb8d69f..eb30ff5483b 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -35,7 +35,7 @@ SoundSourceM4A::SoundSourceM4A(QUrl url) } Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::MP4::File f(getLocalFilePath().constData()); + TagLib::MP4::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -58,7 +58,7 @@ Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } QImage SoundSourceM4A::parseCoverArt() const { - TagLib::MP4::File f(getLocalFilePath().constData()); + TagLib::MP4::File f(getLocalFileNameBytes().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { return readMP4TagCover(*mp4); diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index b54d153ff97..afea4bb0a2f 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -102,7 +102,7 @@ AudioSourcePointer AudioSourceMediaFoundation::create(QUrl url) { } Result AudioSourceMediaFoundation::postConstruct() { - const QString fileName(getUrl().toLocalFile()); + const QString fileName(getLocalFileName()); if (sDebug) { qDebug() << "open()" << fileName; diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 78c59b2969b..574c905c11c 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -43,7 +43,7 @@ SoundSourceMediaFoundation::SoundSourceMediaFoundation(QUrl url) Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { // Must be toLocal8Bit since Windows fopen does not do UTF-8 - TagLib::MP4::File f(getLocalFilePath().constData()); + TagLib::MP4::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -66,7 +66,7 @@ Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata } QImage SoundSourceMediaFoundation::parseCoverArt() const { - TagLib::MP4::File f(getLocalFilePath().constData()); + TagLib::MP4::File f(getLocalFileNameBytes().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { return Mixxx::readMP4TagCover(*mp4); diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 3efcd04596e..e2989cd79ed 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -17,10 +17,9 @@ AudioSourcePointer AudioSourceWV::create(QUrl url) { } Result AudioSourceWV::postConstruct() { - const QString fileName(getUrl().toLocalFile()); char msg[80]; // hold possible error message m_wpc = WavpackOpenFileInput( - fileName.toLocal8Bit().constData(), msg, + getLocalFileNameBytes().constData(), msg, OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); if (!m_wpc) { qDebug() << "SSWV::open: failed to open file : " << msg; diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 558b13d6e68..acd1c8c6e0d 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -18,7 +18,7 @@ SoundSourceWV::SoundSourceWV(QUrl url) } Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::WavPack::File f(getLocalFilePath().constData()); + TagLib::WavPack::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -41,7 +41,7 @@ Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } QImage SoundSourceWV::parseCoverArt() const { - TagLib::WavPack::File f(getLocalFilePath().constData()); + TagLib::WavPack::File f(getLocalFileNameBytes().constData()); TagLib::APE::Tag *ape = f.APETag(); if (ape) { return Mixxx::readAPETagCover(*ape); diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index efe0356e248..eb2ad5c3d14 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -22,15 +22,12 @@ AudioSourcePointer AudioSource::onCreate(AudioSource* pNewAudioSource) { } } -AudioSource::AudioSource(QUrl url) : - m_url(url), - m_channelCount(kChannelCountDefault), - m_frameRate(kFrameRateDefault), - m_frameCount(kFrameCountDefault), - m_bitrate(kBitrateDefault) { -} - -AudioSource::~AudioSource() { +AudioSource::AudioSource(QUrl url) + : UrlResource(url), + m_channelCount(kChannelCountDefault), + m_frameRate(kFrameRateDefault), + m_frameCount(kFrameCountDefault), + m_bitrate(kBitrateDefault) { } void AudioSource::setChannelCount(size_type channelCount) { diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 0795ddc8451..fdfb56df23a 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -1,12 +1,13 @@ #ifndef MIXXX_AUDIOSOURCE_H #define MIXXX_AUDIOSOURCE_H +#include "urlresource.h" + #include "util/assert.h" #include "util/types.h" // CSAMPLE #include "util/defs.h" // Result #include -#include #include // size_t / diff_t @@ -31,7 +32,7 @@ typedef QSharedPointer AudioSourcePointer; // // Audio sources are implicitly opened upon creation and // closed upon destruction. -class AudioSource { +class AudioSource: public UrlResource { public: typedef std::size_t size_type; typedef std::ptrdiff_t diff_type; @@ -57,12 +58,6 @@ class AudioSource { static const size_type kBitrateZero = 0; static const size_type kBitrateDefault = kBitrateZero; - virtual ~AudioSource(); - - const QUrl& getUrl() const { - return m_url; - } - // Returns the number of channels. The number of channels // must be constant over time. inline size_type getChannelCount() const { @@ -236,8 +231,6 @@ class AudioSource { bool readStereoSamples = false) const; private: - const QUrl m_url; - size_type m_channelCount; size_type m_frameRate; size_type m_frameCount; diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index f9ed7b0db31..afbdbc54333 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -25,7 +25,7 @@ AudioSourcePointer AudioSourceCoreAudio::create(QUrl url) { // soundsource overrides Result AudioSourceCoreAudio::postConstruct() { - const QString fileName(getUrl().toLocalFile()); + const QString fileName(getLocalFileName()); //Open the audio file. OSStatus err; diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 1652e243e5d..997cfe781f2 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -38,8 +38,7 @@ Result AudioSourceFFmpeg::postConstruct() { unsigned int i; AVDictionary *l_iFormatOpts = NULL; - const QString fileName(getUrl().toLocalFile()); - const QByteArray qBAFilename = fileName.toLocal8Bit(); + const QByteArray qBAFilename(getLocalFileNameBytes()); qDebug() << "New AudioSourceFFmpeg :" << qBAFilename; diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index ec76a471fd3..02df581e039 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -57,7 +57,7 @@ void FLAC_error_cb(const FLAC__StreamDecoder*, AudioSourceFLAC::AudioSourceFLAC(QUrl url) : AudioSource(url), - m_file(getUrl().toLocalFile()), + m_file(getLocalFileName()), m_decoder(NULL), m_minBlocksize(0), m_maxBlocksize(0), diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index c2fe4732194..8db692d0231 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -52,14 +52,12 @@ AudioSourcePointer AudioSourceModPlug::create(QUrl url) { } Result AudioSourceModPlug::postConstruct() { - const QString fileName(getUrl().toLocalFile()); - ScopedTimer t("AudioSourceModPlug::open()"); - qDebug() << "[ModPlug] Loading ModPlug module " << fileName; - // read module file to byte array + const QString fileName(getLocalFileName()); QFile modFile(fileName); + qDebug() << "[ModPlug] Loading ModPlug module " << modFile.fileName(); modFile.open(QIODevice::ReadOnly); m_fileBuf = modFile.readAll(); modFile.close(); diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 85fe83f21b5..29335a6ec57 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -65,7 +65,7 @@ int decodeFrameHeader( AudioSourceMp3::AudioSourceMp3(QUrl url) : AudioSource(url), - m_file(getUrl().toLocalFile()), + m_file(getLocalFileName()), m_fileSize(0), m_pFileData(NULL), m_avgSeekFrameCount(0), diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index fb0d9813c70..4c4d4d162a4 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -23,22 +23,21 @@ AudioSourcePointer AudioSourceOggVorbis::create(QUrl url) { } Result AudioSourceOggVorbis::postConstruct() { - const QString fileName(getUrl().toLocalFile()); - const QByteArray qbaFilename(fileName.toLocal8Bit()); + const QByteArray qbaFilename(getLocalFileNameBytes()); if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { - qWarning() << "Failed to open OggVorbis file:" << fileName; + qWarning() << "Failed to open OggVorbis file:" << getUrl(); return ERR; } if (!ov_seekable(&m_vf)) { - qWarning() << "OggVorbis file is not seekable:" << fileName; + qWarning() << "OggVorbis file is not seekable:" << getUrl(); return ERR; } // lookup the ogg's channels and sample rate const vorbis_info* vi = ov_info(&m_vf, kLogicalBitstreamIndex); if (!vi) { - qWarning() << "Failed to read OggVorbis file:" << fileName; + qWarning() << "Failed to read OggVorbis file:" << getUrl(); return ERR; } setChannelCount(vi->channels); @@ -55,7 +54,7 @@ Result AudioSourceOggVorbis::postConstruct() { if (0 <= pcmTotal) { setFrameCount(pcmTotal); } else { - qWarning() << "Failed to read total length of OggVorbis file:" << fileName; + qWarning() << "Failed to read total length of OggVorbis file:" << getUrl(); return ERR; } diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 0679817927c..94e8be69d6f 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -26,18 +26,17 @@ AudioSourcePointer AudioSourceOpus::create(QUrl url) { } Result AudioSourceOpus::postConstruct() { - const QString fileName(getUrl().toLocalFile()); - const QByteArray qbaFilename(fileName.toLocal8Bit()); + const QByteArray qbaFilename(getLocalFileNameBytes()); int errorCode = 0; m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); if (!m_pOggOpusFile) { - qDebug() << "Failed to open OggOpus file:" << fileName << "errorCode" + qDebug() << "Failed to open OggOpus file:" << getUrl() << "errorCode" << errorCode; return ERR; } if (!op_seekable(m_pOggOpusFile)) { - qWarning() << "OggOpus file is not seekable:" << fileName; + qWarning() << "OggOpus file is not seekable:" << getUrl(); return ERR; } @@ -45,7 +44,7 @@ Result AudioSourceOpus::postConstruct() { if (0 < channelCount) { setChannelCount(channelCount); } else { - qWarning() << "Failed to read channel configuration of OggOpus file:" << fileName; + qWarning() << "Failed to read channel configuration of OggOpus file:" << getUrl(); return ERR; } @@ -53,7 +52,7 @@ Result AudioSourceOpus::postConstruct() { if (0 <= pcmTotal) { setFrameCount(pcmTotal); } else { - qWarning() << "Failed to read total length of OggOpus file:" << fileName; + qWarning() << "Failed to read total length of OggOpus file:" << getUrl(); return ERR; } @@ -61,7 +60,7 @@ Result AudioSourceOpus::postConstruct() { if (0 < bitrate) { setBitrate(bitrate / 1000); } else { - qWarning() << "Failed to determine bitrate of OggOpus file:" << fileName; + qWarning() << "Failed to determine bitrate of OggOpus file:" << getUrl(); return ERR; } diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index 4ee258320fc..e0ac2aa23f0 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -17,24 +17,24 @@ AudioSourcePointer AudioSourceSndFile::create(QUrl url) { } Result AudioSourceSndFile::postConstruct() { - const QString fileName(getUrl().toLocalFile()); #ifdef __WINDOWS__ // Pointer valid until string changed - LPCWSTR lpcwFilename = (LPCWSTR)fileName.utf16(); + const QString fileName(getLocalFileName()); + LPCWSTR lpcwFilename = (LPCWSTR) fileName.utf16(); m_pSndFile = sf_wchar_open(lpcwFilename, SFM_READ, &m_sfInfo); #else - m_pSndFile = sf_open(fileName.toLocal8Bit().constData(), SFM_READ, + m_pSndFile = sf_open(getLocalFileNameBytes().constData(), SFM_READ, &m_sfInfo); #endif if (m_pSndFile == NULL) { // sf_format_check is only for writes - qWarning() << "Error opening libsndfile file:" << fileName + qWarning() << "Error opening libsndfile file:" << getUrl() << sf_strerror(m_pSndFile); return ERR; } if (sf_error(m_pSndFile) > 0) { - qWarning() << "Error opening libsndfile file:" << fileName + qWarning() << "Error opening libsndfile file:" << getUrl() << sf_strerror(m_pSndFile); return ERR; } diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index a81f7c1172c..ae10fb1750c 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -25,18 +25,15 @@ namespace Mixxx { } SoundSource::SoundSource(QUrl url) - : m_url(url), + : UrlResource(url), m_type(getTypeFromUrl(url)) { DEBUG_ASSERT(getUrl().isValid()); } SoundSource::SoundSource(QUrl url, QString type) - : m_url(url), + : UrlResource(url), m_type(type) { DEBUG_ASSERT(getUrl().isValid()); } -SoundSource::~SoundSource() { -} - } //namespace Mixxx diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index dbac01cdddc..d5f17a35c4d 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -50,15 +50,10 @@ namespace Mixxx { /* Base class for sound sources. */ -class SoundSource { +class SoundSource: public UrlResource { public: - virtual ~SoundSource(); - static QString getTypeFromUrl(QUrl url); - inline const QUrl& getUrl() const { - return m_url; - } inline const QString& getType() const { return m_type; } @@ -81,13 +76,7 @@ class SoundSource { explicit SoundSource(QUrl url); SoundSource(QUrl url, QString type); - inline QByteArray getLocalFilePath() const { - DEBUG_ASSERT(getUrl().isLocalFile()); - return getUrl().toLocalFile().toLocal8Bit(); - } - private: - const QUrl m_url; const QString m_type; }; diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index 440ed65236c..35efdbf8e21 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -42,7 +42,7 @@ SoundSourceCoreAudio::SoundSourceCoreAudio(QUrl url) Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { if (getType() == "m4a") { - TagLib::MP4::File f(getLocalFilePath().constData()); + TagLib::MP4::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -59,7 +59,7 @@ Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) cons } } } else if (getType() == "mp3") { - TagLib::MPEG::File f(getLocalFilePath().constData()); + TagLib::MPEG::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -92,7 +92,7 @@ Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) cons QImage SoundSourceCoreAudio::parseCoverArt() const { QImage coverArt; if (getType() == "m4a") { - TagLib::MP4::File f(getLocalFilePath().constData()); + TagLib::MP4::File f(getLocalFileNameBytes().constData()); TagLib::MP4::Tag *mp4(f.tag()); if (mp4) { return Mixxx::readMP4TagCover(*mp4); @@ -100,7 +100,7 @@ QImage SoundSourceCoreAudio::parseCoverArt() const { return QImage(); } } else if (getType() == "mp3") { - TagLib::MPEG::File f(getLocalFilePath().constData()); + TagLib::MPEG::File f(getLocalFileNameBytes().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index 4ad62d88170..12768b91712 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -65,7 +65,7 @@ SoundSourceFFmpeg::SoundSourceFFmpeg(QUrl url) } Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - qDebug() << "ffmpeg: SoundSourceFFmpeg::parseMetadata" << getFilename(); + qDebug() << "ffmpeg: SoundSourceFFmpeg::parseMetadata" << getLocalFileName(); AVFormatContext *FmtCtx = avformat_alloc_context(); AVCodecContext *CodecCtx = NULL; @@ -73,9 +73,9 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { unsigned int i; AVDictionary *l_iFormatOpts = NULL; - if (avformat_open_input(&FmtCtx, getLocalFilePath().constData(), NULL, + if (avformat_open_input(&FmtCtx, getLocalFileNameBytes().constData(), NULL, &l_iFormatOpts) !=0) { - qDebug() << "av_open_input_file: cannot open" << getFilename(); + qDebug() << "av_open_input_file: cannot open" << getLocalFileName(); return ERR; } @@ -92,7 +92,7 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { // Retrieve stream information if (avformat_find_stream_info(FmtCtx, NULL)<0) { qDebug() << "av_find_stream_info: Can't find metadata" << - getFilename(); + getLocalFileName(); avcodec_close(CodecCtx); avformat_close_input(&FmtCtx); av_free(FmtCtx); @@ -108,7 +108,7 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } if (iAudioStream == -1) { qDebug() << "cannot find an audio stream: Can't find stream" << - getFilename(); + getLocalFileName(); avcodec_close(CodecCtx); avformat_close_input(&FmtCtx); av_free(FmtCtx); diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 07d4387835d..66643163f22 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -33,7 +33,7 @@ SoundSourceFLAC::SoundSourceFLAC(QUrl url) } Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::FLAC::File f(getLocalFilePath().constData()); + TagLib::FLAC::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -61,7 +61,7 @@ Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } QImage SoundSourceFLAC::parseCoverArt() const { - TagLib::FLAC::File f(getLocalFilePath().constData()); + TagLib::FLAC::File f(getLocalFileNameBytes().constData()); QImage coverArt; TagLib::Ogg::XiphComment *xiph(f.xiphComment()); if (xiph) { diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index 421a4c98d9e..1e7efb5f97f 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -50,7 +50,7 @@ SoundSourceModPlug::SoundSourceModPlug(QUrl url) : Result SoundSourceModPlug::parseMetadata( Mixxx::TrackMetadata* pMetadata) const { - QFile modFile(getLocalFilePath()); + QFile modFile(getLocalFileNameBytes()); modFile.open(QIODevice::ReadOnly); const QByteArray fileBuf(modFile.readAll()); modFile.close(); diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index f932d99c64a..ae257d95333 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -32,7 +32,7 @@ SoundSourceMp3::SoundSourceMp3(QUrl url) } Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::MPEG::File f(getLocalFilePath().constData()); + TagLib::MPEG::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -62,7 +62,7 @@ Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { QImage SoundSourceMp3::parseCoverArt() const { QImage coverArt; - TagLib::MPEG::File f(getLocalFilePath().constData()); + TagLib::MPEG::File f(getLocalFileNameBytes().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index c1cd08dd28b..fe0122256b0 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -36,7 +36,7 @@ SoundSourceOggVorbis::SoundSourceOggVorbis(QUrl url) : */ Result SoundSourceOggVorbis::parseMetadata( Mixxx::TrackMetadata* pMetadata) const { - TagLib::Ogg::Vorbis::File f(getLocalFilePath().constData()); + TagLib::Ogg::Vorbis::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; @@ -59,7 +59,7 @@ Result SoundSourceOggVorbis::parseMetadata( } QImage SoundSourceOggVorbis::parseCoverArt() const { - TagLib::Ogg::Vorbis::File f(getLocalFilePath().constData()); + TagLib::Ogg::Vorbis::File f(getLocalFileNameBytes().constData()); TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { return Mixxx::readXiphCommentCover(*xiph); diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 477f7ebf938..74849bf5803 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -43,7 +43,7 @@ class OggOpusFileOwner { Parse the file to get metadata */ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - const QByteArray qbaFilename(getLocalFilePath()); + const QByteArray qbaFilename(getLocalFileNameBytes()); // If we don't have new enough Taglib we use libopusfile parser! #if TAGLIB_HAS_OPUSFILE @@ -124,7 +124,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { QImage SoundSourceOpus::parseCoverArt() const { #if TAGLIB_HAS_OPUSFILE - TagLib::Ogg::Opus::File f(getLocalFilePath().constData()); + TagLib::Ogg::Opus::File f(getLocalFileNameBytes().constData()); TagLib::Ogg::XiphComment *xiph = f.tag(); if (xiph) { return Mixxx::readXiphCommentCover(*xiph); diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 13a59443a62..f0a114fba30 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -24,7 +24,7 @@ SoundSourceSndFile::SoundSourceSndFile(QUrl url) Result SoundSourceSndFile::parseMetadata( Mixxx::TrackMetadata* pMetadata) const { if (getType() == "flac") { - TagLib::FLAC::File f(getLocalFilePath().constData()); + TagLib::FLAC::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -46,7 +46,7 @@ Result SoundSourceSndFile::parseMetadata( } } } else if (getType() == "wav") { - TagLib::RIFF::WAV::File f(getLocalFilePath().constData()); + TagLib::RIFF::WAV::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -63,7 +63,7 @@ Result SoundSourceSndFile::parseMetadata( } } else if (getType().startsWith("aif")) { // Try AIFF - TagLib::RIFF::AIFF::File f(getLocalFilePath().constData()); + TagLib::RIFF::AIFF::File f(getLocalFileNameBytes().constData()); if (!readAudioProperties(pMetadata, f)) { return ERR; } @@ -84,7 +84,7 @@ QImage SoundSourceSndFile::parseCoverArt() const { QImage coverArt; if (getType() == "flac") { - TagLib::FLAC::File f(getLocalFilePath().constData()); + TagLib::FLAC::File f(getLocalFileNameBytes().constData()); TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); @@ -105,14 +105,14 @@ QImage SoundSourceSndFile::parseCoverArt() const { } } } else if (getType() == "wav") { - TagLib::RIFF::WAV::File f(getLocalFilePath().constData()); + TagLib::RIFF::WAV::File f(getLocalFileNameBytes().constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); } } else if (getType().startsWith("aif")) { // Try AIFF - TagLib::RIFF::AIFF::File f(getLocalFilePath().constData()); + TagLib::RIFF::AIFF::File f(getLocalFileNameBytes().constData()); TagLib::ID3v2::Tag* id3v2 = f.tag(); if (id3v2) { coverArt = Mixxx::readID3v2TagCover(*id3v2); diff --git a/src/sources/urlresource.h b/src/sources/urlresource.h new file mode 100644 index 00000000000..7cb2722ceee --- /dev/null +++ b/src/sources/urlresource.h @@ -0,0 +1,36 @@ +#ifndef MIXXX_URLRESOURCE_H +#define MIXXX_URLRESOURCE_H + +#include "util/assert.h" + +#include + +namespace Mixxx { + +class UrlResource { +public: + virtual ~UrlResource() {} + + const QUrl& getUrl() const { + return m_url; + } + +protected: + explicit UrlResource(QUrl url) + : m_url(url) { + } + + inline QString getLocalFileName() const { + return getUrl().toLocalFile(); + } + inline QByteArray getLocalFileNameBytes() const { + return getLocalFileName().toLocal8Bit(); + } + +private: + const QUrl m_url; +}; + +} // namespace Mixxx + +#endif // MIXXX_URLRESOURCE_H From add73d6919d317c2182b2b995de26002da31978f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 26 Jan 2015 19:24:13 +0100 Subject: [PATCH 138/481] MP3: Ignore all recoverable errors during re-synchronization after seeking --- src/sources/audiosourcemp3.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 29335a6ec57..f2f1c19d18e 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -455,11 +455,11 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { if (pMadThisFrame != m_madStream.this_frame) { - // Suppress common cases of "lost synchronization" - // warnings. When seeking through the file those - // warnings are expected while skipping over - // prefetched frames. - if ((MAD_ERROR_LOSTSYNC == m_madStream.error) && (NULL == pSampleBuffer)) { + // Ignore all recoverable errors (and especially + // "lost synchronization" warnings) while skipping + // over prefetched frames after seeking. + if (NULL == pSampleBuffer) { + // Decoded samples will simply be discarded qDebug() << "Recoverable MP3 frame decoding error while skipping:" << mad_stream_errorstr(&m_madStream); } else { From bb47a433547d4c677e7a3dd05c581cee8bd87964 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 26 Jan 2015 21:27:22 +0100 Subject: [PATCH 139/481] Clarification of comments in AnalyserQueue --- src/analyserqueue.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index ac0abfcf2dd..2825d40ae40 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -204,12 +204,13 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi // This should only happen at the end of an audio stream, // otherwise a decoding error must have occurred. if (frameIndex < pAudioSource->getFrameIndexMax()) { - // EOF not reached + // EOF not reached -> Maybe a corrupt file? qWarning() << "Failed to read sample data from file:" << tio->getFilename() << "@" << frameIndex; if (0 >= framesRead) { - // If no frames have been read, abort the analysis + // If no frames have been read then abort the analysis. + // Otherwise we might get stuck in this loop forever. dieflag = true; // abort cancelled = false; // completed, no retry } From 56c44266d1b88895444418de09fa81b1cfaa4b89 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 27 Jan 2015 00:13:46 +0100 Subject: [PATCH 140/481] Fix ReplayGain metadata type --- src/metadata/trackmetadata.cpp | 10 +++++----- src/metadata/trackmetadata.h | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index afcea616b2b..9ddc1b7597e 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -5,12 +5,12 @@ namespace Mixxx { /*static*/ const double TrackMetadata::BPM_UNDEFINED = 0.0; -/*static*/ const double TrackMetadata::BPM_MIN = 0.0; // exclusive lower bound -/*static*/ const double TrackMetadata::BPM_MAX = 300.0; // inclusive upper bound +/*static*/ const double TrackMetadata::BPM_MIN = 0.0; // lower bound (inclusive) +/*static*/ const double TrackMetadata::BPM_MAX = 300.0; // lower bound (inclusive) -/*static*/ const double TrackMetadata::REPLAYGAIN_UNDEFINED = 0.0f; -/*static*/ const double TrackMetadata::REPLAYGAIN_MIN = 0.0f; // exclusive lower bound -/*static*/ const double TrackMetadata::REPLAYGAIN_0DB = 1.0f; +/*static*/ const float TrackMetadata::REPLAYGAIN_UNDEFINED = 0.0f; +/*static*/ const float TrackMetadata::REPLAYGAIN_MIN = 0.0f; // lower bound (inclusive) +/*static*/ const float TrackMetadata::REPLAYGAIN_0DB = 1.0f; TrackMetadata::TrackMetadata() : m_channels(0), diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 3cfc07d56b8..30672ff8dcd 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -122,8 +122,8 @@ class TrackMetadata { // beats / minute static const double BPM_UNDEFINED; - static const double BPM_MIN; // exclusive lower bound - static const double BPM_MAX; // inclusive upper bound + static const double BPM_MIN; // lower bound (exclusive) + static const double BPM_MAX; // upper bound (inclusive) inline double getBpm() const { return m_bpm; } @@ -139,9 +139,9 @@ class TrackMetadata { static double parseBpmString(const QString& sBpm); bool setBpmString(const QString& sBpm); - static const double REPLAYGAIN_UNDEFINED; - static const double REPLAYGAIN_MIN; // exclusive lower bound - static const double REPLAYGAIN_0DB; + static const float REPLAYGAIN_UNDEFINED; + static const float REPLAYGAIN_MIN; // lower bound (exclusive) + static const float REPLAYGAIN_0DB; inline float getReplayGain() const { return m_replayGain; } From d7b834e704e57570768e39d7b6dafa5edda01fc8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 27 Jan 2015 00:16:33 +0100 Subject: [PATCH 141/481] Improve parsing of BPM and ReplayGain metadata --- src/metadata/trackmetadata.cpp | 129 ++++++++++++++++++++++----------- src/metadata/trackmetadata.h | 12 ++- 2 files changed, 93 insertions(+), 48 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 9ddc1b7597e..81af0d59fb7 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -12,69 +12,110 @@ namespace Mixxx { /*static*/ const float TrackMetadata::REPLAYGAIN_MIN = 0.0f; // lower bound (inclusive) /*static*/ const float TrackMetadata::REPLAYGAIN_0DB = 1.0f; -TrackMetadata::TrackMetadata() : - m_channels(0), - m_sampleRate(0), - m_bitrate(0), - m_duration(0), - m_bpm(BPM_UNDEFINED), - m_replayGain(REPLAYGAIN_UNDEFINED) { -} +namespace { -double TrackMetadata::parseBpmString(const QString& sBpm) { +double parseBpmString(const QString& sBpm, bool* pValid = 0) { + if (pValid) { + *pValid = false; + } if (sBpm.trimmed().isEmpty()) { - return BPM_UNDEFINED; + return TrackMetadata::BPM_UNDEFINED; } bool bpmValid = false; double bpm = sBpm.toDouble(&bpmValid); - if ((!bpmValid) || (BPM_MIN > bpm)) { + if (bpmValid) { + if (TrackMetadata::BPM_UNDEFINED == bpm) { + // special case + if (pValid) { + *pValid = true; + } + return bpm; + } + while (TrackMetadata::BPM_MAX < bpm) { + // TODO(XXX): Why? + qDebug() << "Scaling BPM:" << bpm; + bpm /= 10.0; + } + if (TrackMetadata::isBpmValid(bpm)) { + if (pValid) { + *pValid = true; + } + return bpm; + } else { + qDebug() << "BPM out of range:" << bpm; + } + } else { qDebug() << "Failed to parse BPM:" << sBpm; - return BPM_UNDEFINED; } - while (bpm > BPM_MAX) { - bpm /= 10.0; - } - return bpm; + return TrackMetadata::BPM_UNDEFINED; } -bool TrackMetadata::setBpmString(const QString& sBpm) { - const double bpm = parseBpmString(sBpm); - if (BPM_UNDEFINED != bpm) { - setBpm(parseBpmString(sBpm)); - return true; +float parseReplayGainDbString(QString sReplayGainDb, bool* pValid = 0) { + if (pValid) { + *pValid = false; + } + sReplayGainDb.remove("dB"); // TODO(XXX): Why? + if (sReplayGainDb.trimmed().isEmpty()) { + return TrackMetadata::REPLAYGAIN_UNDEFINED; + } + bool replayGainDbValid = false; + const float replayGainDb = sReplayGainDb.toFloat(&replayGainDbValid); + if (replayGainDbValid) { + if (TrackMetadata::REPLAYGAIN_UNDEFINED == replayGainDb) { + // special case + if (pValid) { + *pValid = true; + } + return replayGainDb; + } + // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. + // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by + // setting value to 0 (i.e. rescan via analyserrg) + if (TrackMetadata::REPLAYGAIN_0DB == replayGainDb) { + qDebug() << "Ignoring 0dB replay gain:" << replayGainDb; + return TrackMetadata::REPLAYGAIN_UNDEFINED; + } + if (TrackMetadata::isReplayGainValid(replayGainDb)) { + if (pValid) { + *pValid = true; + } + return replayGainDb; + } else { + qDebug() << "Replay gain out of range:" << replayGainDb; + } } else { - return false; + qDebug() << "Failed to parse replay gain:" << sReplayGainDb; } + return TrackMetadata::REPLAYGAIN_UNDEFINED; } -float TrackMetadata::parseReplayGainDbString(QString sReplayGainDb) { - sReplayGainDb.remove("dB"); - bool replayGainDbValid = false; - const double replayGainDb = sReplayGainDb.toDouble(&replayGainDbValid); - if (!replayGainDbValid) { - return REPLAYGAIN_UNDEFINED; - } - const float replayGain = db2ratio(replayGainDb); - if (REPLAYGAIN_MIN > replayGain) { - return REPLAYGAIN_UNDEFINED; - } - // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. - // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by - // setting value to 0 (i.e. rescan via analyserrg) - if (REPLAYGAIN_0DB == replayGain) { - return REPLAYGAIN_UNDEFINED; +} + +TrackMetadata::TrackMetadata() : + m_channels(0), + m_sampleRate(0), + m_bitrate(0), + m_duration(0), + m_bpm(BPM_UNDEFINED), + m_replayGain(REPLAYGAIN_UNDEFINED) { +} + +bool TrackMetadata::setBpmString(const QString& sBpm) { + bool bpmValid; + const double bpm = parseBpmString(sBpm, &bpmValid); + if (bpmValid) { + setBpm(bpm); } - return replayGain; + return bpmValid; } bool TrackMetadata::setReplayGainDbString(QString sReplayGainDb) { - const float replayGain = parseReplayGainDbString(sReplayGainDb); - if (REPLAYGAIN_UNDEFINED != replayGain) { + bool replayGainValid; + const float replayGain = parseReplayGainDbString(sReplayGainDb, &replayGainValid); + if (replayGainValid) { setReplayGain(replayGain); - return true; - } else { - return false; } + return replayGainValid; } } //namespace Mixxx diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 30672ff8dcd..94697caf03c 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -127,8 +127,11 @@ class TrackMetadata { inline double getBpm() const { return m_bpm; } + inline static bool isBpmValid(double bpm) { + return (BPM_MIN < bpm) && (BPM_MAX >= bpm); + } inline bool isBpmValid() const { - return (BPM_MIN < getBpm()) && (BPM_MAX >= getBpm()); + return isBpmValid(getBpm()); } inline void setBpm(double bpm) { m_bpm = bpm; @@ -136,7 +139,6 @@ class TrackMetadata { inline void resetBpm() { m_bpm = BPM_UNDEFINED; } - static double parseBpmString(const QString& sBpm); bool setBpmString(const QString& sBpm); static const float REPLAYGAIN_UNDEFINED; @@ -145,8 +147,11 @@ class TrackMetadata { inline float getReplayGain() const { return m_replayGain; } + inline static bool isReplayGainValid(float replayGain) { + return REPLAYGAIN_MIN < replayGain; + } inline bool isReplayGainValid() const { - return REPLAYGAIN_MIN < getReplayGain(); + return isReplayGainValid(getReplayGain()); } inline void setReplayGain(float replayGain) { m_replayGain = replayGain; @@ -154,7 +159,6 @@ class TrackMetadata { inline void resetReplayGain() { m_replayGain = REPLAYGAIN_UNDEFINED; } - static float parseReplayGainDbString(QString sReplayGainDb); // in dB bool setReplayGainDbString(QString sReplayGainDb); // in dB private: From 45c65b4075ecc662fbbdb1dda5162d3e8ba008f6 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 27 Jan 2015 17:16:46 +0100 Subject: [PATCH 142/481] Fixes and unit tests for parsing BPM and ReplayGain --- src/metadata/trackmetadata.cpp | 35 ++++++---- src/test/metadatatest.cpp | 116 +++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 src/test/metadatatest.cpp diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 81af0d59fb7..a1970dc8970 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -54,32 +54,41 @@ float parseReplayGainDbString(QString sReplayGainDb, bool* pValid = 0) { if (pValid) { *pValid = false; } - sReplayGainDb.remove("dB"); // TODO(XXX): Why? - if (sReplayGainDb.trimmed().isEmpty()) { + QString normalizedReplayGainDb(sReplayGainDb.toLower().trimmed()); + const int plusIndex = normalizedReplayGainDb.indexOf('+'); + if (0 == plusIndex) { + // strip leading "+" + normalizedReplayGainDb = normalizedReplayGainDb.mid(plusIndex + 1).trimmed(); + } + const int dbIndex = normalizedReplayGainDb.indexOf("db"); + if ((0 <= dbIndex) && ((normalizedReplayGainDb.length() - 2) == dbIndex)) { + // strip trailing "db" + normalizedReplayGainDb = normalizedReplayGainDb.left(dbIndex).trimmed(); + } + if (normalizedReplayGainDb.isEmpty()) { return TrackMetadata::REPLAYGAIN_UNDEFINED; } bool replayGainDbValid = false; - const float replayGainDb = sReplayGainDb.toFloat(&replayGainDbValid); + const double replayGainDb = normalizedReplayGainDb.toDouble(&replayGainDbValid); if (replayGainDbValid) { - if (TrackMetadata::REPLAYGAIN_UNDEFINED == replayGainDb) { - // special case - if (pValid) { - *pValid = true; - } - return replayGainDb; - } + const float replayGain = db2ratio(replayGainDb); + DEBUG_ASSERT(TrackMetadata::REPLAYGAIN_UNDEFINED != replayGain); // impossible // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by // setting value to 0 (i.e. rescan via analyserrg) - if (TrackMetadata::REPLAYGAIN_0DB == replayGainDb) { + if (TrackMetadata::REPLAYGAIN_0DB == replayGain) { + // special case qDebug() << "Ignoring 0dB replay gain:" << replayGainDb; + if (pValid) { + *pValid = true; + } return TrackMetadata::REPLAYGAIN_UNDEFINED; } - if (TrackMetadata::isReplayGainValid(replayGainDb)) { + if (TrackMetadata::isReplayGainValid(replayGain)) { if (pValid) { *pValid = true; } - return replayGainDb; + return replayGain; } else { qDebug() << "Replay gain out of range:" << replayGainDb; } diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp new file mode 100644 index 00000000000..2756f244f29 --- /dev/null +++ b/src/test/metadatatest.cpp @@ -0,0 +1,116 @@ +#include + +#include "metadata/trackmetadata.h" +#include "util/math.h" + +#include + +namespace { + +class MetadataTest : public testing::Test { + protected: + + MetadataTest() { + } + + virtual void SetUp() { + } + + virtual void TearDown() { + } + + void parseBpm(double initialValue, QString inputValue, bool expectedResult, double expectedValue) { + //qDebug() << "parseBpm" << initialValue << inputValue << expectedResult << expectedValue; + + Mixxx::TrackMetadata trackMetadata; + trackMetadata.setBpm(initialValue); + + const bool actualResult = trackMetadata.setBpmString(inputValue); + + EXPECT_EQ(expectedResult, actualResult); + EXPECT_DOUBLE_EQ(trackMetadata.getBpm(), expectedValue); + } + + void parseReplayGainDb(float initialValue, QString inputValue, bool expectedResult, float expectedValue) { + //qDebug() << "parseReplayGainDb" << initialValue << inputValue << expectedResult << expectedValue; + + Mixxx::TrackMetadata trackMetadata; + trackMetadata.setReplayGain(initialValue); + + const bool actualResult = trackMetadata.setReplayGainDbString(inputValue); + + EXPECT_EQ(expectedResult, actualResult); + EXPECT_FLOAT_EQ(trackMetadata.getReplayGain(), expectedValue); + } +}; + +TEST_F(MetadataTest, ParseBpmPrecision) { + parseBpm(100.0, "128.1234", true, 128.1234); // 4 fractional digits +} + +TEST_F(MetadataTest, ParseBpmValidRange) { + for (int bpm100 = int(Mixxx::TrackMetadata::BPM_MIN) * 100; int(Mixxx::TrackMetadata::BPM_MAX) * 100 >= bpm100; ++bpm100) { + const double expectedValue = bpm100 / 100.0; + const double initialValues[] = { + Mixxx::TrackMetadata::BPM_UNDEFINED, + 128.5 + }; + const QString inputValues[] = { + QString("%1").arg(expectedValue), + QString(" %1 ").arg(expectedValue), + }; + for (size_t i = 0; i < sizeof(initialValues) / sizeof(initialValues[0]); ++i) { + for (size_t j = 0; j < sizeof(inputValues) / sizeof(inputValues[0]); ++j) { + parseBpm(initialValues[i], inputValues[j], true, expectedValue); + } + } + } +} + +TEST_F(MetadataTest, ParseBpmDecimalScaling) { + parseBpm(100.0, "345678", true, 34.5678); + parseBpm(100.0, "2345678", true, 234.5678); +} + +TEST_F(MetadataTest, ParseBpmInvalid) { + parseBpm(Mixxx::TrackMetadata::BPM_UNDEFINED, "", false, Mixxx::TrackMetadata::BPM_UNDEFINED); + parseBpm(123.45, "abcde", false, 123.45); +} + +TEST_F(MetadataTest, ParseReplayGainDbValidRange) { + for (int replayGainDb = -100; 100 >= replayGainDb; ++replayGainDb) { + const float initialValues[] = { + Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED, + 0.5f + }; + const QString inputValues[] = { + QString("%1 ").arg(replayGainDb), + QString(" %1dB ").arg(replayGainDb), + QString(" %1 DB ").arg(replayGainDb), + QString(" %1db ").arg(replayGainDb) + }; + float expectedValue; + if (0 != replayGainDb) { + // regular case + expectedValue = db2ratio(double(replayGainDb)); + } else { + // special case: 0 dB -> undefined + expectedValue = Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED; + } + for (size_t i = 0; i < sizeof(initialValues) / sizeof(initialValues[0]); ++i) { + for (size_t j = 0; j < sizeof(inputValues) / sizeof(inputValues[0]); ++j) { + parseReplayGainDb(initialValues[i], inputValues[j], true, expectedValue); + if (0 <= replayGainDb) { + parseReplayGainDb(initialValues[i], QString(" + ") + inputValues[j], true, expectedValue); + } + } + } + } +} + +TEST_F(MetadataTest, ParseReplayGainDbInvalid) { + parseReplayGainDb(Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED, "", false, Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED); + parseReplayGainDb(0.5, "abcde", false, 0.5); +} + +} // namespace From 3e9ffdf69359a5d24f44f68eb2fdff5a6672acc4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 17:00:14 +0100 Subject: [PATCH 143/481] Do not read album replay gain --- src/metadata/trackmetadatataglib.cpp | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index c497db23924..650409b3f02 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -188,11 +188,7 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, if (replaygainFrame && replaygainFrame->fieldList().size() >= 2) { const QString desc( toQString(replaygainFrame->description()).toLower()); - if (desc == "replaygain_album_gain") { - pTrackMetadata->setReplayGainDbString( - toQString(replaygainFrame->fieldList()[1])); - } - // Prefer track gain over album gain. + // Only read track gain (not album gain) if (desc == "replaygain_track_gain") { pTrackMetadata->setReplayGainDbString( toQString(replaygainFrame->fieldList()[1])); @@ -245,11 +241,7 @@ void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { pTrackMetadata->setBpmString(toQString(tag.itemListMap()["BPM"])); } - if (tag.itemListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pTrackMetadata->setReplayGainDbString( - toQString(tag.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); - } - //Prefer track gain over album gain. + // Only read track gain (not album gain) if (tag.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { pTrackMetadata->setReplayGainDbString( toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); @@ -307,11 +299,7 @@ void readXiphComment(TrackMetadata* pTrackMetadata, toQStringFirst(tag.fieldListMap()["TEMPO"])); } - if (tag.fieldListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pTrackMetadata->setReplayGainDbString( - toQStringFirst(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); - } - //Prefer track gain over album gain. + // Only read track gain (not album gain) if (tag.fieldListMap().contains("REPLAYGAIN_TRACK_GAIN")) { pTrackMetadata->setReplayGainDbString( toQStringFirst(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); @@ -423,15 +411,10 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:KEY"])); } - // Apparently iTunes stores replaygain in this property. - if (tag.itemListMap().contains( - "----:com.apple.iTunes:replaygain_album_gain")) { - // TODO(XXX) find tracks with this property and check what it looks - } - //Prefer track gain over album gain. + // Only read track gain (not album gain) if (tag.itemListMap().contains( "----:com.apple.iTunes:replaygain_track_gain")) { - // TODO(XXX) find tracks with this property and check what it looks + // TODO(XXX) find tracks with this property and check what it looks like } } From 7c91a4e1494bf51929a043183f098a45d548d435 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 17:01:31 +0100 Subject: [PATCH 144/481] Format BPM and replay gain metadata as strings --- src/metadata/trackmetadata.cpp | 23 +++++++++++++++++++++-- src/metadata/trackmetadata.h | 2 ++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index a1970dc8970..d321592b37b 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -50,17 +50,20 @@ double parseBpmString(const QString& sBpm, bool* pValid = 0) { return TrackMetadata::BPM_UNDEFINED; } +const QString REPLAYGAIN_UNIT("dB"); +const QString REPLAYGAIN_SUFFIX(" " + REPLAYGAIN_UNIT); + float parseReplayGainDbString(QString sReplayGainDb, bool* pValid = 0) { if (pValid) { *pValid = false; } - QString normalizedReplayGainDb(sReplayGainDb.toLower().trimmed()); + QString normalizedReplayGainDb(sReplayGainDb.trimmed()); const int plusIndex = normalizedReplayGainDb.indexOf('+'); if (0 == plusIndex) { // strip leading "+" normalizedReplayGainDb = normalizedReplayGainDb.mid(plusIndex + 1).trimmed(); } - const int dbIndex = normalizedReplayGainDb.indexOf("db"); + const int dbIndex = normalizedReplayGainDb.indexOf(REPLAYGAIN_UNIT, Qt::CaseInsensitive); if ((0 <= dbIndex) && ((normalizedReplayGainDb.length() - 2) == dbIndex)) { // strip trailing "db" normalizedReplayGainDb = normalizedReplayGainDb.left(dbIndex).trimmed(); @@ -118,6 +121,14 @@ bool TrackMetadata::setBpmString(const QString& sBpm) { return bpmValid; } +QString TrackMetadata::getBpmString() const { + if (isBpmValid()) { + return QString::number(getBpm(), 'f'); + } else { + return QString(); + } +} + bool TrackMetadata::setReplayGainDbString(QString sReplayGainDb) { bool replayGainValid; const float replayGain = parseReplayGainDbString(sReplayGainDb, &replayGainValid); @@ -127,4 +138,12 @@ bool TrackMetadata::setReplayGainDbString(QString sReplayGainDb) { return replayGainValid; } +QString TrackMetadata::getReplayGainDbString() const { + if (isReplayGainValid()) { + return QString::number(ratio2db(getReplayGain()), 'f') + REPLAYGAIN_SUFFIX; + } else { + return QString(); + } +} + } //namespace Mixxx diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 94697caf03c..a3c2130de72 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -140,6 +140,7 @@ class TrackMetadata { m_bpm = BPM_UNDEFINED; } bool setBpmString(const QString& sBpm); + QString getBpmString() const; static const float REPLAYGAIN_UNDEFINED; static const float REPLAYGAIN_MIN; // lower bound (exclusive) @@ -160,6 +161,7 @@ class TrackMetadata { m_replayGain = REPLAYGAIN_UNDEFINED; } bool setReplayGainDbString(QString sReplayGainDb); // in dB + QString getReplayGainDbString() const; // in dB private: QString m_artist; From 6b6abd05de94ebf736416a1d2173c4b6753e93f8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 17:02:06 +0100 Subject: [PATCH 145/481] Improve reading/writing of tags --- src/metadata/trackmetadata.cpp | 54 +++--- src/metadata/trackmetadatataglib.cpp | 265 +++++++++++++-------------- src/test/metadatatest.cpp | 8 +- 3 files changed, 169 insertions(+), 158 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index d321592b37b..ba8ce59f3dc 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -14,7 +14,15 @@ namespace Mixxx { namespace { -double parseBpmString(const QString& sBpm, bool* pValid = 0) { +QString formatBpm(double bpm) { + if (TrackMetadata::isBpmValid(bpm)) { + return QString::number(bpm, 'f'); + } else { + return QString(); + } +} + +double parseBpm(const QString& sBpm, bool* pValid = 0) { if (pValid) { *pValid = false; } @@ -33,7 +41,7 @@ double parseBpmString(const QString& sBpm, bool* pValid = 0) { } while (TrackMetadata::BPM_MAX < bpm) { // TODO(XXX): Why? - qDebug() << "Scaling BPM:" << bpm; + qDebug() << "Scaling BPM value:" << bpm; bpm /= 10.0; } if (TrackMetadata::isBpmValid(bpm)) { @@ -42,7 +50,7 @@ double parseBpmString(const QString& sBpm, bool* pValid = 0) { } return bpm; } else { - qDebug() << "BPM out of range:" << bpm; + qDebug() << "Invalid BPM value:" << sBpm << "->" << bpm; } } else { qDebug() << "Failed to parse BPM:" << sBpm; @@ -53,7 +61,15 @@ double parseBpmString(const QString& sBpm, bool* pValid = 0) { const QString REPLAYGAIN_UNIT("dB"); const QString REPLAYGAIN_SUFFIX(" " + REPLAYGAIN_UNIT); -float parseReplayGainDbString(QString sReplayGainDb, bool* pValid = 0) { +QString formatReplayGainDb(double replayGain) { + if (TrackMetadata::isReplayGainValid(replayGain)) { + return QString::number(ratio2db(replayGain), 'f') + REPLAYGAIN_SUFFIX; + } else { + return QString(); + } +} + +double parseReplayGainDb(QString sReplayGainDb, bool* pValid = 0) { if (pValid) { *pValid = false; } @@ -63,10 +79,10 @@ float parseReplayGainDbString(QString sReplayGainDb, bool* pValid = 0) { // strip leading "+" normalizedReplayGainDb = normalizedReplayGainDb.mid(plusIndex + 1).trimmed(); } - const int dbIndex = normalizedReplayGainDb.indexOf(REPLAYGAIN_UNIT, Qt::CaseInsensitive); - if ((0 <= dbIndex) && ((normalizedReplayGainDb.length() - 2) == dbIndex)) { - // strip trailing "db" - normalizedReplayGainDb = normalizedReplayGainDb.left(dbIndex).trimmed(); + const int unitIndex = normalizedReplayGainDb.lastIndexOf(REPLAYGAIN_UNIT, -1, Qt::CaseInsensitive); + if ((0 <= unitIndex) && ((normalizedReplayGainDb.length() - 2) == unitIndex)) { + // strip trailing unit suffix + normalizedReplayGainDb = normalizedReplayGainDb.left(unitIndex).trimmed(); } if (normalizedReplayGainDb.isEmpty()) { return TrackMetadata::REPLAYGAIN_UNDEFINED; @@ -74,14 +90,14 @@ float parseReplayGainDbString(QString sReplayGainDb, bool* pValid = 0) { bool replayGainDbValid = false; const double replayGainDb = normalizedReplayGainDb.toDouble(&replayGainDbValid); if (replayGainDbValid) { - const float replayGain = db2ratio(replayGainDb); + const double replayGain = db2ratio(replayGainDb); DEBUG_ASSERT(TrackMetadata::REPLAYGAIN_UNDEFINED != replayGain); // impossible // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by // setting value to 0 (i.e. rescan via analyserrg) if (TrackMetadata::REPLAYGAIN_0DB == replayGain) { // special case - qDebug() << "Ignoring 0dB replay gain:" << replayGainDb; + qDebug() << "Ignoring replay gain:" << formatReplayGainDb(replayGain); if (pValid) { *pValid = true; } @@ -93,7 +109,7 @@ float parseReplayGainDbString(QString sReplayGainDb, bool* pValid = 0) { } return replayGain; } else { - qDebug() << "Replay gain out of range:" << replayGainDb; + qDebug() << "Invalid replay gain value:" << sReplayGainDb << " -> "<< replayGain; } } else { qDebug() << "Failed to parse replay gain:" << sReplayGainDb; @@ -114,7 +130,7 @@ TrackMetadata::TrackMetadata() : bool TrackMetadata::setBpmString(const QString& sBpm) { bool bpmValid; - const double bpm = parseBpmString(sBpm, &bpmValid); + const double bpm = parseBpm(sBpm, &bpmValid); if (bpmValid) { setBpm(bpm); } @@ -122,16 +138,12 @@ bool TrackMetadata::setBpmString(const QString& sBpm) { } QString TrackMetadata::getBpmString() const { - if (isBpmValid()) { - return QString::number(getBpm(), 'f'); - } else { - return QString(); - } + return formatBpm(getBpm()); } bool TrackMetadata::setReplayGainDbString(QString sReplayGainDb) { bool replayGainValid; - const float replayGain = parseReplayGainDbString(sReplayGainDb, &replayGainValid); + const float replayGain = parseReplayGainDb(sReplayGainDb, &replayGainValid); if (replayGainValid) { setReplayGain(replayGain); } @@ -139,11 +151,7 @@ bool TrackMetadata::setReplayGainDbString(QString sReplayGainDb) { } QString TrackMetadata::getReplayGainDbString() const { - if (isReplayGainValid()) { - return QString::number(ratio2db(getReplayGain()), 'f') + REPLAYGAIN_SUFFIX; - } else { - return QString(); - } + return formatReplayGainDb(getReplayGain()); } } //namespace Mixxx diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 650409b3f02..50f4264640a 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -77,25 +77,6 @@ inline TagLib::String toTagLibString(const QString& str) { return TagLib::String(qba.constData(), TagLib::String::UTF8); } -template -inline QString formatString(const T& value) { - return QString("%1").arg(value); -} - -template -inline QString formatString(const T& value, const T& emptyValue) { - if (value == emptyValue) { - return QString(); /// empty string - } else { - return formatString(value); - } -} - -template -inline QString formatBpmString(const T& bpm) { - return formatString(bpm, TrackMetadata::BPM_UNDEFINED); -} - } // anonymous namespace bool readAudioProperties(TrackMetadata* pTrackMetadata, @@ -135,12 +116,12 @@ void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { int iYear = tag.year(); if (iYear > 0) { - pTrackMetadata->setYear(QString("%1").arg(iYear)); + pTrackMetadata->setYear(QString::number(iYear)); } int iTrack = tag.track(); if (iTrack > 0) { - pTrackMetadata->setTrackNumber(QString("%1").arg(iTrack)); + pTrackMetadata->setTrackNumber(QString::number(iTrack)); } if (kDebugMetadata) { @@ -168,34 +149,6 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, readTag(pTrackMetadata, tag); - const TagLib::ID3v2::FrameList bpmFrame(tag.frameListMap()["TBPM"]); - if (!bpmFrame.isEmpty()) { - pTrackMetadata->setBpmString(toQStringFirst(bpmFrame)); - } - - const TagLib::ID3v2::FrameList keyFrame(tag.frameListMap()["TKEY"]); - if (!keyFrame.isEmpty()) { - pTrackMetadata->setKey(toQStringFirst(keyFrame)); - } - - // Foobar2000-style ID3v2.3.0 tags - // TODO: Check if everything is ok. - TagLib::ID3v2::FrameList textFrames(tag.frameListMap()["TXXX"]); - for (TagLib::ID3v2::FrameList::ConstIterator it(textFrames.begin()); - it != textFrames.end(); ++it) { - TagLib::ID3v2::UserTextIdentificationFrame* replaygainFrame = - dynamic_cast(*it); - if (replaygainFrame && replaygainFrame->fieldList().size() >= 2) { - const QString desc( - toQString(replaygainFrame->description()).toLower()); - // Only read track gain (not album gain) - if (desc == "replaygain_track_gain") { - pTrackMetadata->setReplayGainDbString( - toQString(replaygainFrame->fieldList()[1])); - } - } - } - const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()["TPE2"]); if (!albumArtistFrame.isEmpty()) { pTrackMetadata->setAlbumArtist(toQStringFirst(albumArtistFrame)); @@ -223,6 +176,34 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, if (!recordingDateFrame.isEmpty()) { pTrackMetadata->setYear(toQStringFirst(recordingDateFrame)); } + + const TagLib::ID3v2::FrameList bpmFrame(tag.frameListMap()["TBPM"]); + if (!bpmFrame.isEmpty()) { + pTrackMetadata->setBpmString(toQStringFirst(bpmFrame)); + } + + // Foobar2000-style ID3v2.3.0 tags + // TODO: Check if everything is ok. + TagLib::ID3v2::FrameList textFrames(tag.frameListMap()["TXXX"]); + for (TagLib::ID3v2::FrameList::ConstIterator it(textFrames.begin()); + it != textFrames.end(); ++it) { + TagLib::ID3v2::UserTextIdentificationFrame* replaygainFrame = + dynamic_cast(*it); + if (replaygainFrame && replaygainFrame->fieldList().size() >= 2) { + const QString desc( + toQString(replaygainFrame->description())); + // Only read track gain (not album gain) + if (0 == desc.compare("REPLAYGAIN_TRACK_GAIN", Qt::CaseInsensitive)) { + pTrackMetadata->setReplayGainDbString( + toQString(replaygainFrame->fieldList()[1])); + } + } + } + + const TagLib::ID3v2::FrameList keyFrame(tag.frameListMap()["TKEY"]); + if (!keyFrame.isEmpty()) { + pTrackMetadata->setKey(toQStringFirst(keyFrame)); + } } void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { @@ -237,16 +218,6 @@ void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { readTag(pTrackMetadata, tag); - if (tag.itemListMap().contains("BPM")) { - pTrackMetadata->setBpmString(toQString(tag.itemListMap()["BPM"])); - } - - // Only read track gain (not album gain) - if (tag.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainDbString( - toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); - } - if (tag.itemListMap().contains("Album Artist")) { pTrackMetadata->setAlbumArtist( toQString(tag.itemListMap()["Album Artist"])); @@ -263,6 +234,16 @@ void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { if (tag.itemListMap().contains("Year")) { pTrackMetadata->setYear(toQString(tag.itemListMap()["Year"])); } + + if (tag.itemListMap().contains("BPM")) { + pTrackMetadata->setBpmString(toQString(tag.itemListMap()["BPM"])); + } + + // Only read track gain (not album gain) + if (tag.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { + pTrackMetadata->setReplayGainDbString( + toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); + } } void readXiphComment(TrackMetadata* pTrackMetadata, @@ -288,6 +269,37 @@ void readXiphComment(TrackMetadata* pTrackMetadata, toQStringFirst(tag.fieldListMap()["COMMENT"])); } + if (tag.fieldListMap().contains("ALBUMARTIST")) { + pTrackMetadata->setAlbumArtist( + toQStringFirst(tag.fieldListMap()["ALBUMARTIST"])); + } + if (pTrackMetadata->getAlbumArtist().isEmpty() + && tag.fieldListMap().contains("ALBUM_ARTIST")) { + // try alternative field name + pTrackMetadata->setAlbumArtist( + toQStringFirst(tag.fieldListMap()["ALBUM_ARTIST"])); + } + if (pTrackMetadata->getAlbumArtist().isEmpty() + && tag.fieldListMap().contains("ALBUM ARTIST")) { + // try alternative field name + pTrackMetadata->setAlbumArtist( + toQStringFirst(tag.fieldListMap()["ALBUM ARTIST"])); + } + + if (tag.fieldListMap().contains("COMPOSER")) { + pTrackMetadata->setComposer( + toQStringFirst(tag.fieldListMap()["COMPOSER"])); + } + + if (tag.fieldListMap().contains("GROUPING")) { + pTrackMetadata->setGrouping( + toQStringFirst(tag.fieldListMap()["GROUPING"])); + } + + if (tag.fieldListMap().contains("DATE")) { + pTrackMetadata->setYear(toQStringFirst(tag.fieldListMap()["DATE"])); + } + // Some tags use "BPM" so check for that. if (tag.fieldListMap().contains("BPM")) { pTrackMetadata->setBpmString(toQStringFirst(tag.fieldListMap()["BPM"])); @@ -316,43 +328,11 @@ void readXiphComment(TrackMetadata* pTrackMetadata, if (tag.fieldListMap().contains("KEY")) { pTrackMetadata->setKey(toQStringFirst(tag.fieldListMap()["KEY"])); } - if (pTrackMetadata->getKey().isEmpty() - && tag.fieldListMap().contains("INITIALKEY")) { - // try alternative field name + if (tag.fieldListMap().contains("INITIALKEY")) { + // This is the preferred field for storing the musical key. pTrackMetadata->setKey( toQStringFirst(tag.fieldListMap()["INITIALKEY"])); } - - if (tag.fieldListMap().contains("ALBUMARTIST")) { - pTrackMetadata->setAlbumArtist( - toQStringFirst(tag.fieldListMap()["ALBUMARTIST"])); - } - if (pTrackMetadata->getAlbumArtist().isEmpty() - && tag.fieldListMap().contains("ALBUM_ARTIST")) { - // try alternative field name - pTrackMetadata->setAlbumArtist( - toQStringFirst(tag.fieldListMap()["ALBUM_ARTIST"])); - } - if (pTrackMetadata->getAlbumArtist().isEmpty() - && tag.fieldListMap().contains("ALBUM ARTIST")) { - // try alternative field name - pTrackMetadata->setAlbumArtist( - toQStringFirst(tag.fieldListMap()["ALBUM ARTIST"])); - } - - if (tag.fieldListMap().contains("COMPOSER")) { - pTrackMetadata->setComposer( - toQStringFirst(tag.fieldListMap()["COMPOSER"])); - } - - if (tag.fieldListMap().contains("GROUPING")) { - pTrackMetadata->setGrouping( - toQStringFirst(tag.fieldListMap()["GROUPING"])); - } - - if (tag.fieldListMap().contains("DATE")) { - pTrackMetadata->setYear(toQStringFirst(tag.fieldListMap()["DATE"])); - } } void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { @@ -367,21 +347,6 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { readTag(pTrackMetadata, tag); - // Get BPM - if (tag.itemListMap().contains("tmpo")) { - // Read the BPM as an integer value. - pTrackMetadata->setBpm(tag.itemListMap()["tmpo"].toInt()); - } - if (tag.itemListMap().contains("----:com.apple.iTunes:BPM")) { - // This is the preferred field for storing the BPM - // with fractional digits as a floating-point value. - // If this field contains a valid value the integer - // BPM value that might have been read before is - // overwritten. - pTrackMetadata->setBpmString( - toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:BPM"])); - } - // Get Album Artist if (tag.itemListMap().contains("aART")) { pTrackMetadata->setAlbumArtist( @@ -405,10 +370,19 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { pTrackMetadata->setYear(toQStringFirst(tag.itemListMap()["\251day"])); } - // Get KEY (conforms to Rapid Evolution) - if (tag.itemListMap().contains("----:com.apple.iTunes:KEY")) { - pTrackMetadata->setKey( - toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:KEY"])); + // Get BPM + if (tag.itemListMap().contains("tmpo")) { + // Read the BPM as an integer value. + pTrackMetadata->setBpm(tag.itemListMap()["tmpo"].toInt()); + } + if (tag.itemListMap().contains("----:com.apple.iTunes:BPM")) { + // This is the preferred field for storing the BPM + // with fractional digits as a floating-point value. + // If this field contains a valid value the integer + // BPM value that might have been read before is + // overwritten. + pTrackMetadata->setBpmString( + toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:BPM"])); } // Only read track gain (not album gain) @@ -416,6 +390,18 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { "----:com.apple.iTunes:replaygain_track_gain")) { // TODO(XXX) find tracks with this property and check what it looks like } + + // Read musical key (conforms to Rapid Evolution) + if (tag.itemListMap().contains("----:com.apple.iTunes:KEY")) { + pTrackMetadata->setKey( + toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:KEY"])); + } + // Read musical key (conforms to MixedInKey, Serato, Traktor) + if (tag.itemListMap().contains("----:com.apple.iTunes:initialkey")) { + // This is the preferred field for storing the musical key! + pTrackMetadata->setKey( + toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:initialkey"])); + } } QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag) { @@ -548,7 +534,9 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, writeID3v2TextIdentificationFrame(pTag, "TPE2", trackMetadata.getAlbumArtist()); writeID3v2TextIdentificationFrame(pTag, "TBPM", - formatBpmString(trackMetadata.getBpm())); + trackMetadata.getBpmString()); + writeID3v2TextIdentificationFrame(pTag, "TBPM", + trackMetadata.getBpmString()); writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); writeID3v2TextIdentificationFrame(pTag, "TCOM", trackMetadata.getComposer()); @@ -557,8 +545,9 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, if (4 <= pHeader->majorVersion()) { // ID3v2.4.0: TDRC replaces TYER + TDAT writeID3v2TextIdentificationFrame(pTag, "TDRC", - formatString(trackMetadata.getYear())); + trackMetadata.getYear()); } + // TODO(uklotzde): Write TXXX - REPLAYGAIN_TRACK_GAIN return true; } @@ -571,15 +560,18 @@ bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { // Write common metadata writeTag(pTag, trackMetadata); - pTag->addValue("BPM", - toTagLibString(formatBpmString(trackMetadata.getBpm())), true); pTag->addValue("Album Artist", toTagLibString(trackMetadata.getAlbumArtist()), true); - pTag->addValue("Composer", toTagLibString(trackMetadata.getComposer()), - true); - pTag->addValue("Grouping", toTagLibString(trackMetadata.getGrouping()), - true); - pTag->addValue("Year", toTagLibString(trackMetadata.getYear()), true); + pTag->addValue("Composer", + toTagLibString(trackMetadata.getComposer()), true); + pTag->addValue("Grouping", + toTagLibString(trackMetadata.getGrouping()), true); + pTag->addValue("Year", + toTagLibString(trackMetadata.getYear()), true); + pTag->addValue("BPM", + toTagLibString(trackMetadata.getBpmString()), true); + pTag->addValue("REPLAYGAIN_TRACK_GAIN", + toTagLibString(trackMetadata.getReplayGainDbString()), true); return true; } @@ -600,27 +592,31 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, pTag->addField("ALBUMARTIST", toTagLibString(trackMetadata.getAlbumArtist())); + pTag->removeField("COMPOSER"); + pTag->addField("COMPOSER", toTagLibString(trackMetadata.getComposer())); + + pTag->removeField("GROUPING"); + pTag->addField("GROUPING", toTagLibString(trackMetadata.getGrouping())); + + pTag->removeField("DATE"); + pTag->addField("DATE", toTagLibString(trackMetadata.getYear())); + // Some tools use "BPM" so write that. pTag->removeField("BPM"); pTag->addField("BPM", - toTagLibString(formatBpmString(trackMetadata.getBpm()))); + toTagLibString(trackMetadata.getBpmString())); pTag->removeField("TEMPO"); pTag->addField("TEMPO", - toTagLibString(formatBpmString(trackMetadata.getBpm()))); + toTagLibString(trackMetadata.getBpmString())); pTag->removeField("INITIALKEY"); pTag->addField("INITIALKEY", toTagLibString(trackMetadata.getKey())); pTag->removeField("KEY"); pTag->addField("KEY", toTagLibString(trackMetadata.getKey())); - pTag->removeField("COMPOSER"); - pTag->addField("COMPOSER", toTagLibString(trackMetadata.getComposer())); - - pTag->removeField("GROUPING"); - pTag->addField("GROUPING", toTagLibString(trackMetadata.getGrouping())); - - pTag->removeField("DATE"); - pTag->addField("DATE", toTagLibString(trackMetadata.getYear())); + pTag->removeField("REPLAYGAIN_TRACK_GAIN"); + pTag->addField("REPLAYGAIN_TRACK_GAIN", + toTagLibString(trackMetadata.getReplayGainDbString())); return true; } @@ -662,8 +658,11 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { pTag->itemListMap().erase("tmpo"); } writeMP4Atom(pTag, "----:com.apple.iTunes:BPM", - formatBpmString(trackMetadata.getBpm())); - writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", trackMetadata.getKey()); + trackMetadata.getBpmString()); + writeMP4Atom(pTag, "----:com.apple.iTunes:initialkey", + trackMetadata.getKey()); + writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", + trackMetadata.getKey()); return true; } diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index 2756f244f29..c829b1e2489 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -19,7 +19,7 @@ class MetadataTest : public testing::Test { virtual void TearDown() { } - void parseBpm(double initialValue, QString inputValue, bool expectedResult, double expectedValue) { + double parseBpm(double initialValue, QString inputValue, bool expectedResult, double expectedValue) { //qDebug() << "parseBpm" << initialValue << inputValue << expectedResult << expectedValue; Mixxx::TrackMetadata trackMetadata; @@ -29,9 +29,11 @@ class MetadataTest : public testing::Test { EXPECT_EQ(expectedResult, actualResult); EXPECT_DOUBLE_EQ(trackMetadata.getBpm(), expectedValue); + + return actualResult; } - void parseReplayGainDb(float initialValue, QString inputValue, bool expectedResult, float expectedValue) { + float parseReplayGainDb(float initialValue, QString inputValue, bool expectedResult, float expectedValue) { //qDebug() << "parseReplayGainDb" << initialValue << inputValue << expectedResult << expectedValue; Mixxx::TrackMetadata trackMetadata; @@ -41,6 +43,8 @@ class MetadataTest : public testing::Test { EXPECT_EQ(expectedResult, actualResult); EXPECT_FLOAT_EQ(trackMetadata.getReplayGain(), expectedValue); + + return actualResult; } }; From 9559f326b20d637422dcc82db3d1d97ee29ff1db Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 17:02:50 +0100 Subject: [PATCH 146/481] Read/write replay gain for MP4 files --- src/metadata/trackmetadatataglib.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 50f4264640a..9da44fcdc86 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -388,7 +388,8 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { // Only read track gain (not album gain) if (tag.itemListMap().contains( "----:com.apple.iTunes:replaygain_track_gain")) { - // TODO(XXX) find tracks with this property and check what it looks like + pTrackMetadata->setReplayGainDbString( + toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:replaygain_track_gain"])); } // Read musical key (conforms to Rapid Evolution) @@ -659,6 +660,8 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { } writeMP4Atom(pTag, "----:com.apple.iTunes:BPM", trackMetadata.getBpmString()); + writeMP4Atom(pTag, "----:com.apple.iTunes:replaygain_track_gain", + trackMetadata.getReplayGainDbString()); writeMP4Atom(pTag, "----:com.apple.iTunes:initialkey", trackMetadata.getKey()); writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", From 7d8f7bdae91d70a853cd5b3ce32c8d1f989cadff Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 17:03:32 +0100 Subject: [PATCH 147/481] Move parsing/formatting of metadata into static functions --- src/metadata/trackmetadata.cpp | 122 +++++++++++---------------- src/metadata/trackmetadata.h | 28 +++--- src/metadata/trackmetadatataglib.cpp | 55 +++++++----- src/test/metadatatest.cpp | 72 ++++++++-------- 4 files changed, 135 insertions(+), 142 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index ba8ce59f3dc..38fcff41865 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -8,39 +8,30 @@ namespace Mixxx { /*static*/ const double TrackMetadata::BPM_MIN = 0.0; // lower bound (inclusive) /*static*/ const double TrackMetadata::BPM_MAX = 300.0; // lower bound (inclusive) -/*static*/ const float TrackMetadata::REPLAYGAIN_UNDEFINED = 0.0f; -/*static*/ const float TrackMetadata::REPLAYGAIN_MIN = 0.0f; // lower bound (inclusive) -/*static*/ const float TrackMetadata::REPLAYGAIN_0DB = 1.0f; +/*static*/ const double TrackMetadata::REPLAYGAIN_UNDEFINED = 0.0; +/*static*/ const double TrackMetadata::REPLAYGAIN_MIN = 0.0; // lower bound (inclusive) +/*static*/ const double TrackMetadata::REPLAYGAIN_0DB = 1.0; -namespace { - -QString formatBpm(double bpm) { - if (TrackMetadata::isBpmValid(bpm)) { - return QString::number(bpm, 'f'); - } else { - return QString(); - } -} - -double parseBpm(const QString& sBpm, bool* pValid = 0) { +double TrackMetadata::parseBpm(const QString& sBpm, bool* pValid) { if (pValid) { *pValid = false; } if (sBpm.trimmed().isEmpty()) { - return TrackMetadata::BPM_UNDEFINED; + return BPM_UNDEFINED; } bool bpmValid = false; double bpm = sBpm.toDouble(&bpmValid); if (bpmValid) { - if (TrackMetadata::BPM_UNDEFINED == bpm) { + if (BPM_UNDEFINED == bpm) { // special case if (pValid) { *pValid = true; } return bpm; } - while (TrackMetadata::BPM_MAX < bpm) { - // TODO(XXX): Why? + while (BPM_MAX < bpm) { + // TODO(XXX): Why do we need to scale values that + // exceed the reasonable range? qDebug() << "Scaling BPM value:" << bpm; bpm /= 10.0; } @@ -55,53 +46,58 @@ double parseBpm(const QString& sBpm, bool* pValid = 0) { } else { qDebug() << "Failed to parse BPM:" << sBpm; } - return TrackMetadata::BPM_UNDEFINED; + return BPM_UNDEFINED; } -const QString REPLAYGAIN_UNIT("dB"); -const QString REPLAYGAIN_SUFFIX(" " + REPLAYGAIN_UNIT); - -QString formatReplayGainDb(double replayGain) { - if (TrackMetadata::isReplayGainValid(replayGain)) { - return QString::number(ratio2db(replayGain), 'f') + REPLAYGAIN_SUFFIX; +QString TrackMetadata::formatBpm(double bpm) { + if (TrackMetadata::isBpmValid(bpm)) { + return QString::number(bpm); } else { return QString(); } } -double parseReplayGainDb(QString sReplayGainDb, bool* pValid = 0) { +namespace { + +const QString REPLAYGAIN_UNIT("dB"); +const QString REPLAYGAIN_SUFFIX(" " + REPLAYGAIN_UNIT); + +} // anonymous namespace + +double TrackMetadata::parseReplayGain(QString sReplayGain, bool* pValid) { if (pValid) { *pValid = false; } - QString normalizedReplayGainDb(sReplayGainDb.trimmed()); - const int plusIndex = normalizedReplayGainDb.indexOf('+'); + QString normalizedReplayGain(sReplayGain.trimmed()); + const int plusIndex = normalizedReplayGain.indexOf('+'); if (0 == plusIndex) { // strip leading "+" - normalizedReplayGainDb = normalizedReplayGainDb.mid(plusIndex + 1).trimmed(); + normalizedReplayGain = normalizedReplayGain.mid(plusIndex + 1).trimmed(); } - const int unitIndex = normalizedReplayGainDb.lastIndexOf(REPLAYGAIN_UNIT, -1, Qt::CaseInsensitive); - if ((0 <= unitIndex) && ((normalizedReplayGainDb.length() - 2) == unitIndex)) { + const int unitIndex = normalizedReplayGain.lastIndexOf(REPLAYGAIN_UNIT, -1, Qt::CaseInsensitive); + if ((0 <= unitIndex) && ((normalizedReplayGain.length() - 2) == unitIndex)) { // strip trailing unit suffix - normalizedReplayGainDb = normalizedReplayGainDb.left(unitIndex).trimmed(); + normalizedReplayGain = normalizedReplayGain.left(unitIndex).trimmed(); } - if (normalizedReplayGainDb.isEmpty()) { - return TrackMetadata::REPLAYGAIN_UNDEFINED; + if (normalizedReplayGain.isEmpty()) { + return REPLAYGAIN_UNDEFINED; } - bool replayGainDbValid = false; - const double replayGainDb = normalizedReplayGainDb.toDouble(&replayGainDbValid); - if (replayGainDbValid) { + bool replayGainValid = false; + const double replayGainDb = normalizedReplayGain.toDouble(&replayGainValid); + if (replayGainValid) { const double replayGain = db2ratio(replayGainDb); - DEBUG_ASSERT(TrackMetadata::REPLAYGAIN_UNDEFINED != replayGain); // impossible - // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. - // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by - // setting value to 0 (i.e. rescan via analyserrg) - if (TrackMetadata::REPLAYGAIN_0DB == replayGain) { + DEBUG_ASSERT(REPLAYGAIN_UNDEFINED != replayGain); // impossible + // Some applications (e.g. Rapid Evolution 3) write a replay gain + // of 0 dB even if the replay gain is undefined. To be safe we + // ignore this special value and instead prefer to recalculate + // the replay gain. + if (REPLAYGAIN_0DB == replayGain) { // special case - qDebug() << "Ignoring replay gain:" << formatReplayGainDb(replayGain); + qDebug() << "Ignoring possibly undefined replay gain:" << formatReplayGain(replayGain); if (pValid) { *pValid = true; } - return TrackMetadata::REPLAYGAIN_UNDEFINED; + return REPLAYGAIN_UNDEFINED; } if (TrackMetadata::isReplayGainValid(replayGain)) { if (pValid) { @@ -109,14 +105,20 @@ double parseReplayGainDb(QString sReplayGainDb, bool* pValid = 0) { } return replayGain; } else { - qDebug() << "Invalid replay gain value:" << sReplayGainDb << " -> "<< replayGain; + qDebug() << "Invalid replay gain value:" << sReplayGain << " -> "<< replayGain; } } else { - qDebug() << "Failed to parse replay gain:" << sReplayGainDb; + qDebug() << "Failed to parse replay gain:" << sReplayGain; } - return TrackMetadata::REPLAYGAIN_UNDEFINED; + return REPLAYGAIN_UNDEFINED; } +QString TrackMetadata::formatReplayGain(double replayGain) { + if (isReplayGainValid(replayGain)) { + return QString::number(ratio2db(replayGain)) + REPLAYGAIN_SUFFIX; + } else { + return QString(); + } } TrackMetadata::TrackMetadata() : @@ -128,30 +130,4 @@ TrackMetadata::TrackMetadata() : m_replayGain(REPLAYGAIN_UNDEFINED) { } -bool TrackMetadata::setBpmString(const QString& sBpm) { - bool bpmValid; - const double bpm = parseBpm(sBpm, &bpmValid); - if (bpmValid) { - setBpm(bpm); - } - return bpmValid; -} - -QString TrackMetadata::getBpmString() const { - return formatBpm(getBpm()); -} - -bool TrackMetadata::setReplayGainDbString(QString sReplayGainDb) { - bool replayGainValid; - const float replayGain = parseReplayGainDb(sReplayGainDb, &replayGainValid); - if (replayGainValid) { - setReplayGain(replayGain); - } - return replayGainValid; -} - -QString TrackMetadata::getReplayGainDbString() const { - return formatReplayGainDb(getReplayGain()); -} - } //namespace Mixxx diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index a3c2130de72..32398f26677 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -139,29 +139,35 @@ class TrackMetadata { inline void resetBpm() { m_bpm = BPM_UNDEFINED; } - bool setBpmString(const QString& sBpm); - QString getBpmString() const; - static const float REPLAYGAIN_UNDEFINED; - static const float REPLAYGAIN_MIN; // lower bound (exclusive) - static const float REPLAYGAIN_0DB; - inline float getReplayGain() const { + static const double REPLAYGAIN_UNDEFINED; + static const double REPLAYGAIN_MIN; // lower bound (exclusive) + static const double REPLAYGAIN_0DB; + inline double getReplayGain() const { return m_replayGain; } - inline static bool isReplayGainValid(float replayGain) { + inline static bool isReplayGainValid(double replayGain) { return REPLAYGAIN_MIN < replayGain; } inline bool isReplayGainValid() const { return isReplayGainValid(getReplayGain()); } - inline void setReplayGain(float replayGain) { + inline void setReplayGain(double replayGain) { m_replayGain = replayGain; } inline void resetReplayGain() { m_replayGain = REPLAYGAIN_UNDEFINED; } - bool setReplayGainDbString(QString sReplayGainDb); // in dB - QString getReplayGainDbString() const; // in dB + + // Parse and format BPM metadata + static double parseBpm(const QString& sBpm, bool* pValid = 0); + static QString formatBpm(double bpm); + + // Parse and format replay gain metadata according to the + // ReplayGain 1.0 specification. + // http://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification + static double parseReplayGain(QString sReplayGain, bool* pValid = 0); + static QString formatReplayGain(double replayGain); private: QString m_artist; @@ -184,7 +190,7 @@ class TrackMetadata { int m_bitrate; int m_duration; double m_bpm; - float m_replayGain; + double m_replayGain; }; } diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 9da44fcdc86..577dcd1c7a2 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -77,6 +77,24 @@ inline TagLib::String toTagLibString(const QString& str) { return TagLib::String(qba.constData(), TagLib::String::UTF8); } +inline bool parseBpm(TrackMetadata* pTrackMetadata, QString sBpm) { + bool bpmValid = false; + double bpm = TrackMetadata::parseBpm(sBpm, &bpmValid); + if (bpmValid) { + pTrackMetadata->setBpm(bpm); + } + return bpmValid; +} + +inline bool parseReplayGain(TrackMetadata* pTrackMetadata, QString sReplayGain) { + bool replayGainValid = false; + double replayGain = TrackMetadata::parseReplayGain(sReplayGain, &replayGainValid); + if (replayGainValid) { + pTrackMetadata->setReplayGain(replayGain); + } + return replayGainValid; +} + } // anonymous namespace bool readAudioProperties(TrackMetadata* pTrackMetadata, @@ -179,7 +197,7 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::FrameList bpmFrame(tag.frameListMap()["TBPM"]); if (!bpmFrame.isEmpty()) { - pTrackMetadata->setBpmString(toQStringFirst(bpmFrame)); + parseBpm(pTrackMetadata, toQStringFirst(bpmFrame)); } // Foobar2000-style ID3v2.3.0 tags @@ -194,7 +212,7 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, toQString(replaygainFrame->description())); // Only read track gain (not album gain) if (0 == desc.compare("REPLAYGAIN_TRACK_GAIN", Qt::CaseInsensitive)) { - pTrackMetadata->setReplayGainDbString( + parseReplayGain(pTrackMetadata, toQString(replaygainFrame->fieldList()[1])); } } @@ -236,12 +254,12 @@ void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { } if (tag.itemListMap().contains("BPM")) { - pTrackMetadata->setBpmString(toQString(tag.itemListMap()["BPM"])); + parseBpm(pTrackMetadata, toQString(tag.itemListMap()["BPM"])); } // Only read track gain (not album gain) if (tag.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainDbString( + parseReplayGain(pTrackMetadata, toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); } } @@ -302,18 +320,17 @@ void readXiphComment(TrackMetadata* pTrackMetadata, // Some tags use "BPM" so check for that. if (tag.fieldListMap().contains("BPM")) { - pTrackMetadata->setBpmString(toQStringFirst(tag.fieldListMap()["BPM"])); + parseBpm(pTrackMetadata, toQStringFirst(tag.fieldListMap()["BPM"])); } // Give preference to the "TEMPO" tag which seems to be more standard if (tag.fieldListMap().contains("TEMPO")) { - pTrackMetadata->setBpmString( - toQStringFirst(tag.fieldListMap()["TEMPO"])); + parseBpm(pTrackMetadata, toQStringFirst(tag.fieldListMap()["TEMPO"])); } // Only read track gain (not album gain) if (tag.fieldListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainDbString( + parseReplayGain(pTrackMetadata, toQStringFirst(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); } @@ -381,14 +398,14 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { // If this field contains a valid value the integer // BPM value that might have been read before is // overwritten. - pTrackMetadata->setBpmString( + parseBpm(pTrackMetadata, toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:BPM"])); } // Only read track gain (not album gain) if (tag.itemListMap().contains( "----:com.apple.iTunes:replaygain_track_gain")) { - pTrackMetadata->setReplayGainDbString( + parseReplayGain(pTrackMetadata, toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:replaygain_track_gain"])); } @@ -535,9 +552,9 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, writeID3v2TextIdentificationFrame(pTag, "TPE2", trackMetadata.getAlbumArtist()); writeID3v2TextIdentificationFrame(pTag, "TBPM", - trackMetadata.getBpmString()); + TrackMetadata::formatBpm(trackMetadata.getBpm())); writeID3v2TextIdentificationFrame(pTag, "TBPM", - trackMetadata.getBpmString()); + TrackMetadata::formatBpm(trackMetadata.getBpm())); writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); writeID3v2TextIdentificationFrame(pTag, "TCOM", trackMetadata.getComposer()); @@ -570,9 +587,9 @@ bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { pTag->addValue("Year", toTagLibString(trackMetadata.getYear()), true); pTag->addValue("BPM", - toTagLibString(trackMetadata.getBpmString()), true); + toTagLibString(TrackMetadata::formatBpm(trackMetadata.getBpm())), true); pTag->addValue("REPLAYGAIN_TRACK_GAIN", - toTagLibString(trackMetadata.getReplayGainDbString()), true); + toTagLibString(TrackMetadata::formatReplayGain(trackMetadata.getReplayGain())), true); return true; } @@ -605,10 +622,10 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, // Some tools use "BPM" so write that. pTag->removeField("BPM"); pTag->addField("BPM", - toTagLibString(trackMetadata.getBpmString())); + toTagLibString(TrackMetadata::formatBpm(trackMetadata.getBpm()))); pTag->removeField("TEMPO"); pTag->addField("TEMPO", - toTagLibString(trackMetadata.getBpmString())); + toTagLibString(TrackMetadata::formatBpm(trackMetadata.getBpm()))); pTag->removeField("INITIALKEY"); pTag->addField("INITIALKEY", toTagLibString(trackMetadata.getKey())); @@ -617,7 +634,7 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, pTag->removeField("REPLAYGAIN_TRACK_GAIN"); pTag->addField("REPLAYGAIN_TRACK_GAIN", - toTagLibString(trackMetadata.getReplayGainDbString())); + toTagLibString(TrackMetadata::formatReplayGain(trackMetadata.getReplayGain()))); return true; } @@ -659,9 +676,9 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { pTag->itemListMap().erase("tmpo"); } writeMP4Atom(pTag, "----:com.apple.iTunes:BPM", - trackMetadata.getBpmString()); + TrackMetadata::formatBpm(trackMetadata.getBpm())); writeMP4Atom(pTag, "----:com.apple.iTunes:replaygain_track_gain", - trackMetadata.getReplayGainDbString()); + TrackMetadata::formatReplayGain(trackMetadata.getReplayGain())); writeMP4Atom(pTag, "----:com.apple.iTunes:initialkey", trackMetadata.getKey()); writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index c829b1e2489..f928e9f2d45 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -19,74 +19,69 @@ class MetadataTest : public testing::Test { virtual void TearDown() { } - double parseBpm(double initialValue, QString inputValue, bool expectedResult, double expectedValue) { - //qDebug() << "parseBpm" << initialValue << inputValue << expectedResult << expectedValue; + double parseBpm(QString inputValue, bool expectedResult, double expectedValue) { + //qDebug() << "parseBpm" << inputValue << expectedResult << expectedValue; - Mixxx::TrackMetadata trackMetadata; - trackMetadata.setBpm(initialValue); - - const bool actualResult = trackMetadata.setBpmString(inputValue); + bool actualResult; + const double actualValue = Mixxx::TrackMetadata::parseBpm(inputValue, &actualResult); EXPECT_EQ(expectedResult, actualResult); - EXPECT_DOUBLE_EQ(trackMetadata.getBpm(), expectedValue); + EXPECT_DOUBLE_EQ(expectedValue, actualValue); + + if (actualResult) { + qDebug() << "BPM:" << inputValue << "->" << Mixxx::TrackMetadata::formatBpm(actualValue); + } return actualResult; } - float parseReplayGainDb(float initialValue, QString inputValue, bool expectedResult, float expectedValue) { - //qDebug() << "parseReplayGainDb" << initialValue << inputValue << expectedResult << expectedValue; + double parseReplayGain(QString inputValue, bool expectedResult, float expectedValue) { + //qDebug() << "parseReplayGain" << inputValue << expectedResult << expectedValue; - Mixxx::TrackMetadata trackMetadata; - trackMetadata.setReplayGain(initialValue); - - const bool actualResult = trackMetadata.setReplayGainDbString(inputValue); + bool actualResult; + const double actualValue = Mixxx::TrackMetadata::parseReplayGain(inputValue, &actualResult); EXPECT_EQ(expectedResult, actualResult); - EXPECT_FLOAT_EQ(trackMetadata.getReplayGain(), expectedValue); + EXPECT_FLOAT_EQ(expectedValue, actualValue); + + if (actualResult) { + qDebug() << "ReplayGain:" << inputValue << "->" << Mixxx::TrackMetadata::formatReplayGain(actualValue); + } return actualResult; } }; TEST_F(MetadataTest, ParseBpmPrecision) { - parseBpm(100.0, "128.1234", true, 128.1234); // 4 fractional digits + parseBpm("128.1234", true, 128.1234); // 4 fractional digits } TEST_F(MetadataTest, ParseBpmValidRange) { for (int bpm100 = int(Mixxx::TrackMetadata::BPM_MIN) * 100; int(Mixxx::TrackMetadata::BPM_MAX) * 100 >= bpm100; ++bpm100) { const double expectedValue = bpm100 / 100.0; - const double initialValues[] = { - Mixxx::TrackMetadata::BPM_UNDEFINED, - 128.5 - }; const QString inputValues[] = { QString("%1").arg(expectedValue), QString(" %1 ").arg(expectedValue), }; - for (size_t i = 0; i < sizeof(initialValues) / sizeof(initialValues[0]); ++i) { - for (size_t j = 0; j < sizeof(inputValues) / sizeof(inputValues[0]); ++j) { - parseBpm(initialValues[i], inputValues[j], true, expectedValue); - } + for (size_t i = 0; i < sizeof(inputValues) / sizeof(inputValues[0]); ++i) { + parseBpm(inputValues[i], true, expectedValue); } } } TEST_F(MetadataTest, ParseBpmDecimalScaling) { - parseBpm(100.0, "345678", true, 34.5678); - parseBpm(100.0, "2345678", true, 234.5678); + parseBpm("345678", true, 34.5678); + parseBpm("2345678", true, 234.5678); } TEST_F(MetadataTest, ParseBpmInvalid) { - parseBpm(Mixxx::TrackMetadata::BPM_UNDEFINED, "", false, Mixxx::TrackMetadata::BPM_UNDEFINED); - parseBpm(123.45, "abcde", false, 123.45); + parseBpm("", false, Mixxx::TrackMetadata::BPM_UNDEFINED); + parseBpm("abcde", false, Mixxx::TrackMetadata::BPM_UNDEFINED); + parseBpm("0 dBA", false, Mixxx::TrackMetadata::BPM_UNDEFINED); } TEST_F(MetadataTest, ParseReplayGainDbValidRange) { for (int replayGainDb = -100; 100 >= replayGainDb; ++replayGainDb) { - const float initialValues[] = { - Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED, - 0.5f - }; const QString inputValues[] = { QString("%1 ").arg(replayGainDb), QString(" %1dB ").arg(replayGainDb), @@ -101,20 +96,19 @@ TEST_F(MetadataTest, ParseReplayGainDbValidRange) { // special case: 0 dB -> undefined expectedValue = Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED; } - for (size_t i = 0; i < sizeof(initialValues) / sizeof(initialValues[0]); ++i) { - for (size_t j = 0; j < sizeof(inputValues) / sizeof(inputValues[0]); ++j) { - parseReplayGainDb(initialValues[i], inputValues[j], true, expectedValue); - if (0 <= replayGainDb) { - parseReplayGainDb(initialValues[i], QString(" + ") + inputValues[j], true, expectedValue); - } + for (size_t i = 0; i < sizeof(inputValues) / sizeof(inputValues[0]); ++i) { + parseReplayGain(inputValues[i], true, expectedValue); + if (0 <= replayGainDb) { + parseReplayGain(QString(" + ") + inputValues[i], true, expectedValue); } } } } TEST_F(MetadataTest, ParseReplayGainDbInvalid) { - parseReplayGainDb(Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED, "", false, Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED); - parseReplayGainDb(0.5, "abcde", false, 0.5); + parseReplayGain("", false, Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED); + parseReplayGain("abcde", false, Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED); + parseReplayGain("0 dBA", false, Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED); } } // namespace From 1be724a9ed92ac06392d098197f0b08ae9bfc406 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 17:23:30 +0100 Subject: [PATCH 148/481] MP3 tags: Write TBPM as integer string --- src/metadata/trackmetadata.cpp | 8 ++++++++ src/metadata/trackmetadata.h | 6 ++++++ src/metadata/trackmetadatataglib.cpp | 6 ++---- src/test/metadatatest.cpp | 12 ++++++------ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 38fcff41865..fb64511e65d 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -57,6 +57,14 @@ QString TrackMetadata::formatBpm(double bpm) { } } +QString TrackMetadata::formatBpm(int bpm) { + if (TrackMetadata::isBpmValid(bpm)) { + return QString::number(bpm); + } else { + return QString(); + } +} + namespace { const QString REPLAYGAIN_UNIT("dB"); diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 32398f26677..7d55ad9d001 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -3,6 +3,8 @@ #include +#include + namespace Mixxx { // DTO for track metadata properties. Must not be subclassed (no virtual destructor)! @@ -127,6 +129,9 @@ class TrackMetadata { inline double getBpm() const { return m_bpm; } + inline int getBpmAsInteger() const { + return round(getBpm()); + } inline static bool isBpmValid(double bpm) { return (BPM_MIN < bpm) && (BPM_MAX >= bpm); } @@ -162,6 +167,7 @@ class TrackMetadata { // Parse and format BPM metadata static double parseBpm(const QString& sBpm, bool* pValid = 0); static QString formatBpm(double bpm); + static QString formatBpm(int bpm); // Parse and format replay gain metadata according to the // ReplayGain 1.0 specification. diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 577dcd1c7a2..a7b514d1817 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -14,8 +14,6 @@ #include #include -#include - #include namespace Mixxx { @@ -554,7 +552,7 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, writeID3v2TextIdentificationFrame(pTag, "TBPM", TrackMetadata::formatBpm(trackMetadata.getBpm())); writeID3v2TextIdentificationFrame(pTag, "TBPM", - TrackMetadata::formatBpm(trackMetadata.getBpm())); + TrackMetadata::formatBpm(trackMetadata.getBpmAsInteger())); writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); writeID3v2TextIdentificationFrame(pTag, "TCOM", trackMetadata.getComposer()); @@ -671,7 +669,7 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { writeMP4Atom(pTag, "\251grp", trackMetadata.getGrouping()); writeMP4Atom(pTag, "\251day", trackMetadata.getYear()); if (trackMetadata.isBpmValid()) { - writeMP4Atom(pTag, "tmpo", (int) round(trackMetadata.getBpm())); + writeMP4Atom(pTag, "tmpo", trackMetadata.getBpmAsInteger()); } else { pTag->itemListMap().erase("tmpo"); } diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index f928e9f2d45..6adb5a0083a 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -28,9 +28,9 @@ class MetadataTest : public testing::Test { EXPECT_EQ(expectedResult, actualResult); EXPECT_DOUBLE_EQ(expectedValue, actualValue); - if (actualResult) { - qDebug() << "BPM:" << inputValue << "->" << Mixxx::TrackMetadata::formatBpm(actualValue); - } +// if (actualResult) { +// qDebug() << "BPM:" << inputValue << "->" << Mixxx::TrackMetadata::formatBpm(actualValue); +// } return actualResult; } @@ -44,9 +44,9 @@ class MetadataTest : public testing::Test { EXPECT_EQ(expectedResult, actualResult); EXPECT_FLOAT_EQ(expectedValue, actualValue); - if (actualResult) { - qDebug() << "ReplayGain:" << inputValue << "->" << Mixxx::TrackMetadata::formatReplayGain(actualValue); - } +// if (actualResult) { +// qDebug() << "ReplayGain:" << inputValue << "->" << Mixxx::TrackMetadata::formatReplayGain(actualValue); +// } return actualResult; } From 5d42ae2d2a05510a54c01513e275fad3e7e8f55b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 18:08:26 +0100 Subject: [PATCH 149/481] Delete duplicate code and add comment --- src/metadata/trackmetadatataglib.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index a7b514d1817..b0e40f4a70a 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -549,8 +549,10 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, // additional tags writeID3v2TextIdentificationFrame(pTag, "TPE2", trackMetadata.getAlbumArtist()); - writeID3v2TextIdentificationFrame(pTag, "TBPM", - TrackMetadata::formatBpm(trackMetadata.getBpm())); + // According to the specification "The 'TBPM' frame contains the number + // of beats per minute in the mainpart of the audio. The BPM is an integer + // and represented as a numerical string." + // Reference: http://id3.org/id3v2.3.0 writeID3v2TextIdentificationFrame(pTag, "TBPM", TrackMetadata::formatBpm(trackMetadata.getBpmAsInteger())); writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); From 7e4366079b9afec6b3c1977770a2e3e91e43aced Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 18:34:44 +0100 Subject: [PATCH 150/481] Use a suitable character encoding for ID3v2 tags --- src/metadata/trackmetadata.cpp | 7 ++++--- src/metadata/trackmetadatataglib.cpp | 22 +++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index fb64511e65d..3e55e0e7fc3 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -30,8 +30,9 @@ double TrackMetadata::parseBpm(const QString& sBpm, bool* pValid) { return bpm; } while (BPM_MAX < bpm) { - // TODO(XXX): Why do we need to scale values that - // exceed the reasonable range? + // Some applications might store the BPM as an + // integer scaled by a factor of 10 or 100 to + // preserve fractional digits. qDebug() << "Scaling BPM value:" << bpm; bpm /= 10.0; } @@ -94,7 +95,7 @@ double TrackMetadata::parseReplayGain(QString sReplayGain, bool* pValid) { const double replayGainDb = normalizedReplayGain.toDouble(&replayGainValid); if (replayGainValid) { const double replayGain = db2ratio(replayGainDb); - DEBUG_ASSERT(REPLAYGAIN_UNDEFINED != replayGain); // impossible + DEBUG_ASSERT(REPLAYGAIN_UNDEFINED != replayGain); // Some applications (e.g. Rapid Evolution 3) write a replay gain // of 0 dB even if the replay gain is undefined. To be safe we // ignore this special value and instead prefer to recalculate diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index b0e40f4a70a..d170ebc5c6c 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -70,9 +70,9 @@ inline QString toQString(const TagLib::APE::Item& apeItem) { return toQString(apeItem.toString()); } -inline TagLib::String toTagLibString(const QString& str) { +inline TagLib::String toTagLibString(const QString& str, TagLib::String::Type textType = TagLib::String::UTF8) { const QByteArray qba(str.toUtf8()); - return TagLib::String(qba.constData(), TagLib::String::UTF8); + return TagLib::String(qba.constData(), textType); } inline bool parseBpm(TrackMetadata* pTrackMetadata, QString sBpm) { @@ -388,7 +388,10 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { // Get BPM if (tag.itemListMap().contains("tmpo")) { // Read the BPM as an integer value. - pTrackMetadata->setBpm(tag.itemListMap()["tmpo"].toInt()); + const TagLib::MP4::Item& item = tag.itemListMap()["tmpo"]; + if (item.atomDataType() == TagLib::MP4::TypeInteger) { + pTrackMetadata->setBpm(tag.itemListMap()["tmpo"].toInt()); + } } if (tag.itemListMap().contains("----:com.apple.iTunes:BPM")) { // This is the preferred field for storing the BPM @@ -490,20 +493,21 @@ void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, const TagLib::ByteVector &id, const QString& text) { TagLib::String::Type textType; - QByteArray textData; if (4 <= pTag->header()->majorVersion()) { - // prefer UTF-8 for ID3v2.4.0 or higher + // For ID3v2.4.0 or higher use UTF-8 textType = TagLib::String::UTF8; - textData = text.toUtf8(); } else { - textType = TagLib::String::Latin1; - textData = text.toLatin1(); + // For ID3v2.3.0/ID3v2.2.0 use UTF-16 with byte order mark + textType = TagLib::String::UTF16; } QScopedPointer pNewFrame( new TagLib::ID3v2::TextIdentificationFrame(id, textType)); pNewFrame->setText(toTagLibString(text)); replaceID3v2Frame(pTag, pNewFrame.data()); - pNewFrame.take(); // release ownership + // Now the plain pointer in pNewFrame is owned and + // managed by pTag. We need to release the ownership + // to avoid double deletion! + pNewFrame.take(); } } // anonymous namespace From 2936daeaab3031c66bb965e0ef26891512ac1c5e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 18:46:46 +0100 Subject: [PATCH 151/481] Move sample conversion code to SampleUtil --- src/musicbrainz/chromaprinter.cpp | 8 +++++--- src/sampleutil.cpp | 17 +++++++++++++---- src/sampleutil.h | 7 +++++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index a339b309cfc..39d0bbc1ccc 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -1,6 +1,7 @@ #include "musicbrainz/chromaprinter.h" #include "soundsourceproxy.h" +#include "sampleutil.h" #include @@ -47,9 +48,10 @@ namespace std::vector fingerprintSamples(readFrames * kFingerprintChannels); // Convert floating-point to integer - for (Mixxx::AudioSource::size_type i = 0; i < fingerprintSamples.size(); ++i) { - fingerprintSamples[i] = SAMPLE(sampleBuffer[i] * SAMPLE_MAX); - } + SampleUtil::convertFloat32ToS16( + &fingerprintSamples[0], + &sampleBuffer[0], + fingerprintSamples.size()); qDebug("reading file took: %d ms" , timerReadingFile.elapsed()); diff --git a/src/sampleutil.cpp b/src/sampleutil.cpp index d793c45d79a..3bc4bd4fc97 100644 --- a/src/sampleutil.cpp +++ b/src/sampleutil.cpp @@ -185,15 +185,24 @@ void SampleUtil::copyWithRampingGain(CSAMPLE* pDest, const CSAMPLE* pSrc, // static void SampleUtil::convertS16ToFloat32(CSAMPLE* pDest, const SAMPLE* pSrc, unsigned int iNumSamples) { - // -32768 is a valid low sample, whereas 32767 is the highest valid sample. - // Note that this means that although some sample values convert to -1.0, - // none will convert to +1.0. - const CSAMPLE kConversionFactor = 0x8000; + // SAMPLE_MIN = -32768 is a valid low sample, whereas SAMPLE_MAX = 32767 + // is the highest valid sample. Note that this means that although some + // sample values convert to -1.0, none will convert to +1.0. + const CSAMPLE kConversionFactor = SAMPLE_MAX; for (unsigned int i = 0; i < iNumSamples; ++i) { pDest[i] = CSAMPLE(pSrc[i]) / kConversionFactor; } } +//static +void SampleUtil::convertFloat32ToS16(SAMPLE* pDest, const CSAMPLE* pSrc, + unsigned int iNumSamples) { + const CSAMPLE kConversionFactor = SAMPLE_MAX; + for (unsigned int i = 0; i < iNumSamples; ++i) { + pDest[i] = SAMPLE(pSrc[i] * kConversionFactor); + } +} + // static bool SampleUtil::sumAbsPerChannel(CSAMPLE* pfAbsL, CSAMPLE* pfAbsR, const CSAMPLE* pBuffer, unsigned int iNumSamples) { diff --git a/src/sampleutil.h b/src/sampleutil.h index cc99fe9295a..ac0ca8f371d 100644 --- a/src/sampleutil.h +++ b/src/sampleutil.h @@ -124,11 +124,14 @@ class SampleUtil { // Convert and normalize a buffer of SAMPLEs in the range [-SAMPLE_MAX, SAMPLE_MAX] // to a buffer of CSAMPLEs in the range [-1.0, 1.0]. - // NOTE(uklotzde): This conversion is deprecated and will be removed - // with the introduction of the new SoundSourceAPI. static void convertS16ToFloat32(CSAMPLE* pDest, const SAMPLE* pSrc, unsigned int iNumSamples); + // Convert and normalize a buffer of CSAMPLEs in the range [-1.0, 1.0] + // to a buffer of SAMPLEs in the range [-SAMPLE_MAX, SAMPLE_MAX]. + static void convertFloat32ToS16(SAMPLE* pDest, const CSAMPLE* pSrc, + unsigned int iNumSamples); + // For each pair of samples in pBuffer (l,r) -- stores the sum of the // absolute values of l in pfAbsL, and the sum of the absolute values of r // in pfAbsR. From 80a09862ecd9aa0c09f1c58d172461d319857541 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 18:49:36 +0100 Subject: [PATCH 152/481] Code formatting --- src/sources/audiosourceflac.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 02df581e039..020c4085bf6 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -176,7 +176,8 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( << m_file.fileName() << ")"; break; } - } DEBUG_ASSERT( + } + DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); const size_type decodeBufferSamples = m_decodeSampleBufferWriteOffset - m_decodeSampleBufferReadOffset; @@ -318,7 +319,8 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( } } } - } DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + } + DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; } From 4651742cb9c548f49ba9b43641ad9edc4bdd2660 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 28 Jan 2015 18:54:08 +0100 Subject: [PATCH 153/481] Fix sample conversion routines CSAMPLE <-> SAMPLE --- src/sampleutil.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/sampleutil.cpp b/src/sampleutil.cpp index 3bc4bd4fc97..60eaf07728a 100644 --- a/src/sampleutil.cpp +++ b/src/sampleutil.cpp @@ -188,7 +188,8 @@ void SampleUtil::convertS16ToFloat32(CSAMPLE* pDest, const SAMPLE* pSrc, // SAMPLE_MIN = -32768 is a valid low sample, whereas SAMPLE_MAX = 32767 // is the highest valid sample. Note that this means that although some // sample values convert to -1.0, none will convert to +1.0. - const CSAMPLE kConversionFactor = SAMPLE_MAX; + DEBUG_ASSERT(-SAMPLE_MIN >= SAMPLE_MAX); + const CSAMPLE kConversionFactor = -SAMPLE_MIN; for (unsigned int i = 0; i < iNumSamples; ++i) { pDest[i] = CSAMPLE(pSrc[i]) / kConversionFactor; } @@ -197,7 +198,8 @@ void SampleUtil::convertS16ToFloat32(CSAMPLE* pDest, const SAMPLE* pSrc, //static void SampleUtil::convertFloat32ToS16(SAMPLE* pDest, const CSAMPLE* pSrc, unsigned int iNumSamples) { - const CSAMPLE kConversionFactor = SAMPLE_MAX; + DEBUG_ASSERT(-SAMPLE_MIN >= SAMPLE_MAX); + const CSAMPLE kConversionFactor = -SAMPLE_MIN; for (unsigned int i = 0; i < iNumSamples; ++i) { pDest[i] = SAMPLE(pSrc[i] * kConversionFactor); } From f49e0cb962466e40c99722ae3121424eda6b83af Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 21:02:42 +0100 Subject: [PATCH 154/481] Review: Add comments about character encodings for different ID3v2 versions --- src/metadata/trackmetadatataglib.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index d170ebc5c6c..6f06f84a09d 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -493,11 +493,22 @@ void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, const TagLib::ByteVector &id, const QString& text) { TagLib::String::Type textType; + // For an overview of the character encodings supported by + // the different ID3v2 versions please refer to the following + // resources: + // http://en.wikipedia.org/wiki/ID3#ID3v2 + // http://id3.org/id3v2.3.0 + // http://id3.org/id3v2.4.0-structure if (4 <= pTag->header()->majorVersion()) { - // For ID3v2.4.0 or higher use UTF-8 + // For ID3v2.4.0 or higher prefer UTF-8, because it is a + // very compact representation for common cases and it is + // independent of the byte order. textType = TagLib::String::UTF8; } else { - // For ID3v2.3.0/ID3v2.2.0 use UTF-16 with byte order mark + // For ID3v2.3.0/ID3v2.2.0 use UCS-2 (UTF-16 encoded Unicode + // with BOM), because UTF-8 and UTF-16BE are only supported + // since ID3v2.4.0 and the alternative ISO-8859-1 does not + // cover all Unicode characters. textType = TagLib::String::UTF16; } QScopedPointer pNewFrame( From 5a5ce62d1476e1fd6a0495add419669522e8d4cb Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 21:04:34 +0100 Subject: [PATCH 155/481] Review: Don't set default values initialized by the constructor again --- src/sources/audiosourcemp3.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index f2f1c19d18e..6aad1297ec5 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -101,9 +101,6 @@ Result AudioSourceMp3::postConstruct() { // Decode all the headers and calculate audio properties - setChannelCount(kChannelCountDefault); - setFrameRate(kFrameRateDefault); - mad_units madUnits = MAD_UNITS_44100_HZ; // default value mad_timer_t madDuration = mad_timer_zero; unsigned long sumBitrate = 0; From 8393a3d461c659fbd58e9c9f4cd33e945e9aff11 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 21:05:45 +0100 Subject: [PATCH 156/481] Review: Correctly distinguish constants by their purpose --- src/sources/audiosourceoggvorbis.cpp | 12 +++++++++--- src/sources/audiosourceopus.cpp | 14 ++++++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 4c4d4d162a4..a96028a5ddf 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -4,7 +4,13 @@ namespace Mixxx { namespace { -const int kLogicalBitstreamIndex = -1; // whole stream/file +// Parameter for ov_info() +// See also: https://xiph.org/vorbis/doc/vorbisfile/ov_info.html +const int kCurrentBitstreamLink = -1; // retrieve ... for the current bitstream + +// Parameter for ov_pcm_total() +// See also: https://xiph.org/vorbis/doc/vorbisfile/ov_pcm_total.html +const int kEntireBitstreamLink = -1; // retrieve ... for the entire physical bitstream } // anonymous namespace @@ -35,7 +41,7 @@ Result AudioSourceOggVorbis::postConstruct() { } // lookup the ogg's channels and sample rate - const vorbis_info* vi = ov_info(&m_vf, kLogicalBitstreamIndex); + const vorbis_info* vi = ov_info(&m_vf, kCurrentBitstreamLink); if (!vi) { qWarning() << "Failed to read OggVorbis file:" << getUrl(); return ERR; @@ -50,7 +56,7 @@ Result AudioSourceOggVorbis::postConstruct() { } } - ogg_int64_t pcmTotal = ov_pcm_total(&m_vf, kLogicalBitstreamIndex); + ogg_int64_t pcmTotal = ov_pcm_total(&m_vf, kEntireBitstreamLink); if (0 <= pcmTotal) { setFrameCount(pcmTotal); } else { diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 94e8be69d6f..34d256b81b1 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -4,7 +4,13 @@ namespace Mixxx { namespace { -const int kLogicalBitstreamIndex = -1; // whole stream/file +// Parameter for op_channel_count() +// See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html +const int kCurrentStreamLink = -1; // get ... of the current (stream) link + +// Parameter for op_pcm_total() and op_bitrate() +// See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html +const int kEntireStreamLink = -1; // get ... of the whole/entire stream } // anonymous namespace @@ -40,7 +46,7 @@ Result AudioSourceOpus::postConstruct() { return ERR; } - const int channelCount = op_channel_count(m_pOggOpusFile, kLogicalBitstreamIndex); + const int channelCount = op_channel_count(m_pOggOpusFile, kCurrentStreamLink); if (0 < channelCount) { setChannelCount(channelCount); } else { @@ -48,7 +54,7 @@ Result AudioSourceOpus::postConstruct() { return ERR; } - const ogg_int64_t pcmTotal = op_pcm_total(m_pOggOpusFile, kLogicalBitstreamIndex); + const ogg_int64_t pcmTotal = op_pcm_total(m_pOggOpusFile, kEntireStreamLink); if (0 <= pcmTotal) { setFrameCount(pcmTotal); } else { @@ -56,7 +62,7 @@ Result AudioSourceOpus::postConstruct() { return ERR; } - const opus_int32 bitrate = op_bitrate(m_pOggOpusFile, kLogicalBitstreamIndex); + const opus_int32 bitrate = op_bitrate(m_pOggOpusFile, kEntireStreamLink); if (0 < bitrate) { setBitrate(bitrate / 1000); } else { From 7bd964c944c9cb87d1a7bcde517d01af186224ce Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 21:07:03 +0100 Subject: [PATCH 157/481] Review: Add comment explaining why the type 'long' is used here --- src/sources/audiosourceoggvorbis.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index a96028a5ddf..e30edbcb733 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -124,6 +124,10 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( while (0 < numberOfFramesRemaining) { float** pcmChannels; int currentSection; + // Use 'long' here, because ov_read_float() returns this type. + // This is an exception from the rule not to any types with + // differing sizes on different platforms. + // https://bugs.launchpad.net/mixxx/+bug/1094143 const long readResult = ov_read_float(&m_vf, &pcmChannels, numberOfFramesRemaining, ¤tSection); if (0 < readResult) { From 2d0685bc6a9d4a472c57c595fd9b662b074d95fe Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 21:08:08 +0100 Subject: [PATCH 158/481] Review: Delete old file headers --- src/sources/soundsource.cpp | 17 ----------------- src/sources/soundsource.h | 19 +------------------ src/sources/soundsourcecoreaudio.cpp | 15 --------------- src/sources/soundsourcecoreaudio.h | 18 ------------------ src/sources/soundsourceffmpeg.cpp | 23 ----------------------- src/sources/soundsourceffmpeg.h | 17 ----------------- src/sources/soundsourceflac.cpp | 15 --------------- src/sources/soundsourceflac.h | 17 ----------------- src/sources/soundsourcemp3.cpp | 16 ---------------- src/sources/soundsourcemp3.h | 17 ----------------- src/sources/soundsourceoggvorbis.cpp | 16 ---------------- src/sources/soundsourceoggvorbis.h | 16 ---------------- 12 files changed, 1 insertion(+), 205 deletions(-) diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index ae10fb1750c..5ae01738f78 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -1,20 +1,3 @@ -/*************************************************************************** - soundsource.cpp - description - ------------------- - begin : Wed Feb 20 2002 - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #include "sources/soundsource.h" #include "metadata/trackmetadata.h" diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index d5f17a35c4d..b4988e01c6d 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -1,20 +1,3 @@ -/*************************************************************************** - soundsource.h - description - ------------------- - begin : Wed Feb 20 2002 - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #ifndef SOUNDSOURCE_H #define SOUNDSOURCE_H @@ -26,7 +9,7 @@ 4 - Mixxx 1.11.0 Pre (added composer field to SoundSource) 5 - Mixxx 1.12.0 Pre (added album artist and grouping fields to SoundSource) 6 - Mixxx 1.12.0 Pre (added cover art suppport) - 7 - Mixxx 1.13.0 New AudioSource API + 7 - Mixxx 1.13.0 New SoundSource/AudioSource API */ #include "sources/audiosource.h" diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index 35efdbf8e21..69b092dfc2a 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -1,18 +1,3 @@ -/** - * \file soundsource.cpp - * \author Albert Santoni - * \date Dec 12, 2010 - */ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #include "sources/soundsourcecoreaudio.h" #include "sources/audiosourcecoreaudio.h" diff --git a/src/sources/soundsourcecoreaudio.h b/src/sources/soundsourcecoreaudio.h index 2cebcbc2976..dd984fa8177 100644 --- a/src/sources/soundsourcecoreaudio.h +++ b/src/sources/soundsourcecoreaudio.h @@ -1,21 +1,3 @@ -/** - * \file soundsourcecoreaudio.h - * \class SoundSourceCoreAudio - * \brief Decodes M4As (etc) using the AudioToolbox framework included as - * part of Core Audio on OS X (and iOS). - * \author Albert Santoni - * \date Dec 12, 2010 - */ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #ifndef SOUNDSOURCECOREAUDIO_H #define SOUNDSOURCECOREAUDIO_H diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index 12768b91712..f68947039cb 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -1,26 +1,3 @@ -/* -*- mode:C++; indent-tabs-mode:t; tab-width:8; c-basic-offset:4; -*- */ -/*************************************************************************** - soundsourceffmpeg.cpp - ffmpeg decoder - ------------------- - copyright : (C) 2007 by Cedric GESTES - (C) 2012-2014 by Tuukka Pasanen - email : tuukka.pasanen@ilmi.fi - - This one tested with FFMPEG 0.10/0.11/1.0/1.1/1.2/2,0/2,1/GIT - Libav 0.8/9/GIT - FFMPEG below 0.10 WON'T work. If you like to it work you can - allways send a patch but it's mostly not worth it! -***************************************************************************/ - -/*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -***************************************************************************/ - #include "sources/soundsourceffmpeg.h" #include "sources/audiosourceffmpeg.h" diff --git a/src/sources/soundsourceffmpeg.h b/src/sources/soundsourceffmpeg.h index 60ea3431cb3..ef96c9c9c5d 100644 --- a/src/sources/soundsourceffmpeg.h +++ b/src/sources/soundsourceffmpeg.h @@ -1,20 +1,3 @@ -/* -*- mode:C++; indent-tabs-mode:t; tab-width:8; c-basic-offset:4; -*- */ -/*************************************************************************** - soundsourceffmpeg.h - ffmpeg decoder - ------------------- - copyright : (C) 2003 by Cedric GESTES - email : goctaf@gmail.com - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #ifndef SOUNDSOURCEFFMPEG_H #define SOUNDSOURCEFFMPEG_H diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 66643163f22..2009ce354d1 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -1,18 +1,3 @@ -/** - * \file soundsourceflac.cpp - * \author Bill Good - * \date May 22, 2010 - */ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #include "sources/soundsourceflac.h" #include "metadata/trackmetadatataglib.h" diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index 01c5d02c03f..9f573358763 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -1,20 +1,3 @@ -/** - * \file sourdsourceflac.h - * \class SoundSourceFLAC - * \brief Decodes FLAC files using libFLAC for Mixxx. - * \author Bill Good - * \date May 22, 2010 - */ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #ifndef SOUNDSOURCEFLAC_H #define SOUNDSOURCEFLAC_H diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index ae257d95333..134e928c419 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -1,19 +1,3 @@ -/*************************************************************************** - soundsourcemp3.cpp - description - ------------------- - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #include "sources/soundsourcemp3.h" #include "sources/audiosourcemp3.h" diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h index 47056bf20fb..3fa243eca90 100644 --- a/src/sources/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -1,20 +1,3 @@ -/*************************************************************************** - soundsourcemp3.h - description - ------------------- - begin : Wed Feb 20 2002 - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #ifndef SOUNDSOURCEMP3_H #define SOUNDSOURCEMP3_H diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index fe0122256b0..ba15ed96598 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -1,19 +1,3 @@ -/*************************************************************************** - soundsourceoggvorbis.cpp - ogg vorbis decoder - ------------------- - copyright : (C) 2003 by Svein Magne Bang - email : - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #include "sources/soundsourceoggvorbis.h" #include "sources/audiosourceoggvorbis.h" diff --git a/src/sources/soundsourceoggvorbis.h b/src/sources/soundsourceoggvorbis.h index 3eda6b960e2..c50b3c503ac 100644 --- a/src/sources/soundsourceoggvorbis.h +++ b/src/sources/soundsourceoggvorbis.h @@ -1,19 +1,3 @@ -/*************************************************************************** - soundsourceoggvorbis.h - ogg vorbis decoder - ------------------- - copyright : (C) 2003 by Svein Magne Bang - email : - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #ifndef SOUNDSOURCEOGGVORBIS_H #define SOUNDSOURCEOGGVORBIS_H From ed82a15eaf5285180f520769ccf000ca696853d5 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 21:08:47 +0100 Subject: [PATCH 159/481] Review: Add a note about code that is commented out --- src/sources/audiosourcecoreaudio.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index afbdbc54333..843298d3da4 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -109,6 +109,10 @@ Result AudioSourceCoreAudio::postConstruct() { setChannelCount(m_outputFormat.NumberChannels()); setFrameRate(m_inputFormat.mSampleRate); + // NOTE(uklotzde): This is what I found when migrating + // the code from SoundSource (sample-oriented) to the new + // AudioSource (frame-oriented) API. It is not documented + // when m_headerFrames > 0 and what the consequences are. setFrameCount(totalFrameCount/* - m_headerFrames*/); //Seek to position 0, which forces us to skip over all the header frames. From 4cc4f8deae8ecef0d343231a8c9915688e6ab4a1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 21:09:23 +0100 Subject: [PATCH 160/481] Review: Add some comments in AudioSourceFFmpeg --- src/sources/audiosourceffmpeg.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 997cfe781f2..1c67b725581 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -68,7 +68,7 @@ Result AudioSourceFFmpeg::postConstruct() { //debug only (Enable if needed) //av_dump_format(m_pFormatCtx, 0, qBAFilename.constData(), false); - // Find the first video stream + // Find the first audio stream m_iAudioStream=-1; for (i=0; inb_streams; i++) @@ -82,7 +82,7 @@ Result AudioSourceFFmpeg::postConstruct() { return ERR; } - // Get a pointer to the codec context for the video stream + // Get a pointer to the codec context for the audio stream m_pCodecCtx=m_pFormatCtx->streams[m_iAudioStream]->codec; // Find the decoder for the audio stream @@ -97,6 +97,7 @@ Result AudioSourceFFmpeg::postConstruct() { } m_pResample = new EncoderFfmpegResample(m_pCodecCtx); + // TODO(XXX): Use AV_SAMPLE_FMT_FLT instead of AV_SAMPLE_FMT_S16 m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_S16); setChannelCount(m_pCodecCtx->channels); From 08675236227fdaf950f7abd83ba5910609dda070 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 21:10:07 +0100 Subject: [PATCH 161/481] Review: Add consistency checks and comments to AudioSourceFLAC --- src/sources/audiosourceflac.cpp | 101 +++++++++++++++++++++++++------- src/sources/audiosourceflac.h | 12 ++-- 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 020c4085bf6..db72111d669 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -11,50 +11,54 @@ namespace { // begin callbacks (have to be regular functions because normal libFLAC isn't C++-aware) FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder*, - FLAC__byte buffer[], size_t *bytes, void *client_data) { + FLAC__byte buffer[], size_t* bytes, void* client_data) { return static_cast(client_data)->flacRead(buffer, bytes); } FLAC__StreamDecoderSeekStatus FLAC_seek_cb(const FLAC__StreamDecoder*, - FLAC__uint64 absolute_byte_offset, void *client_data) { + FLAC__uint64 absolute_byte_offset, void* client_data) { return static_cast(client_data)->flacSeek( absolute_byte_offset); } FLAC__StreamDecoderTellStatus FLAC_tell_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *absolute_byte_offset, void *client_data) { + FLAC__uint64 *absolute_byte_offset, void* client_data) { return static_cast(client_data)->flacTell( absolute_byte_offset); } FLAC__StreamDecoderLengthStatus FLAC_length_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *stream_length, void *client_data) { + FLAC__uint64 *stream_length, void* client_data) { return static_cast(client_data)->flacLength(stream_length); } -FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder*, void *client_data) { +FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder*, void* client_data) { return static_cast(client_data)->flacEOF(); } FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder*, - const FLAC__Frame *frame, const FLAC__int32 * const buffer[], - void *client_data) { + const FLAC__Frame* frame, const FLAC__int32* const buffer[], + void* client_data) { return static_cast(client_data)->flacWrite(frame, buffer); } void FLAC_metadata_cb(const FLAC__StreamDecoder*, - const FLAC__StreamMetadata *metadata, void *client_data) { + const FLAC__StreamMetadata* metadata, void* client_data) { static_cast(client_data)->flacMetadata(metadata); } void FLAC_error_cb(const FLAC__StreamDecoder*, - FLAC__StreamDecoderErrorStatus status, void *client_data) { + FLAC__StreamDecoderErrorStatus status, void* client_data) { static_cast(client_data)->flacError(status); } // end callbacks + +const unsigned kBitsPerSampleDefault = 0; + } + AudioSourceFLAC::AudioSourceFLAC(QUrl url) : AudioSource(url), m_file(getLocalFileName()), @@ -63,6 +67,7 @@ AudioSourceFLAC::AudioSourceFLAC(QUrl url) m_maxBlocksize(0), m_minFramesize(0), m_maxFramesize(0), + m_bitsPerSample(kBitsPerSampleDefault), m_sampleScale(kSampleValueZero), m_decodeSampleBufferReadOffset(0), m_decodeSampleBufferWriteOffset(0), @@ -165,6 +170,10 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( // if our buffer from libflac is empty (either because we explicitly cleared // it or because we've simply used all the samples), ask for a new buffer if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { + // Documentation of FLAC__stream_decoder_process_single(): + // "Depending on what was decoded, the metadata or write callback + // will be called with the decoded metadata block or audio frame." + // See also: https://xiph.org/flac/api/group__flac__stream__decoder.html#ga9d6df4a39892c05955122cf7f987f856 if (FLAC__stream_decoder_process_single(m_decoder)) { if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { @@ -217,7 +226,7 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( // flac callback methods FLAC__StreamDecoderReadStatus AudioSourceFLAC::flacRead(FLAC__byte buffer[], - size_t *bytes) { + size_t* bytes) { *bytes = m_file.read((char*) buffer, *bytes); if (*bytes > 0) { return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; @@ -238,7 +247,7 @@ FLAC__StreamDecoderSeekStatus AudioSourceFLAC::flacSeek(FLAC__uint64 offset) { } } -FLAC__StreamDecoderTellStatus AudioSourceFLAC::flacTell(FLAC__uint64 *offset) { +FLAC__StreamDecoderTellStatus AudioSourceFLAC::flacTell(FLAC__uint64* offset) { if (m_file.isSequential()) { return FLAC__STREAM_DECODER_TELL_STATUS_UNSUPPORTED; } @@ -247,7 +256,7 @@ FLAC__StreamDecoderTellStatus AudioSourceFLAC::flacTell(FLAC__uint64 *offset) { } FLAC__StreamDecoderLengthStatus AudioSourceFLAC::flacLength( - FLAC__uint64 *length) { + FLAC__uint64* length) { if (m_file.isSequential()) { return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; } @@ -263,7 +272,7 @@ FLAC__bool AudioSourceFLAC::flacEOF() { } FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( - const FLAC__Frame *frame, const FLAC__int32 * const buffer[]) { + const FLAC__Frame* frame, const FLAC__int32* const buffer[]) { // decode buffer must be empty before decoding the next frame DEBUG_ASSERT(m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset); // reset decode buffer @@ -324,16 +333,64 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; } -void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata *metadata) { +void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { + // https://xiph.org/flac/api/group__flac__stream__decoder.html#ga43e2329c15731c002ac4182a47990f85 + // "...one STREAMINFO block, followed by zero or more other metadata blocks." + // "...by default the decoder only calls the metadata callback for the STREAMINFO block..." + // "...always before the first audio frame (i.e. write callback)." switch (metadata->type) { case FLAC__METADATA_TYPE_STREAMINFO: - setChannelCount(metadata->data.stream_info.channels); - setFrameRate(metadata->data.stream_info.sample_rate); - setFrameCount(metadata->data.stream_info.total_samples); - m_sampleScale = kSampleValuePeak - / sample_type( - FLAC__int32(1) - << metadata->data.stream_info.bits_per_sample); + { + const size_type channelCount = metadata->data.stream_info.channels; + DEBUG_ASSERT(kChannelCountDefault != channelCount); + if (getChannelCount() == kChannelCountDefault) { + // not set before + setChannelCount(channelCount); + } else { + // already set before -> check for consistency + if (getChannelCount() != channelCount) { + qWarning() << "Unexpected channel count:" + << channelCount << " <> " << getChannelCount(); + } + } + const size_type frameRate = metadata->data.stream_info.sample_rate; + DEBUG_ASSERT(kFrameRateDefault != frameRate); + if (getFrameRate() == kFrameRateDefault) { + // not set before + setFrameRate(frameRate); + } else { + // already set before -> check for consistency + if (getFrameRate() != frameRate) { + qWarning() << "Unexpected frame/sample rate:" + << frameRate << " <> " << getFrameRate(); + } + } + const size_type frameCount = metadata->data.stream_info.total_samples; + DEBUG_ASSERT(kFrameCountDefault != frameCount); + if (getFrameCount() == kFrameCountDefault) { + // not set before + setFrameCount(frameCount); + } else { + // already set before -> check for consistency + if (getFrameCount() != frameCount) { + qWarning() << "Unexpected frame count:" + << frameCount << " <> " << getFrameCount(); + } + } + const unsigned bitsPerSample = metadata->data.stream_info.bits_per_sample; + DEBUG_ASSERT(kBitsPerSampleDefault != bitsPerSample); + if (kBitsPerSampleDefault == m_bitsPerSample) { + // not set before + m_bitsPerSample = bitsPerSample; + m_sampleScale = kSampleValuePeak + / sample_type(FLAC__int32(1) << bitsPerSample); + } else { + // already set before -> check for consistency + if (bitsPerSample != m_bitsPerSample) { + qWarning() << "Unexpected bits per sample:" + << bitsPerSample << " <> " << m_bitsPerSample; + } + } m_minBlocksize = metadata->data.stream_info.min_blocksize; m_maxBlocksize = metadata->data.stream_info.max_blocksize; m_minFramesize = metadata->data.stream_info.min_framesize; @@ -342,7 +399,9 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata *metadata) { m_decodeSampleBufferWriteOffset = 0; m_decodeSampleBuffer.resize(m_maxBlocksize * getChannelCount()); break; + } default: + // Ignore all other metadata types break; } } diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 0b3f9e7e8aa..79de20be369 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -25,14 +25,14 @@ class AudioSourceFLAC: public AudioSource { sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; // callback methods - FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t *bytes); + FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t* bytes); FLAC__StreamDecoderSeekStatus flacSeek(FLAC__uint64 offset); - FLAC__StreamDecoderTellStatus flacTell(FLAC__uint64 *offset); - FLAC__StreamDecoderLengthStatus flacLength(FLAC__uint64 *length); + FLAC__StreamDecoderTellStatus flacTell(FLAC__uint64* offset); + FLAC__StreamDecoderLengthStatus flacLength(FLAC__uint64* length); FLAC__bool flacEOF(); FLAC__StreamDecoderWriteStatus flacWrite(const FLAC__Frame *frame, - const FLAC__int32 * const buffer[]); - void flacMetadata(const FLAC__StreamMetadata *metadata); + const FLAC__int32* const buffer[]); + void flacMetadata(const FLAC__StreamMetadata* metadata); void flacError(FLAC__StreamDecoderErrorStatus status); private: @@ -58,6 +58,8 @@ class AudioSourceFLAC: public AudioSource { size_type m_maxBlocksize; size_type m_minFramesize; size_type m_maxFramesize; + unsigned m_bitsPerSample; + sample_type m_sampleScale; typedef std::vector SampleBuffer; From ac3f8467c9ffaea689aaca7f9cfc9984e9bfadfd Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 22:09:24 +0100 Subject: [PATCH 162/481] Review: Correctly read/write TDRC (ID3v2.4.0) or TYER + TDAT (ID3v2.2.0/2.3.0) --- src/metadata/trackmetadatataglib.cpp | 55 ++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 6f06f84a09d..02414329b97 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -14,6 +14,7 @@ #include #include +#include #include namespace Mixxx { @@ -187,10 +188,36 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, } // ID3v2.4.0: TDRC replaces TYER + TDAT - const TagLib::ID3v2::FrameList recordingDateFrame( - tag.frameListMap()["TDRC"]); - if (!recordingDateFrame.isEmpty()) { - pTrackMetadata->setYear(toQStringFirst(recordingDateFrame)); + const QString recordingTime( + toQStringFirst(tag.frameListMap()["TDRC"])); + if (!recordingTime.isEmpty()) { + pTrackMetadata->setYear(recordingTime); + } else { + // Fallback to TYER + TDAT according to http://id3.org/id3v2.3.0 + // NOTE(uklotzde): We only check the length of both fields, but + // not if they actually contain numeric strings. + const QString recordingYear( + toQStringFirst(tag.frameListMap()["TYER"]).trimmed()); + // "TYER: The 'Year' frame is a numeric string with a year of the + // recording. This frame is always four characters long (until + // the year 10000)." + QString year(recordingYear); + if (4 == recordingYear.length()) { + // "TDAT: The 'Date' frame is a numeric string in the DDMM + // format containing the date for the recording. This field + // is always four characters long. + const QString recordingDate( + toQStringFirst(tag.frameListMap()["TDAT"]).trimmed()); + if (4 == recordingDate.length()) { + year += '-'; + year += recordingDate.left(2); + year += '-'; + year += recordingDate.right(2); + } + } + if (!year.isEmpty()) { + pTrackMetadata->setYear(year); + } } const TagLib::ID3v2::FrameList bpmFrame(tag.frameListMap()["TBPM"]); @@ -579,7 +606,27 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, // ID3v2.4.0: TDRC replaces TYER + TDAT writeID3v2TextIdentificationFrame(pTag, "TDRC", trackMetadata.getYear()); + } else { + // Fallback: Write TYER and TDAT + const QStringList yearParts(trackMetadata.getYear().split('-')); + if (0 < yearParts.length()) { + const QString year(yearParts[0].trimmed()); + if (4 == year.length()) { + writeID3v2TextIdentificationFrame(pTag, "TYER", + year); // yyyy + if (3 == yearParts.length()) { + const QString month(yearParts[1].trimmed()); + const QString day(yearParts[2].trimmed()); + if ((2 == month.length()) && (2 == day.length())) { + // yyyy-MM-dd + writeID3v2TextIdentificationFrame(pTag, "TDAT", + month + day); // MMdd + } + } + } + } } + // TODO(uklotzde): Write TXXX - REPLAYGAIN_TRACK_GAIN return true; From 996cfaf46048a4225dc6a7a046acd1121a627e41 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 22:27:28 +0100 Subject: [PATCH 163/481] Review: Add comments about the APEv2 'Year' item --- src/metadata/trackmetadatataglib.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 02414329b97..024f122cb79 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -274,6 +274,10 @@ void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { pTrackMetadata->setGrouping(toQString(tag.itemListMap()["Grouping"])); } + // The release date (ISO 8601 without 'T' separator between date and time) + // according to the mapping used by MusicBrainz Picard. + // http://wiki.hydrogenaud.io/index.php?title=APE_date + // https://picard.musicbrainz.org/docs/mappings if (tag.itemListMap().contains("Year")) { pTrackMetadata->setYear(toQString(tag.itemListMap()["Year"])); } From 5e012c9dc1a77f25906e6e74e4aa2af63e6f012a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 22:32:18 +0100 Subject: [PATCH 164/481] Review: Add comments about the Ogg 'DATE' field --- src/metadata/trackmetadatataglib.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 024f122cb79..1df03aa0ae8 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -343,6 +343,9 @@ void readXiphComment(TrackMetadata* pTrackMetadata, toQStringFirst(tag.fieldListMap()["GROUPING"])); } + // The release date formatted according to ISO 8601. Might + // be followed by a space character and arbitrary text. + // http://age.hobba.nl/audio/mirroredpages/ogg-tagging.html if (tag.fieldListMap().contains("DATE")) { pTrackMetadata->setYear(toQStringFirst(tag.fieldListMap()["DATE"])); } From d86e7ddfff62e8f7451c4b8b88da03afb26e5b2f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 3 Feb 2015 23:13:31 +0100 Subject: [PATCH 165/481] Review: Only write a numeric year if possible --- src/metadata/trackmetadatataglib.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 1df03aa0ae8..d7c40e4108d 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -567,11 +567,20 @@ bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { pTag->setAlbum(toTagLibString(trackMetadata.getAlbum())); pTag->setGenre(toTagLibString(trackMetadata.getGenre())); pTag->setComment(toTagLibString(trackMetadata.getComment())); - bool yearValid = false; - uint year = trackMetadata.getYear().toUInt(&yearValid); - if (yearValid && (year > 0)) { - pTag->setYear(year); + + // Only write the year into the common tag if the corresponding + // field contains a 4-character numeric string. + const QString yearString(trackMetadata.getYear().trimmed()); + if (4 == yearString.length()) { + bool yearValid = false; + uint year = trackMetadata.getYear().toUInt(&yearValid); + if (yearValid && (year > 0)) { + pTag->setYear(year); + } } + // Derived tags might be able to write the complete string + // from trackMetadata.getYear() into the corresponding field. + bool trackNumberValid = false; uint track = trackMetadata.getTrackNumber().toUInt(&trackNumberValid); if (trackNumberValid && (track > 0)) { From 0f15cc46afac82d373b463ed42cc4829473baed5 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 4 Feb 2015 08:16:53 +0100 Subject: [PATCH 166/481] Prefer DATE over YEAR metadata for Opus files --- src/sources/soundsourceopus.cpp | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 74849bf5803..c3ce1d945eb 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -66,12 +66,19 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } } #else + // Beginning with version 1.9.0 TagLib supports the Opus format. + // Until this becomes the minimum version required by Mixxx tags + // in .opus files must also be parsed using opusfile. The following + // code should removed as soon as it is no longer needed! + // + // NOTE(uklotzde): The following code has been found in SoundSourceOpus + // and will not be improved. We are aware of its shortcomings like + // the lack of proper error handling. + int error = 0; OggOpusFileOwner l_ptrOpusFile( op_open_file(qbaFilename.constData(), &error)); - // From Taglib 1.9.x Opus is supported - // Before that we have parse tags by this code int i = 0; const OpusTags *l_ptrOpusTags = op_tags(l_ptrOpusFile, -1); @@ -81,8 +88,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { pMetadata->setDuration( op_pcm_total(l_ptrOpusFile, -1) / pMetadata->getSampleRate()); - // This is left for debug reasons !! - // qDebug() << "opus: We have " << l_ptrOpusTags->comments; + bool hasDate = false; for (i = 0; i < l_ptrOpusTags->comments; ++i) { QString l_SWholeTag = QString(l_ptrOpusTags->user_comments[i]); QString l_STag = l_SWholeTag.left(l_SWholeTag.indexOf("=")); @@ -94,8 +100,13 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { pMetadata->setAlbum(l_SPayload); } else if (!l_STag.compare("BPM")) { pMetadata->setBpm(l_SPayload.toFloat()); - } else if (!l_STag.compare("YEAR") || !l_STag.compare("DATE")) { - pMetadata->setYear(l_SPayload); + } else if (!l_STag.compare("DATE")) { + // Prefer "DATE" over "YEAR" + pMetadata->setYear(l_SPayload.trimmed()); + // Avoid to overwrite "DATE" with "YEAR" + hasDate |= !pMetadata->getYear().isEmpty(); + } else if (!hasDate && !l_STag.compare("YEAR")) { + pMetadata->setYear(l_SPayload.trimmed()); } else if (!l_STag.compare("GENRE")) { pMetadata->setGenre(l_SPayload); } else if (!l_STag.compare("TRACKNUMBER")) { @@ -106,11 +117,8 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { pMetadata->setAlbumArtist(l_SPayload); } else if (!l_STag.compare("TITLE")) { pMetadata->setTitle(l_SPayload); - } else if (!l_STag.compare("REPLAYGAIN_TRACK_PEAK")) { } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) { pMetadata->setReplayGainDbString (l_SPayload); - } else if (!l_STag.compare("REPLAYGAIN_ALBUM_PEAK")) { - } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) { } // This is left fot debug reasons!! From 2463907a881e5c62bd5c926637f852492f73bda7 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 4 Feb 2015 22:45:57 +0100 Subject: [PATCH 167/481] Support for ISO 8601 date/time representation in metadata --- src/metadata/trackmetadata.h | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 7d55ad9d001..b66cb40ebe3 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -1,7 +1,7 @@ #ifndef TRACKMETADATA_H #define TRACKMETADATA_H -#include +#include #include @@ -54,7 +54,7 @@ class TrackMetadata { m_comment = comment; } - // year, date or date/time + // year, date or date/time formatted according to ISO 8601 inline const QString& getYear() const { return m_year; } @@ -175,6 +175,25 @@ class TrackMetadata { static double parseReplayGain(QString sReplayGain, bool* pValid = 0); static QString formatReplayGain(double replayGain); + // Normalize a year string + inline static QString normalizeYear(QString year) { + return year.simplified().replace(" ", ""); + } + + // Parse an format date/time values according to ISO 8601 + inline static QDate parseDate(QString str) { + return QDate::fromString(normalizeYear(str), Qt::ISODate); + } + inline static QDateTime parseDateTime(QString str) { + return QDateTime::fromString(normalizeYear(str), Qt::ISODate); + } + inline static QString formatDate(QDate date) { + return date.toString(Qt::ISODate); + } + inline static QString formatDateTime(QDateTime dateTime) { + return dateTime.toString(Qt::ISODate); + } + private: QString m_artist; QString m_title; From db052b10715c3e5666c39b4a7899a849021f70c8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 4 Feb 2015 22:46:42 +0100 Subject: [PATCH 168/481] Correctly read/write ID3v2.3.0 TYER/TDAT metadata frames --- src/metadata/trackmetadatataglib.cpp | 53 +++++++++++----------------- src/test/metadatatest.cpp | 45 ++++++++++++++++++++++- 2 files changed, 65 insertions(+), 33 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index d7c40e4108d..950523152fc 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -14,15 +14,26 @@ #include #include -#include #include namespace Mixxx { -// static namespace { + const bool kDebugMetadata = false; +// http://id3.org/id3v2.3.0 +// "TYER: The 'Year' frame is a numeric string with a year of the +// recording. This frame is always four characters long (until +// the year 10000)." +const QString ID3V2_TYER_FORMAT("yyyy"); + +// http://id3.org/id3v2.3.0 +// "TDAT: The 'Date' frame is a numeric string in the DDMM +// format containing the date for the recording. This field +// is always four characters long." +const QString ID3V2_TDAT_FORMAT("ddMM"); + // Taglib strings can be NULL and using it could cause some segfaults, // so in this case it will return a QString() inline QString toQString(const TagLib::String& tString) { @@ -193,26 +204,16 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, if (!recordingTime.isEmpty()) { pTrackMetadata->setYear(recordingTime); } else { - // Fallback to TYER + TDAT according to http://id3.org/id3v2.3.0 - // NOTE(uklotzde): We only check the length of both fields, but - // not if they actually contain numeric strings. + // Fallback to TYER + TDAT const QString recordingYear( toQStringFirst(tag.frameListMap()["TYER"]).trimmed()); - // "TYER: The 'Year' frame is a numeric string with a year of the - // recording. This frame is always four characters long (until - // the year 10000)." QString year(recordingYear); if (4 == recordingYear.length()) { - // "TDAT: The 'Date' frame is a numeric string in the DDMM - // format containing the date for the recording. This field - // is always four characters long. const QString recordingDate( toQStringFirst(tag.frameListMap()["TDAT"]).trimmed()); if (4 == recordingDate.length()) { - year += '-'; - year += recordingDate.left(2); - year += '-'; - year += recordingDate.right(2); + const QDate date(QDate::fromString(recordingYear + recordingDate, ID3V2_TYER_FORMAT + ID3V2_TDAT_FORMAT)); + year = TrackMetadata::formatDate(date); } } if (!year.isEmpty()) { @@ -623,23 +624,11 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, writeID3v2TextIdentificationFrame(pTag, "TDRC", trackMetadata.getYear()); } else { - // Fallback: Write TYER and TDAT - const QStringList yearParts(trackMetadata.getYear().split('-')); - if (0 < yearParts.length()) { - const QString year(yearParts[0].trimmed()); - if (4 == year.length()) { - writeID3v2TextIdentificationFrame(pTag, "TYER", - year); // yyyy - if (3 == yearParts.length()) { - const QString month(yearParts[1].trimmed()); - const QString day(yearParts[2].trimmed()); - if ((2 == month.length()) && (2 == day.length())) { - // yyyy-MM-dd - writeID3v2TextIdentificationFrame(pTag, "TDAT", - month + day); // MMdd - } - } - } + // Fallback to TYER + TDAT + const QDate date(TrackMetadata::parseDate(trackMetadata.getYear())); + if (date.isValid()) { + writeID3v2TextIdentificationFrame(pTag, "TYER", date.toString(ID3V2_TYER_FORMAT)); + writeID3v2TextIdentificationFrame(pTag, "TDAT", date.toString(ID3V2_TDAT_FORMAT)); } } diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index 6adb5a0083a..16b09cd4847 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -1,6 +1,6 @@ #include -#include "metadata/trackmetadata.h" +#include "metadata/trackmetadatataglib.h" #include "util/math.h" #include @@ -111,4 +111,47 @@ TEST_F(MetadataTest, ParseReplayGainDbInvalid) { parseReplayGain("0 dBA", false, Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED); } +TEST_F(MetadataTest, ID3v2Year) { + const char* kYears[] = { + " 1987 ", + " 2001-01-01", + "2002 -12 - 31 ", + "2015 -02 - 04T 18:43", + "2015 -02 - 04 18:43" + "2015 -02 - 04 18:43 followed by arbitrary text" + }; + // Only ID3v2.3.0 and ID3v2.4.0 are supported for writing + for (int majorVersion = 3; 4 >= majorVersion; ++majorVersion) { + qDebug() << "majorVersion" << majorVersion; + for (size_t i = 0; i < sizeof(kYears) / sizeof(kYears[0]); ++i) { + const QString year(kYears[i]); + qDebug() << "year" << year; + TagLib::ID3v2::Tag tag; + tag.header()->setMajorVersion(majorVersion); + { + Mixxx::TrackMetadata trackMetadata; + trackMetadata.setYear(year); + writeID3v2Tag(&tag, trackMetadata); + } + Mixxx::TrackMetadata trackMetadata; + readID3v2Tag(&trackMetadata, tag); + if (4 > majorVersion) { + // ID3v2.3.0: parsed + formatted + const QString expectedYear(Mixxx::TrackMetadata::normalizeYear(year)); + const QString actualYear(trackMetadata.getYear()); + const QDate expectedDate(Mixxx::TrackMetadata::parseDate(expectedYear)); + if (expectedDate.isValid()) { + // Only the date part can be stored in an ID3v2.3.0 tag + EXPECT_EQ(Mixxx::TrackMetadata::formatDate(expectedDate), actualYear); + } else { + EXPECT_EQ(expectedYear, actualYear); + } + } else { + // ID3v2.4.0: currently unverified/unmodified + EXPECT_EQ(year, trackMetadata.getYear()); + } + } + } +} + } // namespace From 22fdcd9ecd8e0fa992507f5563051a2d32d95732 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 5 Feb 2015 23:38:39 +0100 Subject: [PATCH 169/481] Fix reading and parsing of ReplayGain --- src/sources/soundsourceffmpeg.cpp | 2 +- src/sources/soundsourceopus.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index f68947039cb..84fc647c753 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -140,7 +140,7 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { pMetadata->setTitle(strValue); } else if (!strncmp(FmtTag->key, "REPLAYGAIN_TRACK_PEAK", 20)) { } else if (!strncmp(FmtTag->key, "REPLAYGAIN_TRACK_GAIN", 20)) { - pMetadata->setReplayGainDbString (strValue); + pMetadata->setReplayGain(Mixxx::TrackMetadata::parseReplayGain(strValue)); } else if (!strncmp(FmtTag->key, "REPLAYGAIN_ALBUM_PEAK", 20)) { } else if (!strncmp(FmtTag->key, "REPLAYGAIN_ALBUM_GAIN", 20)) { } diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index c3ce1d945eb..03b3b4beccc 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -118,7 +118,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { } else if (!l_STag.compare("TITLE")) { pMetadata->setTitle(l_SPayload); } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) { - pMetadata->setReplayGainDbString (l_SPayload); + pMetadata->setReplayGain(Mixxx::TrackMetadata::parseReplayGain(l_SPayload)); } // This is left fot debug reasons!! From 22656de6a84e8cabcc5b60d7dabb6793c7274387 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 5 Feb 2015 18:51:55 +0100 Subject: [PATCH 170/481] Universal tag reading function for all TagLib file types --- plugins/soundsourcem4a/soundsourcem4a.cpp | 35 -- plugins/soundsourcem4a/soundsourcem4a.h | 3 - .../soundsourcemediafoundation.cpp | 36 -- .../soundsourcemediafoundation.h | 3 - plugins/soundsourcewv/soundsourcewv.cpp | 35 -- plugins/soundsourcewv/soundsourcewv.h | 3 - src/metadata/audiotagger.cpp | 18 +- src/metadata/trackmetadatataglib.cpp | 519 +++++++++++++----- src/metadata/trackmetadatataglib.h | 47 +- src/sources/soundsource.cpp | 13 +- src/sources/soundsource.h | 4 +- src/sources/soundsourcecoreaudio.cpp | 80 --- src/sources/soundsourcecoreaudio.h | 3 - src/sources/soundsourceflac.cpp | 58 -- src/sources/soundsourceflac.h | 3 - src/sources/soundsourcemp3.cpp | 48 -- src/sources/soundsourcemp3.h | 7 - src/sources/soundsourceoggvorbis.cpp | 40 -- src/sources/soundsourceoggvorbis.h | 3 - src/sources/soundsourceopus.cpp | 45 +- src/sources/soundsourceopus.h | 1 - src/sources/soundsourcesndfile.cpp | 107 ---- src/sources/soundsourcesndfile.h | 3 - src/test/metadatatest.cpp | 4 +- 24 files changed, 434 insertions(+), 684 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index eb30ff5483b..db5e5f98b6a 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -19,8 +19,6 @@ #include "metadata/trackmetadatataglib.h" -#include - namespace Mixxx { QList SoundSourceM4A::supportedFileExtensions() { @@ -34,39 +32,6 @@ SoundSourceM4A::SoundSourceM4A(QUrl url) : SoundSourcePlugin(url, "m4a") { } -Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::MP4::File f(getLocalFileNameBytes().constData()); - - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - - TagLib::MP4::Tag *mp4(f.tag()); - if (mp4) { - readMP4Tag(pMetadata, *mp4); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } - - return OK; -} - -QImage SoundSourceM4A::parseCoverArt() const { - TagLib::MP4::File f(getLocalFileNameBytes().constData()); - TagLib::MP4::Tag *mp4(f.tag()); - if (mp4) { - return readMP4TagCover(*mp4); - } else { - return QImage(); - } -} - Mixxx::AudioSourcePointer SoundSourceM4A::open() const { return Mixxx::AudioSourceM4A::create(getUrl()); } diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 61c34b9a7b8..e934788708a 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -35,9 +35,6 @@ class SoundSourceM4A: public SoundSourcePlugin { explicit SoundSourceM4A(QUrl url); - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - QImage parseCoverArt() const /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; }; diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 574c905c11c..06774b4a719 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -25,8 +25,6 @@ #include "metadata/trackmetadatataglib.h" -#include - #include // static @@ -41,40 +39,6 @@ SoundSourceMediaFoundation::SoundSourceMediaFoundation(QUrl url) : SoundSourcePlugin(url, "m4a") { } -Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - // Must be toLocal8Bit since Windows fopen does not do UTF-8 - TagLib::MP4::File f(getLocalFileNameBytes().constData()); - - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - - TagLib::MP4::Tag *mp4(f.tag()); - if (mp4) { - readMP4Tag(pMetadata, *mp4); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } - - return OK; -} - -QImage SoundSourceMediaFoundation::parseCoverArt() const { - TagLib::MP4::File f(getLocalFileNameBytes().constData()); - TagLib::MP4::Tag *mp4(f.tag()); - if (mp4) { - return Mixxx::readMP4TagCover(*mp4); - } else { - return QImage(); - } -} - Mixxx::AudioSourcePointer SoundSourceMediaFoundation::open() const { return Mixxx::AudioSourceMediaFoundation::create(getUrl()); } diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 5f17bca4658..760b538602b 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -35,9 +35,6 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { explicit SoundSourceMediaFoundation(QUrl url); - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - QImage parseCoverArt() const /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; }; diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index acd1c8c6e0d..60eb7d2a04e 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -3,8 +3,6 @@ #include "metadata/trackmetadatataglib.h" -#include - namespace Mixxx { QList SoundSourceWV::supportedFileExtensions() { @@ -17,39 +15,6 @@ SoundSourceWV::SoundSourceWV(QUrl url) : SoundSourcePlugin(url, "wv") { } -Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::WavPack::File f(getLocalFileNameBytes().constData()); - - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - - TagLib::APE::Tag *ape = f.APETag(); - if (ape) { - readAPETag(pMetadata, *ape); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } - - return OK; -} - -QImage SoundSourceWV::parseCoverArt() const { - TagLib::WavPack::File f(getLocalFileNameBytes().constData()); - TagLib::APE::Tag *ape = f.APETag(); - if (ape) { - return Mixxx::readAPETagCover(*ape); - } else { - return QImage(); - } -} - Mixxx::AudioSourcePointer SoundSourceWV::open() const { return Mixxx::AudioSourceWV::create(getUrl()); } diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 8ebe9fb822d..5217671404b 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -18,9 +18,6 @@ class SoundSourceWV: public SoundSourcePlugin { explicit SoundSourceWV(QUrl url); - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - QImage parseCoverArt() const /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; }; diff --git a/src/metadata/audiotagger.cpp b/src/metadata/audiotagger.cpp index ad8bbd6e639..a62f86069af 100644 --- a/src/metadata/audiotagger.cpp +++ b/src/metadata/audiotagger.cpp @@ -51,41 +51,41 @@ bool AudioTagger::save(const Mixxx::TrackMetadata& trackMetadata) { if (filePath.endsWith(".mp3", Qt::CaseInsensitive)) { QScopedPointer pMpegFile( new TagLib::MPEG::File(filePathChars)); - writeID3v2Tag(pMpegFile->ID3v2Tag(true), trackMetadata); // mandatory - writeAPETag(pMpegFile->APETag(false), trackMetadata); // optional + writeTrackMetadataIntoID3v2Tag(pMpegFile->ID3v2Tag(true), trackMetadata); // mandatory + writeTrackMetadataIntoAPETag(pMpegFile->APETag(false), trackMetadata); // optional pFile.reset(pMpegFile.take()); // transfer ownership } else if (filePath.endsWith(".m4a", Qt::CaseInsensitive)) { QScopedPointer pMp4File( new TagLib::MP4::File(filePathChars)); - writeMP4Tag(pMp4File->tag(), trackMetadata); + writeTrackMetadataIntoMP4Tag(pMp4File->tag(), trackMetadata); pFile.reset(pMp4File.take()); // transfer ownership } else if (filePath.endsWith(".ogg", Qt::CaseInsensitive)) { QScopedPointer pOggFile( new TagLib::Ogg::Vorbis::File(filePathChars)); - writeXiphComment(pOggFile->tag(), trackMetadata); + writeTrackMetadataIntoXiphComment(pOggFile->tag(), trackMetadata); pFile.reset(pOggFile.take()); // transfer ownership } else if (filePath.endsWith(".flac", Qt::CaseInsensitive)) { QScopedPointer pFlacFile( new TagLib::FLAC::File(filePathChars)); - writeXiphComment(pFlacFile->xiphComment(true), trackMetadata); // mandatory - writeID3v2Tag(pFlacFile->ID3v2Tag(false), trackMetadata); // optional + writeTrackMetadataIntoXiphComment(pFlacFile->xiphComment(true), trackMetadata); // mandatory + writeTrackMetadataIntoID3v2Tag(pFlacFile->ID3v2Tag(false), trackMetadata); // optional pFile.reset(pFlacFile.take()); // transfer ownership } else if (filePath.endsWith(".wav", Qt::CaseInsensitive)) { QScopedPointer pWavFile( new TagLib::RIFF::WAV::File(filePathChars)); - writeID3v2Tag(pWavFile->tag(), trackMetadata); + writeTrackMetadataIntoID3v2Tag(pWavFile->tag(), trackMetadata); pFile.reset(pWavFile.take()); // transfer ownership } else if (filePath.endsWith(".aif", Qt::CaseInsensitive) || filePath.endsWith(".aiff", Qt::CaseInsensitive)) { QScopedPointer pAiffFile( new TagLib::RIFF::AIFF::File(filePathChars)); - writeID3v2Tag(pAiffFile->tag(), trackMetadata); + writeTrackMetadataIntoID3v2Tag(pAiffFile->tag(), trackMetadata); pFile.reset(pAiffFile.take()); // transfer ownership #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) } else if (filePath.endsWith(".opus", Qt::CaseInsensitive)) { QScopedPointer pOpusFile( new TagLib::Ogg::Opus::File(filePathChars)); - writeXiphComment(pOpusFile->tag(), trackMetadata); + writeTrackMetadataIntoXiphComment(pOpusFile->tag(), trackMetadata); pFile.reset(pOpusFile.take()); // transfer ownership #endif } else { diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 950523152fc..e5bca15c378 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -1,26 +1,59 @@ #include "metadata/trackmetadatataglib.h" +#include "util/assert.h" + +// TagLib has support for the Ogg Opus file format since version 1.9 +#define TAGLIB_HAS_OPUSFILE \ + ((TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9))) + #include +#include +#include + +#include +#include +#include +#include +#if TAGLIB_HAS_OPUSFILE +#include +#endif +#include +#include +#include + #include #include -#include #include #include #include -#include -#include #include -#include #include #include -#include - namespace Mixxx { namespace { -const bool kDebugMetadata = false; +const QString kFileTypeMP3("mp3"); +const QString kFileTypeMP4("mp4"); +const QString kFileTypeFLAC("flac"); +const QString kFileTypeOggVorbis("ogg"); +const QString kFileTypeOggOpus("opus"); +const QString kFileTypeAIFF("aiff"); +const QString kFileTypeWAV("wav"); +const QString kFileTypeWavPack("wv"); + +// Deduce the file type from the file name +QString getFileTypeFromFileName(QString fileName) { + const QString fileType(fileName.section(".", -1).toLower().trimmed()); + if ("m4a" == fileType) { + return kFileTypeMP4; + } + if ("aif" == fileType) { + return kFileTypeAIFF; + } + return fileType; +} // http://id3.org/id3v2.3.0 // "TYER: The 'Year' frame is a numeric string with a year of the @@ -88,6 +121,8 @@ inline TagLib::String toTagLibString(const QString& str, TagLib::String::Type te } inline bool parseBpm(TrackMetadata* pTrackMetadata, QString sBpm) { + DEBUG_ASSERT(pTrackMetadata); + bool bpmValid = false; double bpm = TrackMetadata::parseBpm(sBpm, &bpmValid); if (bpmValid) { @@ -97,6 +132,8 @@ inline bool parseBpm(TrackMetadata* pTrackMetadata, QString sBpm) { } inline bool parseReplayGain(TrackMetadata* pTrackMetadata, QString sReplayGain) { + DEBUG_ASSERT(pTrackMetadata); + bool replayGainValid = false; double replayGain = TrackMetadata::parseReplayGain(sReplayGain, &replayGainValid); if (replayGainValid) { @@ -105,37 +142,239 @@ inline bool parseReplayGain(TrackMetadata* pTrackMetadata, QString sReplayGain) return replayGainValid; } -} // anonymous namespace +void readAudioProperties(TrackMetadata* pTrackMetadata, + const TagLib::AudioProperties& audioProperties) { + DEBUG_ASSERT(pTrackMetadata); + + pTrackMetadata->setChannels(audioProperties.channels()); + pTrackMetadata->setSampleRate(audioProperties.sampleRate()); + pTrackMetadata->setDuration(audioProperties.length()); + pTrackMetadata->setBitrate(audioProperties.bitrate()); +} bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& file) { - if (kDebugMetadata) { - qDebug() << "Parsing" << file.name(); - } - - if (file.isValid()) { - const TagLib::AudioProperties *properties = file.audioProperties(); - if (properties) { - pTrackMetadata->setChannels(properties->channels()); - pTrackMetadata->setSampleRate(properties->sampleRate()); - pTrackMetadata->setDuration(properties->length()); - pTrackMetadata->setBitrate(properties->bitrate()); - - if (kDebugMetadata) { - qDebug() << "TagLib" << "channels" - << pTrackMetadata->getChannels() << "sampleRate" - << pTrackMetadata->getSampleRate() << "bitrate" - << pTrackMetadata->getBitrate() << "duration" - << pTrackMetadata->getDuration(); - } + if (!file.isValid()) { + return false; + } + if (!pTrackMetadata) { + // implicitly succesful + return true; + } + const TagLib::AudioProperties* pAudioProperties = + file.audioProperties(); + if (!pAudioProperties) { + return false; + } + readAudioProperties(pTrackMetadata, *pAudioProperties); + return true; +} - return true; +} // anonymous namespace + +Result readTrackMetadataFromFile(TrackMetadata* pTrackMetadata, QString fileName) { + return readTrackMetadataAndCoverArtFromFile(pTrackMetadata, NULL, fileName); +} + +Result readCoverArtFromFile(QImage* pCoverArt, QString fileName) { + return readTrackMetadataAndCoverArtFromFile(NULL, pCoverArt, fileName); +} + +Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImage* pCoverArt, QString fileName) { + const QString fileType(getFileTypeFromFileName(fileName)); + + qDebug() << "Reading tags of file" << fileName << "with type" << fileType; + + // Rationale: If a file contains different types of tags only + // a single type of tag will be read. Tag types are read in a + // fixed order. Both track metadata and cover art will be read + // from the same tag types. Only the first available tag type + // is read and data in subsequent tags is ignored. + + if (kFileTypeMP3 == fileType) { + TagLib::MPEG::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::ID3v2::Tag* pID3v2Tag = file.ID3v2Tag(); + if (pID3v2Tag) { + readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); + readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); + return OK; + } else { + const TagLib::APE::Tag* pAPETag = file.APETag(); + if (pAPETag) { + readTrackMetadataFromAPETag(pTrackMetadata, *pAPETag); + readCoverArtFromAPETag(pCoverArt, *pAPETag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } + } else if (kFileTypeMP4 == fileType) { + TagLib::MP4::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::MP4::Tag* pMP4Tag = file.tag(); + if (pMP4Tag) { + readTrackMetadataFromMP4Tag(pTrackMetadata, *pMP4Tag); + readCoverArtFromMP4Tag(pCoverArt, *pMP4Tag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } else if (kFileTypeFLAC == fileType) { + TagLib::FLAC::File file(fileName.toLocal8Bit().constData()); + if (pCoverArt) { + // FLAC files may contain cover art that is not part + // of any tag. Use the first picture from this list + // as the default picture for the cover art. + TagLib::List covers = file.pictureList(); + if (!covers.isEmpty()) { + std::list::iterator it = covers.begin(); + TagLib::FLAC::Picture* cover = *it; + *pCoverArt = QImage::fromData( + QByteArray(cover->data().data(), cover->data().size())); + } + } + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::Ogg::XiphComment* pXiphComment = + file.hasXiphComment() ? file.xiphComment() : NULL; + if (pXiphComment) { + readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); + readCoverArtFromXiphComment(pCoverArt, *pXiphComment); + return OK; + } else { + const TagLib::ID3v2::Tag* pID3v2Tag = + file.hasID3v2Tag() ? file.ID3v2Tag() : NULL; + if (pID3v2Tag) { + readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); + readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } + } else if (kFileTypeOggVorbis == fileType) { + TagLib::Ogg::Vorbis::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::Ogg::XiphComment* pXiphComment = file.tag(); + if (pXiphComment) { + readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); + readCoverArtFromXiphComment(pCoverArt, *pXiphComment); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } +#if TAGLIB_HAS_OPUSFILE + } else if (kFileTypeOggOpus == fileType) { + TagLib::Ogg::Opus::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::Ogg::XiphComment* pXiphComment = file.tag(); + if (pXiphComment) { + readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); + readCoverArtFromXiphComment(pCoverArt, *pXiphComment); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } +#endif // TAGLIB_HAS_OPUSFILE + } else if (kFileTypeWavPack == fileType) { + TagLib::WavPack::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::APE::Tag* pAPETag = file.APETag(); + if (pAPETag) { + readTrackMetadataFromAPETag(pTrackMetadata, *pAPETag); + readCoverArtFromAPETag(pCoverArt, *pAPETag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } else if (kFileTypeWAV == fileType) { + TagLib::RIFF::WAV::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + // Taglib provides the ID3v2Tag method for WAV files since Version 1.9 +#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) + // NOTE(uklotzde): ID3v2Tag() always returns a valid pointer for WAV files + // whether or not the file actually has an ID3v2 tag! + const TagLib::ID3v2::Tag* pID3v2Tag = file.hasID3v2Tag() ? file.ID3v2Tag() : NULL; +#else + const TagLib::ID3v2::Tag* pID3v2Tag = file.tag(); +#endif + if (pID3v2Tag) { + readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); + readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } else if (kFileTypeAIFF == fileType) { + TagLib::RIFF::AIFF::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::ID3v2::Tag* pID3v2Tag = file.tag(); + if (pID3v2Tag) { + readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); + readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } } } - return false; + + qWarning() << "File" << fileName << "does not contain any supported tags that could be read!"; + return ERR; } -void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { +void readTrackMetadataFromTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { + if (!pTrackMetadata) { + return; // nothing to do + } + pTrackMetadata->setTitle(toQString(tag.title())); pTrackMetadata->setArtist(toQString(tag.artist())); pTrackMetadata->setAlbum(toQString(tag.album())); @@ -151,31 +390,15 @@ void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { if (iTrack > 0) { pTrackMetadata->setTrackNumber(QString::number(iTrack)); } - - if (kDebugMetadata) { - qDebug() << "TagLib" << "title" << pTrackMetadata->getTitle() - << "artist" << pTrackMetadata->getArtist() << "album" - << pTrackMetadata->getAlbum() << "comment" - << pTrackMetadata->getComment() << "genre" - << pTrackMetadata->getGenre() << "year" - << pTrackMetadata->getYear() << "trackNumber" - << pTrackMetadata->getTrackNumber(); - } } -void readID3v2Tag(TrackMetadata* pTrackMetadata, +void readTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) { - - // Print every frame in the file. - if (kDebugMetadata) { - for (TagLib::ID3v2::FrameList::ConstIterator it( - tag.frameList().begin()); it != tag.frameList().end(); ++it) { - qDebug() << "ID3V2" << (*it)->frameID().data() << "-" - << toQString((*it)->toString()); - } + if (!pTrackMetadata) { + return; // nothing to do } - readTag(pTrackMetadata, tag); + readTrackMetadataFromTag(pTrackMetadata, tag); const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()["TPE2"]); if (!albumArtistFrame.isEmpty()) { @@ -250,17 +473,12 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, } } -void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { - if (kDebugMetadata) { - for (TagLib::APE::ItemListMap::ConstIterator it( - tag.itemListMap().begin()); it != tag.itemListMap().end(); - ++it) { - qDebug() << "APE" << toQString((*it).first) << "-" - << toQString((*it).second.toString()); - } +void readTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { + if (!pTrackMetadata) { + return; // nothing to do } - readTag(pTrackMetadata, tag); + readTrackMetadataFromTag(pTrackMetadata, tag); if (tag.itemListMap().contains("Album Artist")) { pTrackMetadata->setAlbumArtist( @@ -294,18 +512,13 @@ void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { } } -void readXiphComment(TrackMetadata* pTrackMetadata, +void readTrackMetadataFromXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag) { - if (kDebugMetadata) { - for (TagLib::Ogg::FieldListMap::ConstIterator it( - tag.fieldListMap().begin()); it != tag.fieldListMap().end(); - ++it) { - qDebug() << "XIPH" << toQString((*it).first) << "-" - << toQString((*it).second.toString()); - } + if (!pTrackMetadata) { + return; // nothing to do } - readTag(pTrackMetadata, tag); + readTrackMetadataFromTag(pTrackMetadata, tag); // Some applications (like puddletag up to version 1.0.5) write COMMENT // instead DESCRIPTION. If the comment field (correctly populated by TagLib @@ -385,94 +598,105 @@ void readXiphComment(TrackMetadata* pTrackMetadata, } } -void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { - if (kDebugMetadata) { - for (TagLib::MP4::ItemListMap::ConstIterator it( - tag.itemListMap().begin()); it != tag.itemListMap().end(); - ++it) { - qDebug() << "MP4" << toQString((*it).first) << "-" - << toQStringFirst((*it).second); - } +namespace { +// Workaround for missing const member function in TagLib + inline const TagLib::MP4::ItemListMap& getItemListMap(const TagLib::MP4::Tag& tag) { + return const_cast(tag).itemListMap(); + } +} + +void readTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP4::Tag& tag) { + if (!pTrackMetadata) { + return; // nothing to do } - readTag(pTrackMetadata, tag); + readTrackMetadataFromTag(pTrackMetadata, tag); // Get Album Artist - if (tag.itemListMap().contains("aART")) { + if (getItemListMap(tag).contains("aART")) { pTrackMetadata->setAlbumArtist( - toQStringFirst(tag.itemListMap()["aART"])); + toQStringFirst(getItemListMap(tag)["aART"])); } // Get Composer - if (tag.itemListMap().contains("\251wrt")) { + if (getItemListMap(tag).contains("\251wrt")) { pTrackMetadata->setComposer( - toQStringFirst(tag.itemListMap()["\251wrt"])); + toQStringFirst(getItemListMap(tag)["\251wrt"])); } // Get Grouping - if (tag.itemListMap().contains("\251grp")) { + if (getItemListMap(tag).contains("\251grp")) { pTrackMetadata->setGrouping( - toQStringFirst(tag.itemListMap()["\251grp"])); + toQStringFirst(getItemListMap(tag)["\251grp"])); } // Get date/year as string - if (tag.itemListMap().contains("\251day")) { - pTrackMetadata->setYear(toQStringFirst(tag.itemListMap()["\251day"])); + if (getItemListMap(tag).contains("\251day")) { + pTrackMetadata->setYear(toQStringFirst(getItemListMap(tag)["\251day"])); } // Get BPM - if (tag.itemListMap().contains("tmpo")) { + if (getItemListMap(tag).contains("tmpo")) { // Read the BPM as an integer value. - const TagLib::MP4::Item& item = tag.itemListMap()["tmpo"]; + const TagLib::MP4::Item& item = getItemListMap(tag)["tmpo"]; if (item.atomDataType() == TagLib::MP4::TypeInteger) { - pTrackMetadata->setBpm(tag.itemListMap()["tmpo"].toInt()); + pTrackMetadata->setBpm(getItemListMap(tag)["tmpo"].toInt()); } } - if (tag.itemListMap().contains("----:com.apple.iTunes:BPM")) { + if (getItemListMap(tag).contains("----:com.apple.iTunes:BPM")) { // This is the preferred field for storing the BPM // with fractional digits as a floating-point value. // If this field contains a valid value the integer // BPM value that might have been read before is // overwritten. parseBpm(pTrackMetadata, - toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:BPM"])); + toQStringFirst(getItemListMap(tag)["----:com.apple.iTunes:BPM"])); } // Only read track gain (not album gain) - if (tag.itemListMap().contains( + if (getItemListMap(tag).contains( "----:com.apple.iTunes:replaygain_track_gain")) { parseReplayGain(pTrackMetadata, - toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:replaygain_track_gain"])); + toQStringFirst(getItemListMap(tag)["----:com.apple.iTunes:replaygain_track_gain"])); } // Read musical key (conforms to Rapid Evolution) - if (tag.itemListMap().contains("----:com.apple.iTunes:KEY")) { + if (getItemListMap(tag).contains("----:com.apple.iTunes:KEY")) { pTrackMetadata->setKey( - toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:KEY"])); + toQStringFirst(getItemListMap(tag)["----:com.apple.iTunes:KEY"])); } // Read musical key (conforms to MixedInKey, Serato, Traktor) - if (tag.itemListMap().contains("----:com.apple.iTunes:initialkey")) { + if (getItemListMap(tag).contains("----:com.apple.iTunes:initialkey")) { // This is the preferred field for storing the musical key! pTrackMetadata->setKey( - toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:initialkey"])); + toQStringFirst(getItemListMap(tag)["----:com.apple.iTunes:initialkey"])); } } -QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag) { - QImage coverArt; +bool readCoverArtFromID3v2Tag(QImage* pCoverArt, const TagLib::ID3v2::Tag& tag) { + if (!pCoverArt) { + // implicitly successful + return true; + } + TagLib::ID3v2::FrameList covertArtFrame = tag.frameListMap()["APIC"]; if (!covertArtFrame.isEmpty()) { TagLib::ID3v2::AttachedPictureFrame* picframe = static_cast(covertArtFrame.front()); TagLib::ByteVector data = picframe->picture(); - coverArt = QImage::fromData( + *pCoverArt = QImage::fromData( reinterpret_cast(data.data()), data.size()); + return true; } - return coverArt; + return false; } -QImage readAPETagCover(const TagLib::APE::Tag& tag) { - QImage coverArt; +bool readCoverArtFromAPETag(QImage* pCoverArt, const TagLib::APE::Tag& tag) { + if (!pCoverArt) { + // implicitly successful + return true; + } + if (tag.itemListMap().contains("COVER ART (FRONT)")) { const TagLib::ByteVector nullStringTerminator(1, 0); TagLib::ByteVector item = @@ -480,15 +704,20 @@ QImage readAPETagCover(const TagLib::APE::Tag& tag) { int pos = item.find(nullStringTerminator); // skip the filename if (++pos > 0) { const TagLib::ByteVector& data = item.mid(pos); - coverArt = QImage::fromData( + *pCoverArt = QImage::fromData( reinterpret_cast(data.data()), data.size()); + return true; } } - return coverArt; + return false; } -QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag) { - QImage coverArt; +bool readCoverArtFromXiphComment(QImage* pCoverArt, const TagLib::Ogg::XiphComment& tag) { + if (!pCoverArt) { + // implicitly successful + return true; + } + if (tag.fieldListMap().contains("METADATA_BLOCK_PICTURE")) { QByteArray data( QByteArray::fromBase64( @@ -496,37 +725,48 @@ QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag) { TagLib::ByteVector tdata(data.data(), data.size()); TagLib::FLAC::Picture p(tdata); data = QByteArray(p.data().data(), p.data().size()); - coverArt = QImage::fromData(data); + *pCoverArt = QImage::fromData(data); + return true; } else if (tag.fieldListMap().contains("COVERART")) { QByteArray data( QByteArray::fromBase64( tag.fieldListMap()["COVERART"].toString().toCString())); - coverArt = QImage::fromData(data); + *pCoverArt = QImage::fromData(data); + return true; } - return coverArt; + return false; } -QImage readMP4TagCover(/*const*/TagLib::MP4::Tag& tag) { - QImage coverArt; - if (tag.itemListMap().contains("covr")) { +bool readCoverArtFromMP4Tag(QImage* pCoverArt, const TagLib::MP4::Tag& tag) { + if (!pCoverArt) { + // implicitly successful + return true; + } + + if (getItemListMap(tag).contains("covr")) { TagLib::MP4::CoverArtList coverArtList = - tag.itemListMap()["covr"].toCoverArtList(); + getItemListMap(tag)["covr"].toCoverArtList(); TagLib::ByteVector data = coverArtList.front().data(); - coverArt = QImage::fromData( + *pCoverArt = QImage::fromData( reinterpret_cast(data.data()), data.size()); + return true; } - return coverArt; + return false; } namespace { void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { + DEBUG_ASSERT(pTag); + pTag->removeFrames(pFrame->frameID()); pTag->addFrame(pFrame); } void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, const TagLib::ByteVector &id, const QString& text) { + DEBUG_ASSERT(pTag); + TagLib::String::Type textType; // For an overview of the character encodings supported by // the different ID3v2 versions please refer to the following @@ -558,9 +798,10 @@ void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, } // anonymous namespace -bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { - if (NULL == pTag) { - return false; +bool writeTrackMetadataIntoTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { + if (!pTag) { + // implicitly successful + return true; } pTag->setArtist(toTagLibString(trackMetadata.getArtist())); @@ -591,26 +832,30 @@ bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { return true; } -bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, +bool writeTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) { - if (NULL == pTag) { - return false; + if (!pTag) { + // implicitly successful + return true; } + const TagLib::ID3v2::Header* pHeader = pTag->header(); - if ((NULL == pHeader) || (3 > pHeader->majorVersion())) { + if (!pHeader || (3 > pHeader->majorVersion())) { // only ID3v2.3.x and higher (currently only ID3v2.4.x) are supported return false; } // Write common metadata - writeTag(pTag, trackMetadata); + if (!writeTrackMetadataIntoTag(pTag, trackMetadata)) { + return false; + } // additional tags writeID3v2TextIdentificationFrame(pTag, "TPE2", trackMetadata.getAlbumArtist()); // According to the specification "The 'TBPM' frame contains the number - // of beats per minute in the mainpart of the audio. The BPM is an integer - // and represented as a numerical string." + // of beats per minute in the mainpart of the audio. The BPM is an + // integer and represented as a numerical string." // Reference: http://id3.org/id3v2.3.0 writeID3v2TextIdentificationFrame(pTag, "TBPM", TrackMetadata::formatBpm(trackMetadata.getBpmAsInteger())); @@ -637,14 +882,12 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, return true; } -bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { - if (NULL == pTag) { +bool writeTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { + // Write common metadata + if (!writeTrackMetadataIntoTag(pTag, trackMetadata)) { return false; } - // Write common metadata - writeTag(pTag, trackMetadata); - pTag->addValue("Album Artist", toTagLibString(trackMetadata.getAlbumArtist()), true); pTag->addValue("Composer", @@ -661,15 +904,13 @@ bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { return true; } -bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, +bool writeTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& trackMetadata) { - if (NULL == pTag) { + // Write common metadata + if (!writeTrackMetadataIntoTag(pTag, trackMetadata)) { return false; } - // Write common metadata - writeTag(pTag, trackMetadata); - // Taglib does not support the update of Vorbis comments. // thus, we have to remove the old comment and add the new one @@ -725,14 +966,12 @@ void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, } // anonymous namespace -bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { - if (NULL == pTag) { +bool writeTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { + // Write common metadata + if (!writeTrackMetadataIntoTag(pTag, trackMetadata)) { return false; } - // Write common metadata - writeTag(pTag, trackMetadata); - writeMP4Atom(pTag, "aART", trackMetadata.getAlbumArtist()); writeMP4Atom(pTag, "\251wrt", trackMetadata.getComposer()); writeMP4Atom(pTag, "\251grp", trackMetadata.getGrouping()); diff --git a/src/metadata/trackmetadatataglib.h b/src/metadata/trackmetadatataglib.h index 138bf38b0f3..c7f106f9cde 100644 --- a/src/metadata/trackmetadatataglib.h +++ b/src/metadata/trackmetadatataglib.h @@ -11,6 +11,7 @@ #define SOUNDSOURCETAGLIB_H #include "metadata/trackmetadata.h" +#include "util/defs.h" // Result #include #include @@ -21,37 +22,43 @@ namespace Mixxx { -// Read common audio properties of a file -bool readAudioProperties(TrackMetadata* pTrackMetadata, - const TagLib::File& file); +// Read track metadata of supported file types +Result readTrackMetadataFromFile(TrackMetadata* pTrackMetadata, QString fileName); + +// Read cover art of supported file types +Result readCoverArtFromFile(QImage* pCoverArt, QString fileName); + +// Read both track metadata and cover art of supported file types +//(both parameters are optional and might be NULL) +Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImage* pCoverArt, QString fileName); // Read metadata // The general function readTag() is implicitly invoked // from the specialized tag reading functions! -void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag); -void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag); -void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag); -void readXiphComment(TrackMetadata* pTrackMetadata, +void readTrackMetadataFromTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag); +void readTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag); +void readTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag); +void readTrackMetadataFromXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag); -void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag); +void readTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP4::Tag& tag); + +// Read cover art +// In order to avoid processing images when it's not +// needed (TIO building), we must process it separately. +bool readCoverArtFromID3v2Tag(QImage* pCoverArt, const TagLib::ID3v2::Tag& tag); +bool readCoverArtFromAPETag(QImage* pCoverArt, const TagLib::APE::Tag& tag); +bool readCoverArtFromXiphComment(QImage* pCoverArt, const TagLib::Ogg::XiphComment& tag); +bool readCoverArtFromMP4Tag(QImage* pCoverArt, const TagLib::MP4::Tag& tag); // Write metadata // The general function writeTag() is implicitly invoked // from the specialized tag writing functions! -bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, +bool writeTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata); -bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata); -bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, +bool writeTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata); +bool writeTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& trackMetadata); -bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata); - -// Read cover art -// In order to avoid processing images when it's not -// needed (TIO building), we must process it separately. -QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag); -QImage readAPETagCover(const TagLib::APE::Tag& tag); -QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag); -QImage readMP4TagCover(/*const*/TagLib::MP4::Tag& tag); +bool writeTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata); } //namespace Mixxx diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index 5ae01738f78..8976cd1756c 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -1,5 +1,6 @@ #include "sources/soundsource.h" -#include "metadata/trackmetadata.h" + +#include "metadata/trackmetadatataglib.h" namespace Mixxx { @@ -19,4 +20,14 @@ SoundSource::SoundSource(QUrl url, QString type) DEBUG_ASSERT(getUrl().isValid()); } +Result SoundSource::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { + return readTrackMetadataFromFile(pMetadata, getLocalFileName()); +} + +QImage SoundSource::parseCoverArt() const { + QImage coverArt; + readCoverArtFromFile(&coverArt, getLocalFileName()); + return coverArt; +} + } //namespace Mixxx diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index b4988e01c6d..1062de9d500 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -47,10 +47,10 @@ class SoundSource: public UrlResource { // The implementation is free to set inaccurate estimated // values here that are overwritten when the AudioSource is // actually opened for reading. - virtual Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const = 0; + virtual Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const; // Returns the first cover art image embedded within the file (if any). - virtual QImage parseCoverArt() const = 0; + virtual QImage parseCoverArt() const; // Opens the SoundSource for reading audio data. virtual AudioSourcePointer open() const = 0; diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index 69b092dfc2a..809a02d87c3 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -1,10 +1,6 @@ #include "sources/soundsourcecoreaudio.h" #include "sources/audiosourcecoreaudio.h" -#include "metadata/trackmetadatataglib.h" - -#include -#include #include @@ -25,82 +21,6 @@ SoundSourceCoreAudio::SoundSourceCoreAudio(QUrl url) : SoundSource(url) { } -Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - if (getType() == "m4a") { - TagLib::MP4::File f(getLocalFileNameBytes().constData()); - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - TagLib::MP4::Tag *mp4(f.tag()); - if (mp4) { - readMP4Tag(pMetadata, *mp4); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } - } else if (getType() == "mp3") { - TagLib::MPEG::File f(getLocalFileNameBytes().constData()); - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); - if (id3v2) { - readID3v2Tag(pMetadata, *id3v2); - } else { - TagLib::APE::Tag *ape = f.APETag(); - if (ape) { - readAPETag(pMetadata, *ape); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } - } - } else if (getType() == "mp2") { - //TODO: MP2 metadata. Does anyone use mp2 files anymore? - // Feels like 1995 again... - return ERR; - } - - return OK; -} - -QImage SoundSourceCoreAudio::parseCoverArt() const { - QImage coverArt; - if (getType() == "m4a") { - TagLib::MP4::File f(getLocalFileNameBytes().constData()); - TagLib::MP4::Tag *mp4(f.tag()); - if (mp4) { - return Mixxx::readMP4TagCover(*mp4); - } else { - return QImage(); - } - } else if (getType() == "mp3") { - TagLib::MPEG::File f(getLocalFileNameBytes().constData()); - TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); - if (id3v2) { - coverArt = Mixxx::readID3v2TagCover(*id3v2); - } - if (coverArt.isNull()) { - TagLib::APE::Tag *ape = f.APETag(); - if (ape) { - coverArt = Mixxx::readAPETagCover(*ape); - } - } - return coverArt; - } - return coverArt; -} - Mixxx::AudioSourcePointer SoundSourceCoreAudio::open() const { return Mixxx::AudioSourceCoreAudio::create(getUrl()); } diff --git a/src/sources/soundsourcecoreaudio.h b/src/sources/soundsourcecoreaudio.h index dd984fa8177..786da92dcf1 100644 --- a/src/sources/soundsourcecoreaudio.h +++ b/src/sources/soundsourcecoreaudio.h @@ -9,9 +9,6 @@ class SoundSourceCoreAudio : public Mixxx::SoundSource { explicit SoundSourceCoreAudio(QUrl url); - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - QImage parseCoverArt() const /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; }; diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 2009ce354d1..642ffcd7216 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -1,12 +1,7 @@ #include "sources/soundsourceflac.h" -#include "metadata/trackmetadatataglib.h" #include "sources/audiosourceflac.h" -#include - -#include - QList SoundSourceFLAC::supportedFileExtensions() { QList list; list.push_back("flac"); @@ -17,59 +12,6 @@ SoundSourceFLAC::SoundSourceFLAC(QUrl url) : SoundSource(url, "flac") { } -Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::FLAC::File f(getLocalFileNameBytes().constData()); - - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - - TagLib::Ogg::XiphComment *xiph(f.xiphComment()); - if (xiph) { - readXiphComment(pMetadata, *xiph); - } else { - TagLib::ID3v2::Tag *id3v2(f.ID3v2Tag()); - if (id3v2) { - readID3v2Tag(pMetadata, *id3v2); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } - } - - return OK; -} - -QImage SoundSourceFLAC::parseCoverArt() const { - TagLib::FLAC::File f(getLocalFileNameBytes().constData()); - QImage coverArt; - TagLib::Ogg::XiphComment *xiph(f.xiphComment()); - if (xiph) { - coverArt = Mixxx::readXiphCommentCover(*xiph); - } - if (coverArt.isNull()) { - TagLib::ID3v2::Tag *id3v2(f.ID3v2Tag()); - if (id3v2) { - coverArt = Mixxx::readID3v2TagCover(*id3v2); - } - if (coverArt.isNull()) { - TagLib::List covers = f.pictureList(); - if (!covers.isEmpty()) { - std::list::iterator it = covers.begin(); - TagLib::FLAC::Picture* cover = *it; - coverArt = QImage::fromData( - QByteArray(cover->data().data(), cover->data().size())); - } - } - } - return coverArt; -} - Mixxx::AudioSourcePointer SoundSourceFLAC::open() const { return Mixxx::AudioSourceFLAC::create(getUrl()); } diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index 9f573358763..a84b754ffed 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -9,9 +9,6 @@ class SoundSourceFLAC: public Mixxx::SoundSource { explicit SoundSourceFLAC(QUrl url); - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - QImage parseCoverArt() const /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; }; diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 134e928c419..a8ccd92e370 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -1,9 +1,6 @@ #include "sources/soundsourcemp3.h" #include "sources/audiosourcemp3.h" -#include "metadata/trackmetadatataglib.h" - -#include QList SoundSourceMp3::supportedFileExtensions() { QList list; @@ -15,51 +12,6 @@ SoundSourceMp3::SoundSourceMp3(QUrl url) : SoundSource(url, "mp3") { } -Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - TagLib::MPEG::File f(getLocalFileNameBytes().constData()); - - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - - // Now look for MP3 specific metadata (e.g. BPM) - TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); - if (id3v2) { - readID3v2Tag(pMetadata, *id3v2); - } else { - TagLib::APE::Tag *ape = f.APETag(); - if (ape) { - readAPETag(pMetadata, *ape); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } - } - - return OK; -} - -QImage SoundSourceMp3::parseCoverArt() const { - QImage coverArt; - TagLib::MPEG::File f(getLocalFileNameBytes().constData()); - TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); - if (id3v2) { - coverArt = Mixxx::readID3v2TagCover(*id3v2); - } - if (coverArt.isNull()) { - TagLib::APE::Tag *ape = f.APETag(); - if (ape) { - coverArt = Mixxx::readAPETagCover(*ape); - } - } - return coverArt; -} - Mixxx::AudioSourcePointer SoundSourceMp3::open() const { return Mixxx::AudioSourceMp3::create(getUrl()); } diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h index 3fa243eca90..5da814395e3 100644 --- a/src/sources/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -3,19 +3,12 @@ #include "sources/soundsource.h" -/** - *@author Tue and Ken Haste Andersen - */ - class SoundSourceMp3: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); explicit SoundSourceMp3(QUrl url); - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - QImage parseCoverArt() const /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; }; diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index ba15ed96598..b62d1edd69c 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -1,9 +1,6 @@ #include "sources/soundsourceoggvorbis.h" #include "sources/audiosourceoggvorbis.h" -#include "metadata/trackmetadatataglib.h" - -#include QList SoundSourceOggVorbis::supportedFileExtensions() { QList list; @@ -15,43 +12,6 @@ SoundSourceOggVorbis::SoundSourceOggVorbis(QUrl url) : SoundSource(url, "ogg") { } -/* - Parse the the file to get metadata - */ -Result SoundSourceOggVorbis::parseMetadata( - Mixxx::TrackMetadata* pMetadata) const { - TagLib::Ogg::Vorbis::File f(getLocalFileNameBytes().constData()); - - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - - TagLib::Ogg::XiphComment *xiph = f.tag(); - if (xiph) { - readXiphComment(pMetadata, *xiph); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } - - return OK; -} - -QImage SoundSourceOggVorbis::parseCoverArt() const { - TagLib::Ogg::Vorbis::File f(getLocalFileNameBytes().constData()); - TagLib::Ogg::XiphComment *xiph = f.tag(); - if (xiph) { - return Mixxx::readXiphCommentCover(*xiph); - } else { - return QImage(); - } -} - Mixxx::AudioSourcePointer SoundSourceOggVorbis::open() const { return Mixxx::AudioSourceOggVorbis::create(getUrl()); } diff --git a/src/sources/soundsourceoggvorbis.h b/src/sources/soundsourceoggvorbis.h index c50b3c503ac..58e56dfd201 100644 --- a/src/sources/soundsourceoggvorbis.h +++ b/src/sources/soundsourceoggvorbis.h @@ -9,9 +9,6 @@ class SoundSourceOggVorbis: public Mixxx::SoundSource { explicit SoundSourceOggVorbis(QUrl url); - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - QImage parseCoverArt() const /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; }; diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 03b3b4beccc..e876f023f32 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -3,14 +3,6 @@ #include "sources/audiosourceopus.h" #include "metadata/trackmetadatataglib.h" -// TagLib has support for the Ogg Opus file format since version 1.9 -#define TAGLIB_HAS_OPUSFILE \ - ((TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9))) - -#if TAGLIB_HAS_OPUSFILE -#include -#endif - QList SoundSourceOpus::supportedFileExtensions() { QList list; list.push_back("opus"); @@ -43,29 +35,10 @@ class OggOpusFileOwner { Parse the file to get metadata */ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { - const QByteArray qbaFilename(getLocalFileNameBytes()); - -// If we don't have new enough Taglib we use libopusfile parser! -#if TAGLIB_HAS_OPUSFILE - TagLib::Ogg::Opus::File f(qbaFilename.constData()); - - if (!readAudioProperties(pMetadata, f)) { - return ERR; + if (OK == readTrackMetadataFromFile(pMetadata, getLocalFileName())) { + return OK; } - TagLib::Ogg::XiphComment *xiph = f.tag(); - if (xiph) { - readXiphComment(pMetadata, *xiph); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } -#else // Beginning with version 1.9.0 TagLib supports the Opus format. // Until this becomes the minimum version required by Mixxx tags // in .opus files must also be parsed using opusfile. The following @@ -77,7 +50,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { int error = 0; OggOpusFileOwner l_ptrOpusFile( - op_open_file(qbaFilename.constData(), &error)); + op_open_file(getLocalFileNameBytes().constData(), &error)); int i = 0; const OpusTags *l_ptrOpusTags = op_tags(l_ptrOpusFile, -1); @@ -125,22 +98,10 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { //qDebug() << "Comment" << i << l_ptrOpusTags->comment_lengths[i] << //" (" << l_ptrOpusTags->user_comments[i] << ")" << l_STag << "*" << l_SPayload; } -#endif return OK; } -QImage SoundSourceOpus::parseCoverArt() const { -#if TAGLIB_HAS_OPUSFILE - TagLib::Ogg::Opus::File f(getLocalFileNameBytes().constData()); - TagLib::Ogg::XiphComment *xiph = f.tag(); - if (xiph) { - return Mixxx::readXiphCommentCover(*xiph); - } -#endif - return QImage(); -} - Mixxx::AudioSourcePointer SoundSourceOpus::open() const { return Mixxx::AudioSourceOpus::create(getUrl()); } diff --git a/src/sources/soundsourceopus.h b/src/sources/soundsourceopus.h index 5be30ea8f33..da090201de1 100644 --- a/src/sources/soundsourceopus.h +++ b/src/sources/soundsourceopus.h @@ -10,7 +10,6 @@ class SoundSourceOpus: public Mixxx::SoundSource { explicit SoundSourceOpus(QUrl url); Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - QImage parseCoverArt() const /*override*/; Mixxx::AudioSourcePointer open() const /*override*/; }; diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index f0a114fba30..a08bd3acc88 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -1,12 +1,6 @@ #include "sources/soundsourcesndfile.h" #include "sources/audiosourcesndfile.h" -#include "metadata/trackmetadatataglib.h" - -#include -#include -#include -#include QList SoundSourceSndFile::supportedFileExtensions() { QList list; @@ -21,107 +15,6 @@ SoundSourceSndFile::SoundSourceSndFile(QUrl url) : SoundSource(url) { } -Result SoundSourceSndFile::parseMetadata( - Mixxx::TrackMetadata* pMetadata) const { - if (getType() == "flac") { - TagLib::FLAC::File f(getLocalFileNameBytes().constData()); - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - TagLib::Ogg::XiphComment* xiph = f.xiphComment(); - if (xiph) { - readXiphComment(pMetadata, *xiph); - } else { - TagLib::ID3v2::Tag *id3v2(f.ID3v2Tag()); - if (id3v2) { - readID3v2Tag(pMetadata, *id3v2); - } else { - // fallback - const TagLib::Tag *tag(f.tag()); - if (tag) { - readTag(pMetadata, *tag); - } else { - return ERR; - } - } - } - } else if (getType() == "wav") { - TagLib::RIFF::WAV::File f(getLocalFileNameBytes().constData()); - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - // Taglib provides the ID3v2Tag method for WAV files since Version 1.9 -#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) - TagLib::ID3v2::Tag* id3v2(f.ID3v2Tag()); -#else - TagLib::ID3v2::Tag* id3v2(f.tag()); -#endif - if (id3v2) { - readID3v2Tag(pMetadata, *id3v2); - } else { - return ERR; - } - } else if (getType().startsWith("aif")) { - // Try AIFF - TagLib::RIFF::AIFF::File f(getLocalFileNameBytes().constData()); - if (!readAudioProperties(pMetadata, f)) { - return ERR; - } - TagLib::ID3v2::Tag *id3v2(f.tag()); - if (id3v2) { - readID3v2Tag(pMetadata, *id3v2); - } else { - return ERR; - } - } else { - return ERR; - } - - return OK; -} - -QImage SoundSourceSndFile::parseCoverArt() const { - QImage coverArt; - - if (getType() == "flac") { - TagLib::FLAC::File f(getLocalFileNameBytes().constData()); - TagLib::ID3v2::Tag* id3v2 = f.ID3v2Tag(); - if (id3v2) { - coverArt = Mixxx::readID3v2TagCover(*id3v2); - } - if (coverArt.isNull()) { - TagLib::Ogg::XiphComment *xiph = f.xiphComment(); - if (xiph) { - coverArt = Mixxx::readXiphCommentCover(*xiph); - } - } - if (coverArt.isNull()) { - TagLib::List covers = f.pictureList(); - if (!covers.isEmpty()) { - std::list::iterator it = covers.begin(); - TagLib::FLAC::Picture* cover = *it; - coverArt = QImage::fromData( - QByteArray(cover->data().data(), cover->data().size())); - } - } - } else if (getType() == "wav") { - TagLib::RIFF::WAV::File f(getLocalFileNameBytes().constData()); - TagLib::ID3v2::Tag* id3v2 = f.tag(); - if (id3v2) { - coverArt = Mixxx::readID3v2TagCover(*id3v2); - } - } else if (getType().startsWith("aif")) { - // Try AIFF - TagLib::RIFF::AIFF::File f(getLocalFileNameBytes().constData()); - TagLib::ID3v2::Tag* id3v2 = f.tag(); - if (id3v2) { - coverArt = Mixxx::readID3v2TagCover(*id3v2); - } - } - - return coverArt; -} - Mixxx::AudioSourcePointer SoundSourceSndFile::open() const { return Mixxx::AudioSourceSndFile::create(getUrl()); } diff --git a/src/sources/soundsourcesndfile.h b/src/sources/soundsourcesndfile.h index c05ebc4075c..3e32a4f2083 100644 --- a/src/sources/soundsourcesndfile.h +++ b/src/sources/soundsourcesndfile.h @@ -9,9 +9,6 @@ class SoundSourceSndFile: public Mixxx::SoundSource { explicit SoundSourceSndFile(QUrl url); - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - QImage parseCoverArt() const /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; }; diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index 16b09cd4847..7b2335c2758 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -131,10 +131,10 @@ TEST_F(MetadataTest, ID3v2Year) { { Mixxx::TrackMetadata trackMetadata; trackMetadata.setYear(year); - writeID3v2Tag(&tag, trackMetadata); + writeTrackMetadataIntoID3v2Tag(&tag, trackMetadata); } Mixxx::TrackMetadata trackMetadata; - readID3v2Tag(&trackMetadata, tag); + readTrackMetadataFromID3v2Tag(&trackMetadata, tag); if (4 > majorVersion) { // ID3v2.3.0: parsed + formatted const QString expectedYear(Mixxx::TrackMetadata::normalizeYear(year)); From f8a6f524d06b3c2b944c2530657a34c39af21a1a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 7 Feb 2015 00:46:50 +0100 Subject: [PATCH 171/481] Cleanup functions for reading tags --- src/metadata/trackmetadatataglib.cpp | 903 +++++++++++++-------------- src/metadata/trackmetadatataglib.h | 17 +- 2 files changed, 432 insertions(+), 488 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index e5bca15c378..1fc8795eb2f 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -6,6 +6,10 @@ #define TAGLIB_HAS_OPUSFILE \ ((TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9))) +// TagLib has support for hasID3v2Tag()/ID3v2Tag() for WAV files since version 1.9 +#define TAGLIB_HAS_WAV_ID3V2TAG \ + (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) + #include #include #include @@ -21,11 +25,6 @@ #include #include -#include -#include -#include -#include -#include #include #include #include @@ -34,12 +33,12 @@ namespace Mixxx { namespace { +const QString kFileTypeAIFF("aiff"); +const QString kFileTypeFLAC("flac"); const QString kFileTypeMP3("mp3"); const QString kFileTypeMP4("mp4"); -const QString kFileTypeFLAC("flac"); const QString kFileTypeOggVorbis("ogg"); const QString kFileTypeOggOpus("opus"); -const QString kFileTypeAIFF("aiff"); const QString kFileTypeWAV("wav"); const QString kFileTypeWavPack("wv"); @@ -170,206 +169,6 @@ bool readAudioProperties(TrackMetadata* pTrackMetadata, return true; } -} // anonymous namespace - -Result readTrackMetadataFromFile(TrackMetadata* pTrackMetadata, QString fileName) { - return readTrackMetadataAndCoverArtFromFile(pTrackMetadata, NULL, fileName); -} - -Result readCoverArtFromFile(QImage* pCoverArt, QString fileName) { - return readTrackMetadataAndCoverArtFromFile(NULL, pCoverArt, fileName); -} - -Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImage* pCoverArt, QString fileName) { - const QString fileType(getFileTypeFromFileName(fileName)); - - qDebug() << "Reading tags of file" << fileName << "with type" << fileType; - - // Rationale: If a file contains different types of tags only - // a single type of tag will be read. Tag types are read in a - // fixed order. Both track metadata and cover art will be read - // from the same tag types. Only the first available tag type - // is read and data in subsequent tags is ignored. - - if (kFileTypeMP3 == fileType) { - TagLib::MPEG::File file(fileName.toLocal8Bit().constData()); - if (readAudioProperties(pTrackMetadata, file)) { - const TagLib::ID3v2::Tag* pID3v2Tag = file.ID3v2Tag(); - if (pID3v2Tag) { - readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); - readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); - return OK; - } else { - const TagLib::APE::Tag* pAPETag = file.APETag(); - if (pAPETag) { - readTrackMetadataFromAPETag(pTrackMetadata, *pAPETag); - readCoverArtFromAPETag(pCoverArt, *pAPETag); - return OK; - } else { - // fallback - const TagLib::Tag* pTag(file.tag()); - if (pTag) { - readTrackMetadataFromTag(pTrackMetadata, *pTag); - return OK; - } - } - } - } - } else if (kFileTypeMP4 == fileType) { - TagLib::MP4::File file(fileName.toLocal8Bit().constData()); - if (readAudioProperties(pTrackMetadata, file)) { - const TagLib::MP4::Tag* pMP4Tag = file.tag(); - if (pMP4Tag) { - readTrackMetadataFromMP4Tag(pTrackMetadata, *pMP4Tag); - readCoverArtFromMP4Tag(pCoverArt, *pMP4Tag); - return OK; - } else { - // fallback - const TagLib::Tag* pTag(file.tag()); - if (pTag) { - readTrackMetadataFromTag(pTrackMetadata, *pTag); - return OK; - } - } - } - } else if (kFileTypeFLAC == fileType) { - TagLib::FLAC::File file(fileName.toLocal8Bit().constData()); - if (pCoverArt) { - // FLAC files may contain cover art that is not part - // of any tag. Use the first picture from this list - // as the default picture for the cover art. - TagLib::List covers = file.pictureList(); - if (!covers.isEmpty()) { - std::list::iterator it = covers.begin(); - TagLib::FLAC::Picture* cover = *it; - *pCoverArt = QImage::fromData( - QByteArray(cover->data().data(), cover->data().size())); - } - } - if (readAudioProperties(pTrackMetadata, file)) { - const TagLib::Ogg::XiphComment* pXiphComment = - file.hasXiphComment() ? file.xiphComment() : NULL; - if (pXiphComment) { - readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); - readCoverArtFromXiphComment(pCoverArt, *pXiphComment); - return OK; - } else { - const TagLib::ID3v2::Tag* pID3v2Tag = - file.hasID3v2Tag() ? file.ID3v2Tag() : NULL; - if (pID3v2Tag) { - readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); - readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); - return OK; - } else { - // fallback - const TagLib::Tag* pTag(file.tag()); - if (pTag) { - readTrackMetadataFromTag(pTrackMetadata, *pTag); - return OK; - } - } - } - } - } else if (kFileTypeOggVorbis == fileType) { - TagLib::Ogg::Vorbis::File file(fileName.toLocal8Bit().constData()); - if (readAudioProperties(pTrackMetadata, file)) { - const TagLib::Ogg::XiphComment* pXiphComment = file.tag(); - if (pXiphComment) { - readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); - readCoverArtFromXiphComment(pCoverArt, *pXiphComment); - return OK; - } else { - // fallback - const TagLib::Tag* pTag(file.tag()); - if (pTag) { - readTrackMetadataFromTag(pTrackMetadata, *pTag); - return OK; - } - } - } -#if TAGLIB_HAS_OPUSFILE - } else if (kFileTypeOggOpus == fileType) { - TagLib::Ogg::Opus::File file(fileName.toLocal8Bit().constData()); - if (readAudioProperties(pTrackMetadata, file)) { - const TagLib::Ogg::XiphComment* pXiphComment = file.tag(); - if (pXiphComment) { - readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); - readCoverArtFromXiphComment(pCoverArt, *pXiphComment); - return OK; - } else { - // fallback - const TagLib::Tag* pTag(file.tag()); - if (pTag) { - readTrackMetadataFromTag(pTrackMetadata, *pTag); - return OK; - } - } - } -#endif // TAGLIB_HAS_OPUSFILE - } else if (kFileTypeWavPack == fileType) { - TagLib::WavPack::File file(fileName.toLocal8Bit().constData()); - if (readAudioProperties(pTrackMetadata, file)) { - const TagLib::APE::Tag* pAPETag = file.APETag(); - if (pAPETag) { - readTrackMetadataFromAPETag(pTrackMetadata, *pAPETag); - readCoverArtFromAPETag(pCoverArt, *pAPETag); - return OK; - } else { - // fallback - const TagLib::Tag* pTag(file.tag()); - if (pTag) { - readTrackMetadataFromTag(pTrackMetadata, *pTag); - return OK; - } - } - } - } else if (kFileTypeWAV == fileType) { - TagLib::RIFF::WAV::File file(fileName.toLocal8Bit().constData()); - if (readAudioProperties(pTrackMetadata, file)) { - // Taglib provides the ID3v2Tag method for WAV files since Version 1.9 -#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) - // NOTE(uklotzde): ID3v2Tag() always returns a valid pointer for WAV files - // whether or not the file actually has an ID3v2 tag! - const TagLib::ID3v2::Tag* pID3v2Tag = file.hasID3v2Tag() ? file.ID3v2Tag() : NULL; -#else - const TagLib::ID3v2::Tag* pID3v2Tag = file.tag(); -#endif - if (pID3v2Tag) { - readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); - readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); - return OK; - } else { - // fallback - const TagLib::Tag* pTag(file.tag()); - if (pTag) { - readTrackMetadataFromTag(pTrackMetadata, *pTag); - return OK; - } - } - } - } else if (kFileTypeAIFF == fileType) { - TagLib::RIFF::AIFF::File file(fileName.toLocal8Bit().constData()); - if (readAudioProperties(pTrackMetadata, file)) { - const TagLib::ID3v2::Tag* pID3v2Tag = file.tag(); - if (pID3v2Tag) { - readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); - readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); - return OK; - } else { - // fallback - const TagLib::Tag* pTag(file.tag()); - if (pTag) { - readTrackMetadataFromTag(pTrackMetadata, *pTag); - return OK; - } - } - } - } - - qWarning() << "File" << fileName << "does not contain any supported tags that could be read!"; - return ERR; -} - void readTrackMetadataFromTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { if (!pTrackMetadata) { return; // nothing to do @@ -392,46 +191,205 @@ void readTrackMetadataFromTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& } } -void readTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, - const TagLib::ID3v2::Tag& tag) { - if (!pTrackMetadata) { +// Workaround for missing const member function in TagLib +inline const TagLib::MP4::ItemListMap& getItemListMap(const TagLib::MP4::Tag& tag) { + return const_cast(tag).itemListMap(); +} + +void readCoverArtFromID3v2Tag(QImage* pCoverArt, const TagLib::ID3v2::Tag& tag) { + if (!pCoverArt) { return; // nothing to do } - readTrackMetadataFromTag(pTrackMetadata, tag); - - const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()["TPE2"]); - if (!albumArtistFrame.isEmpty()) { - pTrackMetadata->setAlbumArtist(toQStringFirst(albumArtistFrame)); + TagLib::ID3v2::FrameList covertArtFrame = tag.frameListMap()["APIC"]; + if (!covertArtFrame.isEmpty()) { + TagLib::ID3v2::AttachedPictureFrame* picframe = + static_cast(covertArtFrame.front()); + TagLib::ByteVector data = picframe->picture(); + *pCoverArt = QImage::fromData( + reinterpret_cast(data.data()), data.size()); } +} - if (pTrackMetadata->getAlbum().isEmpty()) { - const TagLib::ID3v2::FrameList originalAlbumFrame( - tag.frameListMap()["TOAL"]); - pTrackMetadata->setAlbum(toQStringFirst(originalAlbumFrame)); +void readCoverArtFromAPETag(QImage* pCoverArt, const TagLib::APE::Tag& tag) { + if (!pCoverArt) { + return; // nothing to do } - const TagLib::ID3v2::FrameList composerFrame(tag.frameListMap()["TCOM"]); - if (!composerFrame.isEmpty()) { - pTrackMetadata->setComposer(toQStringFirst(composerFrame)); + if (tag.itemListMap().contains("COVER ART (FRONT)")) { + const TagLib::ByteVector nullStringTerminator(1, 0); + TagLib::ByteVector item = + tag.itemListMap()["COVER ART (FRONT)"].value(); + int pos = item.find(nullStringTerminator); // skip the filename + if (++pos > 0) { + const TagLib::ByteVector& data = item.mid(pos); + *pCoverArt = QImage::fromData( + reinterpret_cast(data.data()), data.size()); + } } +} - const TagLib::ID3v2::FrameList groupingFrame(tag.frameListMap()["TIT1"]); - if (!groupingFrame.isEmpty()) { - pTrackMetadata->setGrouping(toQStringFirst(groupingFrame)); +void readCoverArtFromXiphComment(QImage* pCoverArt, const TagLib::Ogg::XiphComment& tag) { + if (!pCoverArt) { + return; // nothing to do } - // ID3v2.4.0: TDRC replaces TYER + TDAT - const QString recordingTime( - toQStringFirst(tag.frameListMap()["TDRC"])); - if (!recordingTime.isEmpty()) { - pTrackMetadata->setYear(recordingTime); - } else { - // Fallback to TYER + TDAT - const QString recordingYear( - toQStringFirst(tag.frameListMap()["TYER"]).trimmed()); - QString year(recordingYear); - if (4 == recordingYear.length()) { + if (tag.fieldListMap().contains("METADATA_BLOCK_PICTURE")) { + QByteArray data( + QByteArray::fromBase64( + tag.fieldListMap()["METADATA_BLOCK_PICTURE"].front().toCString())); + TagLib::ByteVector tdata(data.data(), data.size()); + TagLib::FLAC::Picture p(tdata); + data = QByteArray(p.data().data(), p.data().size()); + *pCoverArt = QImage::fromData(data); + } else if (tag.fieldListMap().contains("COVERART")) { + QByteArray data( + QByteArray::fromBase64( + tag.fieldListMap()["COVERART"].toString().toCString())); + *pCoverArt = QImage::fromData(data); + } +} + +void readCoverArtFromMP4Tag(QImage* pCoverArt, const TagLib::MP4::Tag& tag) { + if (!pCoverArt) { + return; // nothing to do + } + + if (getItemListMap(tag).contains("covr")) { + TagLib::MP4::CoverArtList coverArtList = + getItemListMap(tag)["covr"].toCoverArtList(); + TagLib::ByteVector data = coverArtList.front().data(); + *pCoverArt = QImage::fromData( + reinterpret_cast(data.data()), data.size()); + } +} + +void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { + DEBUG_ASSERT(pTag); + + pTag->removeFrames(pFrame->frameID()); + pTag->addFrame(pFrame); +} + +void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, + const TagLib::ByteVector &id, const QString& text) { + DEBUG_ASSERT(pTag); + + TagLib::String::Type textType; + // For an overview of the character encodings supported by + // the different ID3v2 versions please refer to the following + // resources: + // http://en.wikipedia.org/wiki/ID3#ID3v2 + // http://id3.org/id3v2.3.0 + // http://id3.org/id3v2.4.0-structure + if (4 <= pTag->header()->majorVersion()) { + // For ID3v2.4.0 or higher prefer UTF-8, because it is a + // very compact representation for common cases and it is + // independent of the byte order. + textType = TagLib::String::UTF8; + } else { + // For ID3v2.3.0/ID3v2.2.0 use UCS-2 (UTF-16 encoded Unicode + // with BOM), because UTF-8 and UTF-16BE are only supported + // since ID3v2.4.0 and the alternative ISO-8859-1 does not + // cover all Unicode characters. + textType = TagLib::String::UTF16; + } + QScopedPointer pNewFrame( + new TagLib::ID3v2::TextIdentificationFrame(id, textType)); + pNewFrame->setText(toTagLibString(text)); + replaceID3v2Frame(pTag, pNewFrame.data()); + // Now the plain pointer in pNewFrame is owned and + // managed by pTag. We need to release the ownership + // to avoid double deletion! + pNewFrame.take(); +} + +void writeTrackMetadataIntoTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { + DEBUG_ASSERT(pTag); + + pTag->setArtist(toTagLibString(trackMetadata.getArtist())); + pTag->setTitle(toTagLibString(trackMetadata.getTitle())); + pTag->setAlbum(toTagLibString(trackMetadata.getAlbum())); + pTag->setGenre(toTagLibString(trackMetadata.getGenre())); + pTag->setComment(toTagLibString(trackMetadata.getComment())); + + // Only write the year into the common tag if the corresponding + // field contains a 4-character numeric string. + const QString yearString(trackMetadata.getYear().trimmed()); + if (4 == yearString.length()) { + bool yearValid = false; + uint year = trackMetadata.getYear().toUInt(&yearValid); + if (yearValid && (year > 0)) { + pTag->setYear(year); + } + } + // Derived tags might be able to write the complete string + // from trackMetadata.getYear() into the corresponding field. + + bool trackNumberValid = false; + uint track = trackMetadata.getTrackNumber().toUInt(&trackNumberValid); + if (trackNumberValid && (track > 0)) { + pTag->setTrack(track); + } +} + +template +inline void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, + const T& value) { + pTag->itemListMap()[key] = value; +} + +void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, + const QString& value) { + if (value.isEmpty()) { + pTag->itemListMap().erase(key); + } else { + writeMP4Atom(pTag, key, TagLib::StringList(toTagLibString(value))); + } +} + +} // anonymous namespace + +void readTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, + const TagLib::ID3v2::Tag& tag) { + if (!pTrackMetadata) { + return; // nothing to do + } + + readTrackMetadataFromTag(pTrackMetadata, tag); + + const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()["TPE2"]); + if (!albumArtistFrame.isEmpty()) { + pTrackMetadata->setAlbumArtist(toQStringFirst(albumArtistFrame)); + } + + if (pTrackMetadata->getAlbum().isEmpty()) { + const TagLib::ID3v2::FrameList originalAlbumFrame( + tag.frameListMap()["TOAL"]); + pTrackMetadata->setAlbum(toQStringFirst(originalAlbumFrame)); + } + + const TagLib::ID3v2::FrameList composerFrame(tag.frameListMap()["TCOM"]); + if (!composerFrame.isEmpty()) { + pTrackMetadata->setComposer(toQStringFirst(composerFrame)); + } + + const TagLib::ID3v2::FrameList groupingFrame(tag.frameListMap()["TIT1"]); + if (!groupingFrame.isEmpty()) { + pTrackMetadata->setGrouping(toQStringFirst(groupingFrame)); + } + + // ID3v2.4.0: TDRC replaces TYER + TDAT + const QString recordingTime( + toQStringFirst(tag.frameListMap()["TDRC"])); + if (!recordingTime.isEmpty()) { + pTrackMetadata->setYear(recordingTime); + } else { + // Fallback to TYER + TDAT + const QString recordingYear( + toQStringFirst(tag.frameListMap()["TYER"]).trimmed()); + QString year(recordingYear); + if (4 == recordingYear.length()) { const QString recordingDate( toQStringFirst(tag.frameListMap()["TDAT"]).trimmed()); if (4 == recordingDate.length()) { @@ -598,13 +556,6 @@ void readTrackMetadataFromXiphComment(TrackMetadata* pTrackMetadata, } } -namespace { -// Workaround for missing const member function in TagLib - inline const TagLib::MP4::ItemListMap& getItemListMap(const TagLib::MP4::Tag& tag) { - return const_cast(tag).itemListMap(); - } -} - void readTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP4::Tag& tag) { if (!pTrackMetadata) { return; // nothing to do @@ -673,220 +624,54 @@ void readTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP } } -bool readCoverArtFromID3v2Tag(QImage* pCoverArt, const TagLib::ID3v2::Tag& tag) { - if (!pCoverArt) { - // implicitly successful - return true; - } +bool writeTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, + const TrackMetadata& trackMetadata) { + DEBUG_ASSERT(pTag); - TagLib::ID3v2::FrameList covertArtFrame = tag.frameListMap()["APIC"]; - if (!covertArtFrame.isEmpty()) { - TagLib::ID3v2::AttachedPictureFrame* picframe = - static_cast(covertArtFrame.front()); - TagLib::ByteVector data = picframe->picture(); - *pCoverArt = QImage::fromData( - reinterpret_cast(data.data()), data.size()); - return true; + const TagLib::ID3v2::Header* pHeader = pTag->header(); + if (!pHeader || (3 > pHeader->majorVersion())) { + // only ID3v2.3.x and higher (currently only ID3v2.4.x) are supported + return false; } - return false; -} -bool readCoverArtFromAPETag(QImage* pCoverArt, const TagLib::APE::Tag& tag) { - if (!pCoverArt) { - // implicitly successful - return true; - } + writeTrackMetadataIntoTag(pTag, trackMetadata); - if (tag.itemListMap().contains("COVER ART (FRONT)")) { - const TagLib::ByteVector nullStringTerminator(1, 0); - TagLib::ByteVector item = - tag.itemListMap()["COVER ART (FRONT)"].value(); - int pos = item.find(nullStringTerminator); // skip the filename - if (++pos > 0) { - const TagLib::ByteVector& data = item.mid(pos); - *pCoverArt = QImage::fromData( - reinterpret_cast(data.data()), data.size()); - return true; + // additional tags + writeID3v2TextIdentificationFrame(pTag, "TPE2", + trackMetadata.getAlbumArtist()); + // According to the specification "The 'TBPM' frame contains the number + // of beats per minute in the mainpart of the audio. The BPM is an + // integer and represented as a numerical string." + // Reference: http://id3.org/id3v2.3.0 + writeID3v2TextIdentificationFrame(pTag, "TBPM", + TrackMetadata::formatBpm(trackMetadata.getBpmAsInteger())); + writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); + writeID3v2TextIdentificationFrame(pTag, "TCOM", + trackMetadata.getComposer()); + writeID3v2TextIdentificationFrame(pTag, "TIT1", + trackMetadata.getGrouping()); + if (4 <= pHeader->majorVersion()) { + // ID3v2.4.0: TDRC replaces TYER + TDAT + writeID3v2TextIdentificationFrame(pTag, "TDRC", + trackMetadata.getYear()); + } else { + // Fallback to TYER + TDAT + const QDate date(TrackMetadata::parseDate(trackMetadata.getYear())); + if (date.isValid()) { + writeID3v2TextIdentificationFrame(pTag, "TYER", date.toString(ID3V2_TYER_FORMAT)); + writeID3v2TextIdentificationFrame(pTag, "TDAT", date.toString(ID3V2_TDAT_FORMAT)); } } - return false; -} - -bool readCoverArtFromXiphComment(QImage* pCoverArt, const TagLib::Ogg::XiphComment& tag) { - if (!pCoverArt) { - // implicitly successful - return true; - } - - if (tag.fieldListMap().contains("METADATA_BLOCK_PICTURE")) { - QByteArray data( - QByteArray::fromBase64( - tag.fieldListMap()["METADATA_BLOCK_PICTURE"].front().toCString())); - TagLib::ByteVector tdata(data.data(), data.size()); - TagLib::FLAC::Picture p(tdata); - data = QByteArray(p.data().data(), p.data().size()); - *pCoverArt = QImage::fromData(data); - return true; - } else if (tag.fieldListMap().contains("COVERART")) { - QByteArray data( - QByteArray::fromBase64( - tag.fieldListMap()["COVERART"].toString().toCString())); - *pCoverArt = QImage::fromData(data); - return true; - } - return false; -} -bool readCoverArtFromMP4Tag(QImage* pCoverArt, const TagLib::MP4::Tag& tag) { - if (!pCoverArt) { - // implicitly successful - return true; - } + // TODO(uklotzde): Write TXXX - REPLAYGAIN_TRACK_GAIN - if (getItemListMap(tag).contains("covr")) { - TagLib::MP4::CoverArtList coverArtList = - getItemListMap(tag)["covr"].toCoverArtList(); - TagLib::ByteVector data = coverArtList.front().data(); - *pCoverArt = QImage::fromData( - reinterpret_cast(data.data()), data.size()); - return true; - } - return false; + return true; } -namespace { - -void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { +bool writeTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { DEBUG_ASSERT(pTag); - pTag->removeFrames(pFrame->frameID()); - pTag->addFrame(pFrame); -} - -void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, - const TagLib::ByteVector &id, const QString& text) { - DEBUG_ASSERT(pTag); - - TagLib::String::Type textType; - // For an overview of the character encodings supported by - // the different ID3v2 versions please refer to the following - // resources: - // http://en.wikipedia.org/wiki/ID3#ID3v2 - // http://id3.org/id3v2.3.0 - // http://id3.org/id3v2.4.0-structure - if (4 <= pTag->header()->majorVersion()) { - // For ID3v2.4.0 or higher prefer UTF-8, because it is a - // very compact representation for common cases and it is - // independent of the byte order. - textType = TagLib::String::UTF8; - } else { - // For ID3v2.3.0/ID3v2.2.0 use UCS-2 (UTF-16 encoded Unicode - // with BOM), because UTF-8 and UTF-16BE are only supported - // since ID3v2.4.0 and the alternative ISO-8859-1 does not - // cover all Unicode characters. - textType = TagLib::String::UTF16; - } - QScopedPointer pNewFrame( - new TagLib::ID3v2::TextIdentificationFrame(id, textType)); - pNewFrame->setText(toTagLibString(text)); - replaceID3v2Frame(pTag, pNewFrame.data()); - // Now the plain pointer in pNewFrame is owned and - // managed by pTag. We need to release the ownership - // to avoid double deletion! - pNewFrame.take(); -} - -} // anonymous namespace - -bool writeTrackMetadataIntoTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { - if (!pTag) { - // implicitly successful - return true; - } - - pTag->setArtist(toTagLibString(trackMetadata.getArtist())); - pTag->setTitle(toTagLibString(trackMetadata.getTitle())); - pTag->setAlbum(toTagLibString(trackMetadata.getAlbum())); - pTag->setGenre(toTagLibString(trackMetadata.getGenre())); - pTag->setComment(toTagLibString(trackMetadata.getComment())); - - // Only write the year into the common tag if the corresponding - // field contains a 4-character numeric string. - const QString yearString(trackMetadata.getYear().trimmed()); - if (4 == yearString.length()) { - bool yearValid = false; - uint year = trackMetadata.getYear().toUInt(&yearValid); - if (yearValid && (year > 0)) { - pTag->setYear(year); - } - } - // Derived tags might be able to write the complete string - // from trackMetadata.getYear() into the corresponding field. - - bool trackNumberValid = false; - uint track = trackMetadata.getTrackNumber().toUInt(&trackNumberValid); - if (trackNumberValid && (track > 0)) { - pTag->setTrack(track); - } - - return true; -} - -bool writeTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, - const TrackMetadata& trackMetadata) { - if (!pTag) { - // implicitly successful - return true; - } - - const TagLib::ID3v2::Header* pHeader = pTag->header(); - if (!pHeader || (3 > pHeader->majorVersion())) { - // only ID3v2.3.x and higher (currently only ID3v2.4.x) are supported - return false; - } - - // Write common metadata - if (!writeTrackMetadataIntoTag(pTag, trackMetadata)) { - return false; - } - - // additional tags - writeID3v2TextIdentificationFrame(pTag, "TPE2", - trackMetadata.getAlbumArtist()); - // According to the specification "The 'TBPM' frame contains the number - // of beats per minute in the mainpart of the audio. The BPM is an - // integer and represented as a numerical string." - // Reference: http://id3.org/id3v2.3.0 - writeID3v2TextIdentificationFrame(pTag, "TBPM", - TrackMetadata::formatBpm(trackMetadata.getBpmAsInteger())); - writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); - writeID3v2TextIdentificationFrame(pTag, "TCOM", - trackMetadata.getComposer()); - writeID3v2TextIdentificationFrame(pTag, "TIT1", - trackMetadata.getGrouping()); - if (4 <= pHeader->majorVersion()) { - // ID3v2.4.0: TDRC replaces TYER + TDAT - writeID3v2TextIdentificationFrame(pTag, "TDRC", - trackMetadata.getYear()); - } else { - // Fallback to TYER + TDAT - const QDate date(TrackMetadata::parseDate(trackMetadata.getYear())); - if (date.isValid()) { - writeID3v2TextIdentificationFrame(pTag, "TYER", date.toString(ID3V2_TYER_FORMAT)); - writeID3v2TextIdentificationFrame(pTag, "TDAT", date.toString(ID3V2_TDAT_FORMAT)); - } - } - - // TODO(uklotzde): Write TXXX - REPLAYGAIN_TRACK_GAIN - - return true; -} - -bool writeTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { - // Write common metadata - if (!writeTrackMetadataIntoTag(pTag, trackMetadata)) { - return false; - } + writeTrackMetadataIntoTag(pTag, trackMetadata); pTag->addValue("Album Artist", toTagLibString(trackMetadata.getAlbumArtist()), true); @@ -906,10 +691,9 @@ bool writeTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& t bool writeTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& trackMetadata) { - // Write common metadata - if (!writeTrackMetadataIntoTag(pTag, trackMetadata)) { - return false; - } + DEBUG_ASSERT(pTag); + + writeTrackMetadataIntoTag(pTag, trackMetadata); // Taglib does not support the update of Vorbis comments. // thus, we have to remove the old comment and add the new one @@ -947,30 +731,10 @@ bool writeTrackMetadataIntoXiphComment(TagLib::Ogg::XiphComment* pTag, return true; } -namespace { - -template -inline void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, - const T& value) { - pTag->itemListMap()[key] = value; -} - -void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, - const QString& value) { - if (value.isEmpty()) { - pTag->itemListMap().erase(key); - } else { - writeMP4Atom(pTag, key, TagLib::StringList(toTagLibString(value))); - } -} - -} // anonymous namespace - bool writeTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { - // Write common metadata - if (!writeTrackMetadataIntoTag(pTag, trackMetadata)) { - return false; - } + DEBUG_ASSERT(pTag); + + writeTrackMetadataIntoTag(pTag, trackMetadata); writeMP4Atom(pTag, "aART", trackMetadata.getAlbumArtist()); writeMP4Atom(pTag, "\251wrt", trackMetadata.getComposer()); @@ -993,4 +757,199 @@ bool writeTrackMetadataIntoMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& t return true; } +Result readTrackMetadataFromFile(TrackMetadata* pTrackMetadata, QString fileName) { + return readTrackMetadataAndCoverArtFromFile(pTrackMetadata, NULL, fileName); +} + +Result readCoverArtFromFile(QImage* pCoverArt, QString fileName) { + return readTrackMetadataAndCoverArtFromFile(NULL, pCoverArt, fileName); +} + +Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImage* pCoverArt, QString fileName) { + const QString fileType(getFileTypeFromFileName(fileName)); + + qDebug() << "Reading tags from file" << fileName << "of type" << fileType; + + // Rationale: If a file contains different types of tags only + // a single type of tag will be read. Tag types are read in a + // fixed order. Both track metadata and cover art will be read + // from the same tag types. Only the first available tag type + // is read and data in subsequent tags is ignored. + + if (kFileTypeMP3 == fileType) { + TagLib::MPEG::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::ID3v2::Tag* pID3v2Tag = file.ID3v2Tag(); + if (pID3v2Tag) { + readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); + readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); + return OK; + } else { + const TagLib::APE::Tag* pAPETag = file.APETag(); + if (pAPETag) { + readTrackMetadataFromAPETag(pTrackMetadata, *pAPETag); + readCoverArtFromAPETag(pCoverArt, *pAPETag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } + } else if (kFileTypeMP4 == fileType) { + TagLib::MP4::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::MP4::Tag* pMP4Tag = file.tag(); + if (pMP4Tag) { + readTrackMetadataFromMP4Tag(pTrackMetadata, *pMP4Tag); + readCoverArtFromMP4Tag(pCoverArt, *pMP4Tag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } else if (kFileTypeFLAC == fileType) { + TagLib::FLAC::File file(fileName.toLocal8Bit().constData()); + if (pCoverArt) { + // FLAC files may contain cover art that is not part + // of any tag. Use the first picture from this list + // as the default picture for the cover art. + TagLib::List covers = file.pictureList(); + if (!covers.isEmpty()) { + std::list::iterator it = covers.begin(); + TagLib::FLAC::Picture* cover = *it; + *pCoverArt = QImage::fromData( + QByteArray(cover->data().data(), cover->data().size())); + } + } + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::Ogg::XiphComment* pXiphComment = + file.hasXiphComment() ? file.xiphComment() : NULL; + if (pXiphComment) { + readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); + readCoverArtFromXiphComment(pCoverArt, *pXiphComment); + return OK; + } else { + const TagLib::ID3v2::Tag* pID3v2Tag = + file.hasID3v2Tag() ? file.ID3v2Tag() : NULL; + if (pID3v2Tag) { + readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); + readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } + } else if (kFileTypeOggVorbis == fileType) { + TagLib::Ogg::Vorbis::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::Ogg::XiphComment* pXiphComment = file.tag(); + if (pXiphComment) { + readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); + readCoverArtFromXiphComment(pCoverArt, *pXiphComment); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } +#if TAGLIB_HAS_OPUSFILE + } else if (kFileTypeOggOpus == fileType) { + TagLib::Ogg::Opus::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::Ogg::XiphComment* pXiphComment = file.tag(); + if (pXiphComment) { + readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); + readCoverArtFromXiphComment(pCoverArt, *pXiphComment); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } +#endif // TAGLIB_HAS_OPUSFILE + } else if (kFileTypeWavPack == fileType) { + TagLib::WavPack::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::APE::Tag* pAPETag = file.APETag(); + if (pAPETag) { + readTrackMetadataFromAPETag(pTrackMetadata, *pAPETag); + readCoverArtFromAPETag(pCoverArt, *pAPETag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } else if (kFileTypeWAV == fileType) { + TagLib::RIFF::WAV::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { +#if TAGLIB_HAS_WAV_ID3V2TAG + const TagLib::ID3v2::Tag* pID3v2Tag = file.hasID3v2Tag() ? file.ID3v2Tag() : NULL; +#else + const TagLib::ID3v2::Tag* pID3v2Tag = file.tag(); +#endif + if (pID3v2Tag) { + readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); + readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } else if (kFileTypeAIFF == fileType) { + TagLib::RIFF::AIFF::File file(fileName.toLocal8Bit().constData()); + if (readAudioProperties(pTrackMetadata, file)) { + const TagLib::ID3v2::Tag* pID3v2Tag = file.tag(); + if (pID3v2Tag) { + readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); + readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); + return OK; + } else { + // fallback + const TagLib::Tag* pTag(file.tag()); + if (pTag) { + readTrackMetadataFromTag(pTrackMetadata, *pTag); + return OK; + } + } + } + } + + qWarning() << "File" << fileName << "does not contain any supported tags that could be read!"; + return ERR; +} + } //namespace Mixxx diff --git a/src/metadata/trackmetadatataglib.h b/src/metadata/trackmetadatataglib.h index c7f106f9cde..37905f436fc 100644 --- a/src/metadata/trackmetadatataglib.h +++ b/src/metadata/trackmetadatataglib.h @@ -32,27 +32,12 @@ Result readCoverArtFromFile(QImage* pCoverArt, QString fileName); //(both parameters are optional and might be NULL) Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImage* pCoverArt, QString fileName); -// Read metadata -// The general function readTag() is implicitly invoked -// from the specialized tag reading functions! -void readTrackMetadataFromTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag); +// Low-level tag read/write functions for testing purposes void readTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag); void readTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag); void readTrackMetadataFromXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag); void readTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP4::Tag& tag); - -// Read cover art -// In order to avoid processing images when it's not -// needed (TIO building), we must process it separately. -bool readCoverArtFromID3v2Tag(QImage* pCoverArt, const TagLib::ID3v2::Tag& tag); -bool readCoverArtFromAPETag(QImage* pCoverArt, const TagLib::APE::Tag& tag); -bool readCoverArtFromXiphComment(QImage* pCoverArt, const TagLib::Ogg::XiphComment& tag); -bool readCoverArtFromMP4Tag(QImage* pCoverArt, const TagLib::MP4::Tag& tag); - -// Write metadata -// The general function writeTag() is implicitly invoked -// from the specialized tag writing functions! bool writeTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata); bool writeTrackMetadataIntoAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata); From 04abe02bc2884d793a79f032ad91dabded7bb09a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 7 Feb 2015 00:47:25 +0100 Subject: [PATCH 172/481] Consolidate functions for writing tags --- src/library/browse/browsetablemodel.cpp | 2 +- src/library/dao/trackdao.cpp | 4 +- src/metadata/audiotagger.cpp | 81 +------------------------ src/metadata/trackmetadatataglib.cpp | 81 +++++++++++++++++++++++++ src/metadata/trackmetadatataglib.h | 5 +- 5 files changed, 90 insertions(+), 83 deletions(-) diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp index c6ad695fb37..b5cd9696381 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/browse/browsetablemodel.cpp @@ -372,7 +372,7 @@ bool BrowseTableModel::setData(const QModelIndex &index, const QVariant &value, QStandardItem* item = itemFromIndex(index); QString track_location = getTrackLocation(index); AudioTagger tagger(track_location, m_current_directory.token()); - if (tagger.save(trackMetadata)) { + if (OK == tagger.save(trackMetadata)) { // Modify underlying interalPointer object item->setText(value.toString()); item->setToolTip(item->text()); diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 40b6b8c58e6..2b2ac0fb5b5 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -1792,7 +1792,9 @@ void TrackDAO::writeMetadataToFile(TrackInfoObject* pTrack) { pTrack->getMetadata(&trackMetadata); AudioTagger tagger(pTrack->getLocation(), pTrack->getSecurityToken()); - tagger.save(trackMetadata); + if (OK != tagger.save(trackMetadata)) { + qWarning() << "Failed to write track metadata:" << pTrack->getLocation(); + } } } diff --git a/src/metadata/audiotagger.cpp b/src/metadata/audiotagger.cpp index a62f86069af..f4b7d8bee5c 100644 --- a/src/metadata/audiotagger.cpp +++ b/src/metadata/audiotagger.cpp @@ -2,23 +2,6 @@ #include "metadata/trackmetadatataglib.h" -#include "util/assert.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) -#include -#endif - -#include - namespace { inline SecurityTokenPointer openSecurityToken(QFileInfo file, @@ -41,67 +24,5 @@ AudioTagger::~AudioTagger() { } bool AudioTagger::save(const Mixxx::TrackMetadata& trackMetadata) { - QScopedPointer pFile; - - const QString filePath(m_file.canonicalFilePath()); - const QByteArray filePathByteArray( - m_file.canonicalFilePath().toLocal8Bit()); - const char* filePathChars = filePathByteArray.constData(); - - if (filePath.endsWith(".mp3", Qt::CaseInsensitive)) { - QScopedPointer pMpegFile( - new TagLib::MPEG::File(filePathChars)); - writeTrackMetadataIntoID3v2Tag(pMpegFile->ID3v2Tag(true), trackMetadata); // mandatory - writeTrackMetadataIntoAPETag(pMpegFile->APETag(false), trackMetadata); // optional - pFile.reset(pMpegFile.take()); // transfer ownership - } else if (filePath.endsWith(".m4a", Qt::CaseInsensitive)) { - QScopedPointer pMp4File( - new TagLib::MP4::File(filePathChars)); - writeTrackMetadataIntoMP4Tag(pMp4File->tag(), trackMetadata); - pFile.reset(pMp4File.take()); // transfer ownership - } else if (filePath.endsWith(".ogg", Qt::CaseInsensitive)) { - QScopedPointer pOggFile( - new TagLib::Ogg::Vorbis::File(filePathChars)); - writeTrackMetadataIntoXiphComment(pOggFile->tag(), trackMetadata); - pFile.reset(pOggFile.take()); // transfer ownership - } else if (filePath.endsWith(".flac", Qt::CaseInsensitive)) { - QScopedPointer pFlacFile( - new TagLib::FLAC::File(filePathChars)); - writeTrackMetadataIntoXiphComment(pFlacFile->xiphComment(true), trackMetadata); // mandatory - writeTrackMetadataIntoID3v2Tag(pFlacFile->ID3v2Tag(false), trackMetadata); // optional - pFile.reset(pFlacFile.take()); // transfer ownership - } else if (filePath.endsWith(".wav", Qt::CaseInsensitive)) { - QScopedPointer pWavFile( - new TagLib::RIFF::WAV::File(filePathChars)); - writeTrackMetadataIntoID3v2Tag(pWavFile->tag(), trackMetadata); - pFile.reset(pWavFile.take()); // transfer ownership - } else if (filePath.endsWith(".aif", Qt::CaseInsensitive) - || filePath.endsWith(".aiff", Qt::CaseInsensitive)) { - QScopedPointer pAiffFile( - new TagLib::RIFF::AIFF::File(filePathChars)); - writeTrackMetadataIntoID3v2Tag(pAiffFile->tag(), trackMetadata); - pFile.reset(pAiffFile.take()); // transfer ownership -#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) - } else if (filePath.endsWith(".opus", Qt::CaseInsensitive)) { - QScopedPointer pOpusFile( - new TagLib::Ogg::Opus::File(filePathChars)); - writeTrackMetadataIntoXiphComment(pOpusFile->tag(), trackMetadata); - pFile.reset(pOpusFile.take()); // transfer ownership -#endif - } else { - qWarning() - << "Unsupported file type! Could not update metadata of track " - << filePath; - return false; - } - - // write audio tags to file - DEBUG_ASSERT(pFile); - if (pFile->save()) { - qDebug() << "Successfully updated metadata of track " << filePath; - return true; - } else { - qWarning() << "Failed to update metadata of track " << filePath; - return false; - } + return OK == writeTrackMetadataIntoFile(trackMetadata, m_file.canonicalFilePath()); } diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 1fc8795eb2f..a64f3336d3f 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -952,4 +952,85 @@ Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImag return ERR; } +Result writeTrackMetadataIntoFile(const TrackMetadata& trackMetadata, QString fileName) { + const QString fileType(getFileTypeFromFileName(fileName)); + + qDebug() << "Writing track metadata into file" << fileName << "of type" << fileType; + + QScopedPointer pFile; + + if (kFileTypeMP3 == fileType) { + QScopedPointer pMPEGFile( + new TagLib::MPEG::File(fileName.toLocal8Bit().constData())); + bool defaultID3V2 = true; + if (pMPEGFile->hasAPETag()) { + writeTrackMetadataIntoAPETag(pMPEGFile->APETag(), trackMetadata); + // Only write ID3v2 tag if it already exists + defaultID3V2 = false; + } + if (defaultID3V2 || pMPEGFile->hasID3v2Tag()) { + writeTrackMetadataIntoID3v2Tag(pMPEGFile->ID3v2Tag(defaultID3V2), trackMetadata); + } + pFile.reset(pMPEGFile.take()); // transfer ownership + } else if (kFileTypeMP4 == fileType) { + QScopedPointer pMP4File( + new TagLib::MP4::File(fileName.toLocal8Bit().constData())); + writeTrackMetadataIntoMP4Tag(pMP4File->tag(), trackMetadata); + pFile.reset(pMP4File.take()); // transfer ownership + } else if (kFileTypeFLAC == fileType) { + QScopedPointer pFLACFile( + new TagLib::FLAC::File(fileName.toLocal8Bit().constData())); + bool defaultXiphComment = true; + if (pFLACFile->hasID3v2Tag()) { + writeTrackMetadataIntoID3v2Tag(pFLACFile->ID3v2Tag(), trackMetadata); + // Only write Xiph Comment if it already exists + defaultXiphComment = false; + } + if (defaultXiphComment || pFLACFile->hasXiphComment()) { + writeTrackMetadataIntoXiphComment(pFLACFile->xiphComment(defaultXiphComment), trackMetadata); + } + pFile.reset(pFLACFile.take()); // transfer ownership + } else if (kFileTypeOggVorbis == fileType) { + QScopedPointer pOggVorbisFile( + new TagLib::Ogg::Vorbis::File(fileName.toLocal8Bit().constData())); + writeTrackMetadataIntoXiphComment(pOggVorbisFile->tag(), trackMetadata); + pFile.reset(pOggVorbisFile.take()); // transfer ownership +#if TAGLIB_HAS_OPUSFILE + } else if (kFileTypeOggOpus == fileType) { + QScopedPointer pOggOpusFile( + new TagLib::Ogg::Opus::File(fileName.toLocal8Bit().constData())); + writeTrackMetadataIntoXiphComment(pOggOpusFile->tag(), trackMetadata); + pFile.reset(pOggOpusFile.take()); // transfer ownership +#endif // TAGLIB_HAS_OPUSFILE + } else if (kFileTypeWavPack == fileType) { + QScopedPointer pWavPackFile( + new TagLib::WavPack::File(fileName.toLocal8Bit().constData())); + writeTrackMetadataIntoAPETag(pWavPackFile->APETag(true), trackMetadata); + pFile.reset(pWavPackFile.take()); // transfer ownership + } else if (kFileTypeWAV == fileType) { + QScopedPointer pWAVFile( + new TagLib::RIFF::WAV::File(fileName.toLocal8Bit().constData())); +#if TAGLIB_HAS_WAV_ID3V2TAG + writeTrackMetadataIntoID3v2Tag(pWAVFile->ID3v2Tag(), trackMetadata); +#else + writeTrackMetadataIntoID3v2Tag(pWavFile->tag(), trackMetadata); +#endif + pFile.reset(pWAVFile.take()); // transfer ownership + } else if (kFileTypeAIFF == fileType) { + QScopedPointer pAIFFFile( + new TagLib::RIFF::AIFF::File(fileName.toLocal8Bit().constData())); + writeTrackMetadataIntoID3v2Tag(pAIFFFile->tag(), trackMetadata); + pFile.reset(pAIFFFile.take()); // transfer ownership + } + if (!pFile) { + qWarning() << "Failed to write track metadata into file" << fileName << "of type" << fileType; + return ERR; + } + if (pFile->save()) { + qWarning() << "Failed to save file" << fileName; + return ERR; + } + return OK; +} + } //namespace Mixxx diff --git a/src/metadata/trackmetadatataglib.h b/src/metadata/trackmetadatataglib.h index 37905f436fc..5100739f172 100644 --- a/src/metadata/trackmetadatataglib.h +++ b/src/metadata/trackmetadatataglib.h @@ -32,7 +32,10 @@ Result readCoverArtFromFile(QImage* pCoverArt, QString fileName); //(both parameters are optional and might be NULL) Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImage* pCoverArt, QString fileName); -// Low-level tag read/write functions for testing purposes +// Write track metadata into the file with the given name +Result writeTrackMetadataIntoFile(const TrackMetadata& trackMetadata, QString fileName); + +// Low-level tag read/write functions are exposed only for testing purposes! void readTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag); void readTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag); void readTrackMetadataFromXiphComment(TrackMetadata* pTrackMetadata, From 50536edc756eda07e1db1f1454bfb689be0ed364 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sat, 7 Feb 2015 00:49:56 +0100 Subject: [PATCH 173/481] Experiments with the delay --- src/effects/native/autopaneffect.cpp | 78 +++++++++++++++++++++++++--- src/effects/native/autopaneffect.h | 9 ++++ src/sampleutil.cpp | 10 ++-- src/sampleutil.h | 2 +- 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index 6c563749aa2..e321cde24a8 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -4,6 +4,7 @@ #include "effects/native/autopaneffect.h" #include "sampleutil.h" +#include "util/experiment.h" const float positionRampingThreshold = 0.005f; @@ -36,7 +37,7 @@ EffectManifest AutoPanEffect::getManifest() { strength->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); strength->setMinimum(0.0); strength->setMaximum(0.5); // there are two steps per period so max is half - strength->setDefault(0.5); + strength->setDefault(0.25); // On my mixer, the period is defined as a multiple of the BPM // I wonder if I should implement it as the bouncing is really nice @@ -50,7 +51,21 @@ EffectManifest AutoPanEffect::getManifest() { period->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); period->setMinimum(1.0); period->setMaximum(500.0); - period->setDefault(50.0); + period->setDefault(220.0); + + // On my mixer, the period is defined as a multiple of the BPM + // I wonder if I should implement it as the bouncing is really nice + // when it is synced. + EffectManifestParameter* delay = manifest.addParameter(); + delay->setId("delay"); + delay->setName(QObject::tr("delay")); + delay->setDescription("Controls length of the delay"); + delay->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + delay->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + delay->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + delay->setMinimum(0.0); + delay->setMaximum(500.0); + delay->setDefault(300.0); return manifest; } @@ -58,7 +73,8 @@ EffectManifest AutoPanEffect::getManifest() { AutoPanEffect::AutoPanEffect(EngineEffect* pEffect, const EffectManifest& manifest) : m_pCurveParameter(pEffect->getParameterById("curve")), - m_pPeriodParameter(pEffect->getParameterById("period")) + m_pPeriodParameter(pEffect->getParameterById("period")), + m_pDelayParameter(pEffect->getParameterById("delay")) { Q_UNUSED(manifest); } @@ -119,12 +135,19 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat gs.frac.setRampingThreshold(positionRampingThreshold); gs.frac.ramped = false; // just for debug + // float delay = round(m_pDelayParameter->value()); + // gs.delay->setDelay(delay); + // gs.delay->process(pInput, gs.m_pDelayBuf, 2048); + + float quarter; + for (unsigned int i = 0; i + 1 < numSamples; i += 2) { - CSAMPLE periodFraction = CSAMPLE(gs.time) / period; + CSAMPLE periodFraction = (CSAMPLE(gs.time) / period); // current quarter in the trigonometric circle - float quarter = floorf(periodFraction * 4.0f); + // float quarter = floorf(periodFraction * 4.0f); + quarter = floorf(periodFraction * 4.0f); // part of the period fraction being a step (not in the slope) CSAMPLE stepsFractionPart = floorf((quarter+1.0f)/2.0f) * stepFrac; @@ -145,19 +168,58 @@ void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupStat gs.frac.setWithRampingApplied( (sin(M_PI * 2.0f * angleFraction) + 1.0f) / 2.0f); - pOutput[i] = pInput[i] * gs.frac * lawCoef; - pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; + if (Experiment::isExperiment()) { + // delay on the reducing channel + + if ( quarter == 0.0 || quarter == 3.0 ){ + // channel 1 increasing + pOutput[i] = pInput[i] * gs.frac * lawCoef; + pOutput[i+1] = (gs.m_pDelayBuf[i+1] + pInput[i+1]) / 2 + * (1.0f - gs.frac) * lawCoef; + // pOutput[i+1] = gs.m_pDelayBuf[i+1] * (1.0f - gs.frac) * lawCoef; + + } else { + // channel 2 increasing + // pOutput[i] = pInput[i] * gs.frac * lawCoef; + pOutput[i] = (gs.m_pDelayBuf[i] + pInput[i]) / 2 + * gs.frac * lawCoef; + pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; + } + + } else if (Experiment::isBase()) { + // delay on both sides + pOutput[i] = (gs.m_pDelayBuf[i] + pInput[i]) / 2 * gs.frac * lawCoef; + pOutput[i+1] = (gs.m_pDelayBuf[i+1] + pInput[i+1]) / 2 * (1.0f - gs.frac) * lawCoef; + + } else { + // no delay + pOutput[i] = pInput[i] * gs.frac * lawCoef; + pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; + } gs.time++; } + /** + * todo + * apply delay with ramping + * + */ + float delay = round(m_pDelayParameter->value()); + gs.delay->setDelay(delay); + gs.delay->process(pInput, gs.m_pDelayBuf, 2048); + /**/ qDebug() - << "| ramped :" << gs.frac.ramped + // << "| ramped :" << gs.frac.ramped + << "| quarter :" << quarter + << "| delay :" << delay // << "| beat_length :" << groupFeatures.beat_length // << "| period :" << period << "| frac :" << gs.frac << "| time :" << gs.time + << "| numSamples :" << numSamples ; + /**/ } diff --git a/src/effects/native/autopaneffect.h b/src/effects/native/autopaneffect.h index 0421bb213c7..e13e6d0eb3d 100644 --- a/src/effects/native/autopaneffect.h +++ b/src/effects/native/autopaneffect.h @@ -10,6 +10,8 @@ #include "engine/effects/engineeffectparameter.h" #include "effects/effectprocessor.h" #include "sampleutil.h" +#include "engine/enginefilterdelay.h" + // This class provides a float value that cannot be increased or decreased // by more than a given value to avoid clicks. @@ -57,15 +59,21 @@ class RampedSample { bool initialized; }; +static const int panMaxDelay = 3300; // allows a 30 Hz filter at 97346; struct PanGroupState { PanGroupState() { time = 0; + delay = new EngineFilterDelay(); + m_pDelayBuf = SampleUtil::alloc(MAX_BUFFER_LEN); } ~PanGroupState() { + // todo delete buffer } unsigned int time; RampedSample frac; + EngineFilterDelay* delay; + CSAMPLE* m_pDelayBuf; }; @@ -94,6 +102,7 @@ class AutoPanEffect : public GroupEffectProcessor { EngineEffectParameter* m_pCurveParameter; EngineEffectParameter* m_pPeriodParameter; + EngineEffectParameter* m_pDelayParameter; DISALLOW_COPY_AND_ASSIGN(AutoPanEffect); }; diff --git a/src/sampleutil.cpp b/src/sampleutil.cpp index 84f5c7b0613..9bf2f8ad86f 100644 --- a/src/sampleutil.cpp +++ b/src/sampleutil.cpp @@ -152,7 +152,7 @@ void SampleUtil::copyWithGain(CSAMPLE* pDest, const CSAMPLE* pSrc, // static void SampleUtil::copyWithRampingGain(CSAMPLE* pDest, const CSAMPLE* pSrc, CSAMPLE_GAIN old_gain, CSAMPLE_GAIN new_gain, - unsigned int iNumSamples) { + unsigned int iNumSamples, bool left/*=true*/, bool right/*=true*/) { if (old_gain == CSAMPLE_GAIN_ONE && new_gain == CSAMPLE_GAIN_ONE) { copy(pDest, pSrc, iNumSamples); return; @@ -166,8 +166,12 @@ void SampleUtil::copyWithRampingGain(CSAMPLE* pDest, const CSAMPLE* pSrc, / CSAMPLE_GAIN(iNumSamples / 2); CSAMPLE_GAIN gain = old_gain; for (unsigned int i = 0; i < iNumSamples; i += 2, gain += gain_delta) { - pDest[i] = pSrc[i] * gain; - pDest[i + 1] = pSrc[i + 1] * gain; + if( left ){ + pDest[i] = pSrc[i] * gain; + } + if( right ){ + pDest[i + 1] = pSrc[i + 1] * gain; + } } // OR! need to test which fares better diff --git a/src/sampleutil.h b/src/sampleutil.h index 804e8259806..67182e2f8f8 100644 --- a/src/sampleutil.h +++ b/src/sampleutil.h @@ -99,7 +99,7 @@ class SampleUtil { // if pDest == pSrc! static void copyWithRampingGain(CSAMPLE* pDest, const CSAMPLE* pSrc, CSAMPLE_GAIN old_gain, CSAMPLE_GAIN new_gain, - unsigned int iNumSamples); + unsigned int iNumSamples, bool left=true, bool right=true); // Add each sample of pSrc, multiplied by the gain, to pDest static void addWithGain(CSAMPLE* pDest, const CSAMPLE* pSrc, From 1180aa905339d79fdcae5230fbf3453eaded85a9 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 7 Feb 2015 12:40:59 +0100 Subject: [PATCH 174/481] Parse and set the common numeric year tag if available --- src/metadata/trackmetadatataglib.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index a64f3336d3f..57ce70ef1b1 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -313,15 +313,11 @@ void writeTrackMetadataIntoTag(TagLib::Tag* pTag, const TrackMetadata& trackMeta pTag->setGenre(toTagLibString(trackMetadata.getGenre())); pTag->setComment(toTagLibString(trackMetadata.getComment())); - // Only write the year into the common tag if the corresponding - // field contains a 4-character numeric string. - const QString yearString(trackMetadata.getYear().trimmed()); - if (4 == yearString.length()) { - bool yearValid = false; - uint year = trackMetadata.getYear().toUInt(&yearValid); - if (yearValid && (year > 0)) { - pTag->setYear(year); - } + // Set the numeric year if available + const QDate yearDate( + TrackMetadata::parseDateTime(trackMetadata.getYear()).date()); + if (yearDate.isValid()) { + pTag->setYear(yearDate.year()); } // Derived tags might be able to write the complete string // from trackMetadata.getYear() into the corresponding field. From cd5a4a6d0d4c0b9cf5a9362d86ef6a3e4d0911b4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 7 Feb 2015 13:04:56 +0100 Subject: [PATCH 175/481] Normalize year string when read from a TrackInfoObject --- src/metadata/trackmetadata.cpp | 12 ++++++++++++ src/metadata/trackmetadata.h | 12 +++++------- src/trackinfoobject.cpp | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 3e55e0e7fc3..45b5b9091f6 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -130,6 +130,18 @@ QString TrackMetadata::formatReplayGain(double replayGain) { } } +QString TrackMetadata::normalizeYear(QString year) { + const QDateTime dateTime(parseDateTime(year)); + if (dateTime.isValid()) { + return dateTime.toString(); + } + const QDate date(dateTime.date()); + if (date.isValid()) { + return date.toString(); + } + return year; +} + TrackMetadata::TrackMetadata() : m_channels(0), m_sampleRate(0), diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index b66cb40ebe3..39198010cbd 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -175,17 +175,12 @@ class TrackMetadata { static double parseReplayGain(QString sReplayGain, bool* pValid = 0); static QString formatReplayGain(double replayGain); - // Normalize a year string - inline static QString normalizeYear(QString year) { - return year.simplified().replace(" ", ""); - } - // Parse an format date/time values according to ISO 8601 inline static QDate parseDate(QString str) { - return QDate::fromString(normalizeYear(str), Qt::ISODate); + return QDate::fromString(str, Qt::ISODate); } inline static QDateTime parseDateTime(QString str) { - return QDateTime::fromString(normalizeYear(str), Qt::ISODate); + return QDateTime::fromString(str, Qt::ISODate); } inline static QString formatDate(QDate date) { return date.toString(Qt::ISODate); @@ -194,6 +189,9 @@ class TrackMetadata { return dateTime.toString(Qt::ISODate); } + // Normalize a year string + static QString normalizeYear(QString year); + private: QString m_artist; QString m_title; diff --git a/src/trackinfoobject.cpp b/src/trackinfoobject.cpp index 088d77f5835..2ad475290f4 100644 --- a/src/trackinfoobject.cpp +++ b/src/trackinfoobject.cpp @@ -226,7 +226,7 @@ void TrackInfoObject::getMetadata(Mixxx::TrackMetadata* pTrackMetadata) { pTrackMetadata->setTitle(getTitle()); pTrackMetadata->setAlbum(getAlbum()); pTrackMetadata->setAlbumArtist(getAlbumArtist()); - pTrackMetadata->setYear(getYear()); + pTrackMetadata->setYear(Mixxx::TrackMetadata::normalizeYear(getYear())); pTrackMetadata->setGenre(getGenre()); pTrackMetadata->setComposer(getComposer()); pTrackMetadata->setGrouping(getGrouping()); From c80d96efc5ad67518ea7f7a6668ed0652848e38e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 7 Feb 2015 14:32:20 +0100 Subject: [PATCH 176/481] Fix reading/writing of TDRC and/or TYER/TDAT for ID3v2 tags --- src/metadata/trackmetadata.cpp | 25 +++++++++--- src/metadata/trackmetadata.h | 4 +- src/metadata/trackmetadatataglib.cpp | 61 ++++++++++++++++++++-------- src/test/metadatatest.cpp | 9 ++-- 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 45b5b9091f6..5b0f840d941 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -131,14 +131,27 @@ QString TrackMetadata::formatReplayGain(double replayGain) { } QString TrackMetadata::normalizeYear(QString year) { - const QDateTime dateTime(parseDateTime(year)); - if (dateTime.isValid()) { - return dateTime.toString(); + const QDateTime yearDateTime(parseDateTime(year)); + if (yearDateTime.isValid()) { + return yearDateTime.toString(); } - const QDate date(dateTime.date()); - if (date.isValid()) { - return date.toString(); + const QDate yearDate(yearDateTime.date()); + if (yearDate.isValid()) { + return yearDate.toString(); } + bool yearUIntValid = false; + const int yearUInt = year.toUInt(&yearUIntValid); + if (yearUIntValid) { + if (0 == yearUInt) { + // special case: empty + return QString(); + } + const QString yyyy(QString::number(yearUInt)); + if (4 == yyyy.length()) { + return yyyy; + } + } + // unmodified return year; } diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 39198010cbd..b6e25671cf0 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -177,10 +177,10 @@ class TrackMetadata { // Parse an format date/time values according to ISO 8601 inline static QDate parseDate(QString str) { - return QDate::fromString(str, Qt::ISODate); + return QDate::fromString(str.trimmed().replace(" ", ""), Qt::ISODate); } inline static QDateTime parseDateTime(QString str) { - return QDateTime::fromString(str, Qt::ISODate); + return QDateTime::fromString(str.trimmed().replace(" ", ""), Qt::ISODate); } inline static QString formatDate(QDate date) { return date.toString(Qt::ISODate); diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 57ce70ef1b1..0a2cb1f3c0b 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -272,7 +272,7 @@ void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { } void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, - const TagLib::ByteVector &id, const QString& text) { + const TagLib::ByteVector &id, const QString& text, bool isNumericOrURL = false) { DEBUG_ASSERT(pTag); TagLib::String::Type textType; @@ -288,11 +288,17 @@ void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, // independent of the byte order. textType = TagLib::String::UTF8; } else { - // For ID3v2.3.0/ID3v2.2.0 use UCS-2 (UTF-16 encoded Unicode - // with BOM), because UTF-8 and UTF-16BE are only supported - // since ID3v2.4.0 and the alternative ISO-8859-1 does not - // cover all Unicode characters. - textType = TagLib::String::UTF16; + if (isNumericOrURL) { + // According to the ID3v2.3.0 specification: "All numeric + // strings and URLs are always encoded as ISO-8859-1." + textType = TagLib::String::Latin1; + } else { + // For ID3v2.3.0 use UCS-2 (UTF-16 encoded Unicode with BOM) + // for arbitrary text, because UTF-8 and UTF-16BE are only + // supported since ID3v2.4.0 and the alternative ISO-8859-1 + // does not cover all Unicode characters. + textType = TagLib::String::UTF16; + } } QScopedPointer pNewFrame( new TagLib::ID3v2::TextIdentificationFrame(id, textType)); @@ -378,19 +384,24 @@ void readTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, // ID3v2.4.0: TDRC replaces TYER + TDAT const QString recordingTime( toQStringFirst(tag.frameListMap()["TDRC"])); - if (!recordingTime.isEmpty()) { - pTrackMetadata->setYear(recordingTime); + if ((4 <= tag.header()->majorVersion()) && !recordingTime.isEmpty()) { + pTrackMetadata->setYear(recordingTime); } else { // Fallback to TYER + TDAT const QString recordingYear( toQStringFirst(tag.frameListMap()["TYER"]).trimmed()); QString year(recordingYear); - if (4 == recordingYear.length()) { + if (ID3V2_TYER_FORMAT.length() == recordingYear.length()) { const QString recordingDate( toQStringFirst(tag.frameListMap()["TDAT"]).trimmed()); - if (4 == recordingDate.length()) { - const QDate date(QDate::fromString(recordingYear + recordingDate, ID3V2_TYER_FORMAT + ID3V2_TDAT_FORMAT)); - year = TrackMetadata::formatDate(date); + if (ID3V2_TDAT_FORMAT.length() == recordingDate.length()) { + const QDate date( + QDate::fromString( + recordingYear + recordingDate, + ID3V2_TYER_FORMAT + ID3V2_TDAT_FORMAT)); + if (date.isValid()) { + year = TrackMetadata::formatDate(date); + } } } if (!year.isEmpty()) { @@ -640,22 +651,36 @@ bool writeTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, // integer and represented as a numerical string." // Reference: http://id3.org/id3v2.3.0 writeID3v2TextIdentificationFrame(pTag, "TBPM", - TrackMetadata::formatBpm(trackMetadata.getBpmAsInteger())); + TrackMetadata::formatBpm(trackMetadata.getBpmAsInteger()), true); writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); writeID3v2TextIdentificationFrame(pTag, "TCOM", trackMetadata.getComposer()); writeID3v2TextIdentificationFrame(pTag, "TIT1", trackMetadata.getGrouping()); - if (4 <= pHeader->majorVersion()) { - // ID3v2.4.0: TDRC replaces TYER + TDAT + // NOTE(uklotz): Need to overwrite the TDRC frame if it + // already exists. TagLib (1.9.x) writes a TDRC frame + // even for ID3v2.3.0 tags if the numeric year is set. + if ((4 <= pHeader->majorVersion()) || !pTag->frameList("TDRC").isEmpty()) { writeID3v2TextIdentificationFrame(pTag, "TDRC", trackMetadata.getYear()); - } else { + } + if (4 > pHeader->majorVersion()) { // Fallback to TYER + TDAT const QDate date(TrackMetadata::parseDate(trackMetadata.getYear())); if (date.isValid()) { - writeID3v2TextIdentificationFrame(pTag, "TYER", date.toString(ID3V2_TYER_FORMAT)); - writeID3v2TextIdentificationFrame(pTag, "TDAT", date.toString(ID3V2_TDAT_FORMAT)); + // Valid date + writeID3v2TextIdentificationFrame(pTag, "TYER", date.toString(ID3V2_TYER_FORMAT), true); + writeID3v2TextIdentificationFrame(pTag, "TDAT", date.toString(ID3V2_TDAT_FORMAT), true); + } else { + // Numeric year + bool yearUIntValid = false; + const int yearUInt = trackMetadata.getYear().toUInt(&yearUIntValid); + if (yearUIntValid) { + const QString tyer(QString::number(yearUInt)); + if (ID3V2_TYER_FORMAT.length() == tyer.length()) { + writeID3v2TextIdentificationFrame(pTag, "TYER", tyer, true); + } + } } } diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index 7b2335c2758..86fd02887d2 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -117,7 +117,7 @@ TEST_F(MetadataTest, ID3v2Year) { " 2001-01-01", "2002 -12 - 31 ", "2015 -02 - 04T 18:43", - "2015 -02 - 04 18:43" + "2015 -02 - 04 18:43", "2015 -02 - 04 18:43 followed by arbitrary text" }; // Only ID3v2.3.0 and ID3v2.4.0 are supported for writing @@ -125,7 +125,6 @@ TEST_F(MetadataTest, ID3v2Year) { qDebug() << "majorVersion" << majorVersion; for (size_t i = 0; i < sizeof(kYears) / sizeof(kYears[0]); ++i) { const QString year(kYears[i]); - qDebug() << "year" << year; TagLib::ID3v2::Tag tag; tag.header()->setMajorVersion(majorVersion); { @@ -137,14 +136,14 @@ TEST_F(MetadataTest, ID3v2Year) { readTrackMetadataFromID3v2Tag(&trackMetadata, tag); if (4 > majorVersion) { // ID3v2.3.0: parsed + formatted - const QString expectedYear(Mixxx::TrackMetadata::normalizeYear(year)); const QString actualYear(trackMetadata.getYear()); - const QDate expectedDate(Mixxx::TrackMetadata::parseDate(expectedYear)); + const QDate expectedDate(Mixxx::TrackMetadata::parseDate(year)); if (expectedDate.isValid()) { // Only the date part can be stored in an ID3v2.3.0 tag EXPECT_EQ(Mixxx::TrackMetadata::formatDate(expectedDate), actualYear); } else { - EXPECT_EQ(expectedYear, actualYear); + // numeric year (without month/day) + EXPECT_EQ(Mixxx::TrackMetadata::normalizeYear(year), actualYear); } } else { // ID3v2.4.0: currently unverified/unmodified From 2535da32f1c3b1a9b587f242c9fdb3ce1b6f044d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 7 Feb 2015 15:05:29 +0100 Subject: [PATCH 177/481] Add function for parsing the numeric year --- src/metadata/trackmetadata.cpp | 9 +++++++++ src/metadata/trackmetadata.h | 1 + 2 files changed, 10 insertions(+) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 5b0f840d941..b218676511d 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -155,6 +155,15 @@ QString TrackMetadata::normalizeYear(QString year) { return year; } +uint TrackMetadata::parseNumericYear(QString year) { + const QDateTime yearDateTime(parseDateTime(year)); + if (yearDateTime.date().isValid()) { + return yearDateTime.date().year(); + } else { + return year.toUInt(); + } +} + TrackMetadata::TrackMetadata() : m_channels(0), m_sampleRate(0), diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index b6e25671cf0..e02797ecb2e 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -191,6 +191,7 @@ class TrackMetadata { // Normalize a year string static QString normalizeYear(QString year); + static uint parseNumericYear(QString year); private: QString m_artist; From acc00d26a9b6add0cabb2c7e36d22271c7317e50 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 7 Feb 2015 17:37:00 +0100 Subject: [PATCH 178/481] Display the calendar year in browse columns Fixes bug #1418083 --- src/library/basesqltablemodel.cpp | 7 +++---- src/library/browse/browsethread.cpp | 8 +++++--- src/library/browse/browsethread.h | 2 +- src/metadata/trackmetadata.cpp | 31 +++++++++++++++++++++++++++-- src/metadata/trackmetadata.h | 6 +++++- src/test/metadatatest.cpp | 18 +++++++++++++++++ 6 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/library/basesqltablemodel.cpp b/src/library/basesqltablemodel.cpp index 61484e98396..7fcf8282048 100644 --- a/src/library/basesqltablemodel.cpp +++ b/src/library/basesqltablemodel.cpp @@ -16,6 +16,7 @@ #include "playermanager.h" #include "playerinfo.h" #include "track/keyutils.h" +#include "metadata/trackmetadata.h" #include "util/time.h" #include "util/dnd.h" #include "util/assert.h" @@ -542,10 +543,8 @@ QVariant BaseSqlTableModel::data(const QModelIndex& index, int role) const { } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM_LOCK)) { value = value.toBool(); } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_YEAR)) { - int year = value.toInt(); - if (year <= 0) { - // clear invalid values - value = QString(); + if (Qt::DisplayRole == role) { + value = Mixxx::TrackMetadata::formatCalendarYear(value.toString()); } } else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TRACKNUMBER)) { int track_number = value.toInt(); diff --git a/src/library/browse/browsethread.cpp b/src/library/browse/browsethread.cpp index e79cfb6245f..95219af6eba 100644 --- a/src/library/browse/browsethread.cpp +++ b/src/library/browse/browsethread.cpp @@ -9,6 +9,7 @@ #include "library/browse/browsetablemodel.h" #include "soundsourceproxy.h" +#include "metadata/trackmetadata.h" #include "util/time.h" #include "util/trace.h" @@ -156,9 +157,10 @@ void BrowseThread::populateModel() { item->setData(item->text().toInt(), Qt::UserRole); row_data.insert(COLUMN_TRACK_NUMBER, item); - item = new QStandardItem(tio.getYear()); - item->setToolTip(item->text()); - item->setData(item->text().toInt(), Qt::UserRole); + const QString year(tio.getYear()); + item = new QStandardItem(year); + item->setToolTip(year); + item->setData(Mixxx::TrackMetadata::formatCalendarYear(year), Qt::UserRole); row_data.insert(COLUMN_YEAR, item); item = new QStandardItem(tio.getGenre()); diff --git a/src/library/browse/browsethread.h b/src/library/browse/browsethread.h index eb8988d8899..cee56c2dade 100644 --- a/src/library/browse/browsethread.h +++ b/src/library/browse/browsethread.h @@ -19,7 +19,7 @@ // that is used to read ID3 metadata // from a particular folder. // -// The BroseTableModel uses this class. +// The BrowseTableModel uses this class. // Note: Don't call getInstance() from places // other than the GUI thread. class BrowseTableModel; diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index b218676511d..fe8327fc255 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -155,13 +155,40 @@ QString TrackMetadata::normalizeYear(QString year) { return year; } -uint TrackMetadata::parseNumericYear(QString year) { +uint TrackMetadata::parseCalendarYear(QString year) { const QDateTime yearDateTime(parseDateTime(year)); if (yearDateTime.date().isValid()) { return yearDateTime.date().year(); } else { - return year.toUInt(); + bool uintValid = false; + uint uintYear = year.toUInt(&uintValid); + if (uintValid) { + return uintYear; + } } + return kCalendarYearInvalid; +} + +QString TrackMetadata::formatCalendarYear(QString year) { + const QDateTime yearDateTime(parseDateTime(year)); + if (yearDateTime.date().isValid()) { + // numeric year + return QString::number(yearDateTime.date().year()); + } else { + bool uintValid = false; + uint uintYear = year.toUInt(&uintValid); + if (uintValid) { + if (kCalendarYearInvalid == uintYear) { + // empty string + return QString(); + } else { + // numeric string + return QString::number(uintYear); + } + } + } + // unmodified + return year; } TrackMetadata::TrackMetadata() : diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index e02797ecb2e..3f069be4756 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -191,7 +191,11 @@ class TrackMetadata { // Normalize a year string static QString normalizeYear(QString year); - static uint parseNumericYear(QString year); + + // Parse and format the calendar year (for simplified display) + static const uint kCalendarYearInvalid = 0; + static uint parseCalendarYear(QString year); + static QString formatCalendarYear(QString year); private: QString m_artist; diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index 86fd02887d2..c6cb30782fb 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -153,4 +153,22 @@ TEST_F(MetadataTest, ID3v2Year) { } } +TEST_F(MetadataTest, CalendarYear) { + // Parsing + EXPECT_EQ(uint(2014), Mixxx::TrackMetadata::parseCalendarYear("2014-04-29T07:00:00Z")); + EXPECT_EQ(uint(2014), Mixxx::TrackMetadata::parseCalendarYear("2014-04-29")); + EXPECT_EQ(uint(2014), Mixxx::TrackMetadata::parseCalendarYear("2014")); + EXPECT_EQ(uint(0), Mixxx::TrackMetadata::parseCalendarYear("0")); + EXPECT_EQ(uint(0), Mixxx::TrackMetadata::parseCalendarYear("-1")); + EXPECT_EQ(uint(0), Mixxx::TrackMetadata::parseCalendarYear("year")); + + // Formatting + EXPECT_EQ("2014", Mixxx::TrackMetadata::formatCalendarYear("2014-04-29T07:00:00Z")); + EXPECT_EQ("2014", Mixxx::TrackMetadata::formatCalendarYear("2014-04-29")); + EXPECT_EQ("2014", Mixxx::TrackMetadata::formatCalendarYear("2014")); + EXPECT_EQ("", Mixxx::TrackMetadata::formatCalendarYear("0")); + EXPECT_EQ("-1", Mixxx::TrackMetadata::formatCalendarYear("-1")); + EXPECT_EQ("year", Mixxx::TrackMetadata::formatCalendarYear("year")); +} + } // namespace From 830d9c90fe798067b34e1f3aa58dde407dfd1680 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 8 Feb 2015 20:27:48 +0100 Subject: [PATCH 179/481] Add TODO and NOTE about the year column in browse mode --- src/library/browse/browsethread.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/library/browse/browsethread.cpp b/src/library/browse/browsethread.cpp index 95219af6eba..ce08584183b 100644 --- a/src/library/browse/browsethread.cpp +++ b/src/library/browse/browsethread.cpp @@ -157,10 +157,18 @@ void BrowseThread::populateModel() { item->setData(item->text().toInt(), Qt::UserRole); row_data.insert(COLUMN_TRACK_NUMBER, item); + // TODO(XXX): Display the calendar year + // calendarYear = Mixxx::TrackMetadata::formatCalendarYear(year) + // in the overview instead of the complete ISO 8601 year string. + // The complete year string should only be displayed when editing + // the year cell. + // NOTE(uklotzde): QStandardItem does not distinguish between + // Qt::DisplayRole and Qt::EditRole! const QString year(tio.getYear()); item = new QStandardItem(year); item->setToolTip(year); - item->setData(Mixxx::TrackMetadata::formatCalendarYear(year), Qt::UserRole); + // Column is sorted according to the numeric calendar year + item->setData(Mixxx::TrackMetadata::parseCalendarYear(year), Qt::UserRole); row_data.insert(COLUMN_YEAR, item); item = new QStandardItem(tio.getGenre()); From 5aee33b174124714a8bb0941be53e19321dc6c77 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 9 Feb 2015 17:27:07 +0100 Subject: [PATCH 180/481] Only display the calendar year in browse overview --- src/library/browse/browsethread.cpp | 34 +++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/library/browse/browsethread.cpp b/src/library/browse/browsethread.cpp index ce08584183b..51f260a8d10 100644 --- a/src/library/browse/browsethread.cpp +++ b/src/library/browse/browsethread.cpp @@ -90,6 +90,29 @@ void BrowseThread::run() { m_mutex.unlock(); } +namespace { + +class YearItem: public QStandardItem { +public: + explicit YearItem(QString year): + QStandardItem(year) { + } + + QVariant data(int role) const { + switch (role) { + case Qt::DisplayRole: + { + const QString year(QStandardItem::data(role).toString()); + return Mixxx::TrackMetadata::formatCalendarYear(year); + } + default: + return QStandardItem::data(role); + } + } +}; + +} + void BrowseThread::populateModel() { m_path_mutex.lock(); MDir thisPath = m_path; @@ -157,17 +180,10 @@ void BrowseThread::populateModel() { item->setData(item->text().toInt(), Qt::UserRole); row_data.insert(COLUMN_TRACK_NUMBER, item); - // TODO(XXX): Display the calendar year - // calendarYear = Mixxx::TrackMetadata::formatCalendarYear(year) - // in the overview instead of the complete ISO 8601 year string. - // The complete year string should only be displayed when editing - // the year cell. - // NOTE(uklotzde): QStandardItem does not distinguish between - // Qt::DisplayRole and Qt::EditRole! const QString year(tio.getYear()); - item = new QStandardItem(year); + item = new YearItem(year); item->setToolTip(year); - // Column is sorted according to the numeric calendar year + // The year column is sorted according to the numeric calendar year item->setData(Mixxx::TrackMetadata::parseCalendarYear(year), Qt::UserRole); row_data.insert(COLUMN_YEAR, item); From 575576f0f37402ef86074202db34f91404b838bc Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 12 Feb 2015 01:06:37 +0100 Subject: [PATCH 181/481] Rename BPM and Replay Gain constants --- src/metadata/trackmetadata.cpp | 42 +++++++++++++++++----------------- src/metadata/trackmetadata.h | 20 ++++++++-------- src/test/metadatatest.cpp | 16 ++++++------- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index fe8327fc255..a925b0cf322 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -4,32 +4,32 @@ namespace Mixxx { -/*static*/ const double TrackMetadata::BPM_UNDEFINED = 0.0; -/*static*/ const double TrackMetadata::BPM_MIN = 0.0; // lower bound (inclusive) -/*static*/ const double TrackMetadata::BPM_MAX = 300.0; // lower bound (inclusive) +/*static*/ const double TrackMetadata::kBpmUndefined = 0.0; +/*static*/ const double TrackMetadata::kBpmMin = 0.0; // lower bound (exclusive) +/*static*/ const double TrackMetadata::kBpmMax = 300.0; // upper bound (inclusive) -/*static*/ const double TrackMetadata::REPLAYGAIN_UNDEFINED = 0.0; -/*static*/ const double TrackMetadata::REPLAYGAIN_MIN = 0.0; // lower bound (inclusive) -/*static*/ const double TrackMetadata::REPLAYGAIN_0DB = 1.0; +/*static*/ const double TrackMetadata::kReplayGainUndefined = 0.0; +/*static*/ const double TrackMetadata::kReplayGainMin = 0.0; // lower bound (inclusive) +/*static*/ const double TrackMetadata::kReplayGain0dB = 1.0; double TrackMetadata::parseBpm(const QString& sBpm, bool* pValid) { if (pValid) { *pValid = false; } if (sBpm.trimmed().isEmpty()) { - return BPM_UNDEFINED; + return kBpmUndefined; } bool bpmValid = false; double bpm = sBpm.toDouble(&bpmValid); if (bpmValid) { - if (BPM_UNDEFINED == bpm) { + if (kBpmUndefined == bpm) { // special case if (pValid) { *pValid = true; } return bpm; } - while (BPM_MAX < bpm) { + while (kBpmMax < bpm) { // Some applications might store the BPM as an // integer scaled by a factor of 10 or 100 to // preserve fractional digits. @@ -47,7 +47,7 @@ double TrackMetadata::parseBpm(const QString& sBpm, bool* pValid) { } else { qDebug() << "Failed to parse BPM:" << sBpm; } - return BPM_UNDEFINED; + return kBpmUndefined; } QString TrackMetadata::formatBpm(double bpm) { @@ -68,8 +68,8 @@ QString TrackMetadata::formatBpm(int bpm) { namespace { -const QString REPLAYGAIN_UNIT("dB"); -const QString REPLAYGAIN_SUFFIX(" " + REPLAYGAIN_UNIT); +const QString kReplayGainUnit("dB"); +const QString kReplayGainSuffix(" " + kReplayGainUnit); } // anonymous namespace @@ -83,30 +83,30 @@ double TrackMetadata::parseReplayGain(QString sReplayGain, bool* pValid) { // strip leading "+" normalizedReplayGain = normalizedReplayGain.mid(plusIndex + 1).trimmed(); } - const int unitIndex = normalizedReplayGain.lastIndexOf(REPLAYGAIN_UNIT, -1, Qt::CaseInsensitive); + const int unitIndex = normalizedReplayGain.lastIndexOf(kReplayGainUnit, -1, Qt::CaseInsensitive); if ((0 <= unitIndex) && ((normalizedReplayGain.length() - 2) == unitIndex)) { // strip trailing unit suffix normalizedReplayGain = normalizedReplayGain.left(unitIndex).trimmed(); } if (normalizedReplayGain.isEmpty()) { - return REPLAYGAIN_UNDEFINED; + return kReplayGainUndefined; } bool replayGainValid = false; const double replayGainDb = normalizedReplayGain.toDouble(&replayGainValid); if (replayGainValid) { const double replayGain = db2ratio(replayGainDb); - DEBUG_ASSERT(REPLAYGAIN_UNDEFINED != replayGain); + DEBUG_ASSERT(kReplayGainUndefined != replayGain); // Some applications (e.g. Rapid Evolution 3) write a replay gain // of 0 dB even if the replay gain is undefined. To be safe we // ignore this special value and instead prefer to recalculate // the replay gain. - if (REPLAYGAIN_0DB == replayGain) { + if (kReplayGain0dB == replayGain) { // special case qDebug() << "Ignoring possibly undefined replay gain:" << formatReplayGain(replayGain); if (pValid) { *pValid = true; } - return REPLAYGAIN_UNDEFINED; + return kReplayGainUndefined; } if (TrackMetadata::isReplayGainValid(replayGain)) { if (pValid) { @@ -119,12 +119,12 @@ double TrackMetadata::parseReplayGain(QString sReplayGain, bool* pValid) { } else { qDebug() << "Failed to parse replay gain:" << sReplayGain; } - return REPLAYGAIN_UNDEFINED; + return kReplayGainUndefined; } QString TrackMetadata::formatReplayGain(double replayGain) { if (isReplayGainValid(replayGain)) { - return QString::number(ratio2db(replayGain)) + REPLAYGAIN_SUFFIX; + return QString::number(ratio2db(replayGain)) + kReplayGainSuffix; } else { return QString(); } @@ -196,8 +196,8 @@ TrackMetadata::TrackMetadata() : m_sampleRate(0), m_bitrate(0), m_duration(0), - m_bpm(BPM_UNDEFINED), - m_replayGain(REPLAYGAIN_UNDEFINED) { + m_bpm(kBpmUndefined), + m_replayGain(kReplayGainUndefined) { } } //namespace Mixxx diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 3f069be4756..b4bf35828ae 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -123,9 +123,9 @@ class TrackMetadata { } // beats / minute - static const double BPM_UNDEFINED; - static const double BPM_MIN; // lower bound (exclusive) - static const double BPM_MAX; // upper bound (inclusive) + static const double kBpmUndefined; + static const double kBpmMin; // lower bound (exclusive) + static const double kBpmMax; // upper bound (inclusive) inline double getBpm() const { return m_bpm; } @@ -133,7 +133,7 @@ class TrackMetadata { return round(getBpm()); } inline static bool isBpmValid(double bpm) { - return (BPM_MIN < bpm) && (BPM_MAX >= bpm); + return (kBpmMin < bpm) && (kBpmMax >= bpm); } inline bool isBpmValid() const { return isBpmValid(getBpm()); @@ -142,17 +142,17 @@ class TrackMetadata { m_bpm = bpm; } inline void resetBpm() { - m_bpm = BPM_UNDEFINED; + m_bpm = kBpmUndefined; } - static const double REPLAYGAIN_UNDEFINED; - static const double REPLAYGAIN_MIN; // lower bound (exclusive) - static const double REPLAYGAIN_0DB; + static const double kReplayGainUndefined; + static const double kReplayGainMin; // lower bound (exclusive) + static const double kReplayGain0dB; inline double getReplayGain() const { return m_replayGain; } inline static bool isReplayGainValid(double replayGain) { - return REPLAYGAIN_MIN < replayGain; + return kReplayGainMin < replayGain; } inline bool isReplayGainValid() const { return isReplayGainValid(getReplayGain()); @@ -161,7 +161,7 @@ class TrackMetadata { m_replayGain = replayGain; } inline void resetReplayGain() { - m_replayGain = REPLAYGAIN_UNDEFINED; + m_replayGain = kReplayGainUndefined; } // Parse and format BPM metadata diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index c6cb30782fb..bef375aa084 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -57,7 +57,7 @@ TEST_F(MetadataTest, ParseBpmPrecision) { } TEST_F(MetadataTest, ParseBpmValidRange) { - for (int bpm100 = int(Mixxx::TrackMetadata::BPM_MIN) * 100; int(Mixxx::TrackMetadata::BPM_MAX) * 100 >= bpm100; ++bpm100) { + for (int bpm100 = int(Mixxx::TrackMetadata::kBpmMin) * 100; int(Mixxx::TrackMetadata::kBpmMax) * 100 >= bpm100; ++bpm100) { const double expectedValue = bpm100 / 100.0; const QString inputValues[] = { QString("%1").arg(expectedValue), @@ -75,9 +75,9 @@ TEST_F(MetadataTest, ParseBpmDecimalScaling) { } TEST_F(MetadataTest, ParseBpmInvalid) { - parseBpm("", false, Mixxx::TrackMetadata::BPM_UNDEFINED); - parseBpm("abcde", false, Mixxx::TrackMetadata::BPM_UNDEFINED); - parseBpm("0 dBA", false, Mixxx::TrackMetadata::BPM_UNDEFINED); + parseBpm("", false, Mixxx::TrackMetadata::kBpmUndefined); + parseBpm("abcde", false, Mixxx::TrackMetadata::kBpmUndefined); + parseBpm("0 dBA", false, Mixxx::TrackMetadata::kBpmUndefined); } TEST_F(MetadataTest, ParseReplayGainDbValidRange) { @@ -94,7 +94,7 @@ TEST_F(MetadataTest, ParseReplayGainDbValidRange) { expectedValue = db2ratio(double(replayGainDb)); } else { // special case: 0 dB -> undefined - expectedValue = Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED; + expectedValue = Mixxx::TrackMetadata::kReplayGainUndefined; } for (size_t i = 0; i < sizeof(inputValues) / sizeof(inputValues[0]); ++i) { parseReplayGain(inputValues[i], true, expectedValue); @@ -106,9 +106,9 @@ TEST_F(MetadataTest, ParseReplayGainDbValidRange) { } TEST_F(MetadataTest, ParseReplayGainDbInvalid) { - parseReplayGain("", false, Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED); - parseReplayGain("abcde", false, Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED); - parseReplayGain("0 dBA", false, Mixxx::TrackMetadata::REPLAYGAIN_UNDEFINED); + parseReplayGain("", false, Mixxx::TrackMetadata::kReplayGainUndefined); + parseReplayGain("abcde", false, Mixxx::TrackMetadata::kReplayGainUndefined); + parseReplayGain("0 dBA", false, Mixxx::TrackMetadata::kReplayGainUndefined); } TEST_F(MetadataTest, ID3v2Year) { From 12604eb5a89e34ba0369d193e2eacaf1ac6bdae8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 12 Feb 2015 01:07:46 +0100 Subject: [PATCH 182/481] Improve parsing and formatting of year strings in metadata --- src/metadata/trackmetadata.cpp | 98 +++++++++++++++++----------------- src/metadata/trackmetadata.h | 11 ++-- src/test/metadatatest.cpp | 15 +++--- src/trackinfoobject.cpp | 2 +- 4 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index a925b0cf322..f1cf7e4b979 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -12,6 +12,8 @@ namespace Mixxx { /*static*/ const double TrackMetadata::kReplayGainMin = 0.0; // lower bound (inclusive) /*static*/ const double TrackMetadata::kReplayGain0dB = 1.0; +/*static*/ const int TrackMetadata::kCalendarYearEmpty = 0; // displayed as an empty string + double TrackMetadata::parseBpm(const QString& sBpm, bool* pValid) { if (pValid) { *pValid = false; @@ -130,65 +132,63 @@ QString TrackMetadata::formatReplayGain(double replayGain) { } } -QString TrackMetadata::normalizeYear(QString year) { - const QDateTime yearDateTime(parseDateTime(year)); - if (yearDateTime.isValid()) { - return yearDateTime.toString(); - } - const QDate yearDate(yearDateTime.date()); - if (yearDate.isValid()) { - return yearDate.toString(); - } - bool yearUIntValid = false; - const int yearUInt = year.toUInt(&yearUIntValid); - if (yearUIntValid) { - if (0 == yearUInt) { - // special case: empty - return QString(); - } - const QString yyyy(QString::number(yearUInt)); - if (4 == yyyy.length()) { - return yyyy; - } - } - // unmodified - return year; -} - -uint TrackMetadata::parseCalendarYear(QString year) { +int TrackMetadata::parseCalendarYear(QString year, bool* pValid) { const QDateTime yearDateTime(parseDateTime(year)); if (yearDateTime.date().isValid()) { + if (pValid) { + *pValid = true; + } return yearDateTime.date().year(); } else { - bool uintValid = false; - uint uintYear = year.toUInt(&uintValid); - if (uintValid) { - return uintYear; + bool calendarYearValid = false; + const int calendarYear = year.toInt(&calendarYearValid); + if (pValid) { + *pValid = calendarYearValid; + } + if (calendarYearValid) { + return calendarYear; + } else { + return kCalendarYearEmpty; } } - return kCalendarYearInvalid; } -QString TrackMetadata::formatCalendarYear(QString year) { - const QDateTime yearDateTime(parseDateTime(year)); - if (yearDateTime.date().isValid()) { - // numeric year - return QString::number(yearDateTime.date().year()); - } else { - bool uintValid = false; - uint uintYear = year.toUInt(&uintValid); - if (uintValid) { - if (kCalendarYearInvalid == uintYear) { - // empty string - return QString(); - } else { - // numeric string - return QString::number(uintYear); - } +QString TrackMetadata::formatCalendarYear(QString year, bool* pValid) { + bool calendarYearValid = false; + int calendarYear = parseCalendarYear(year, &calendarYearValid); + if (pValid) { + *pValid = calendarYearValid; + } + if (calendarYearValid) { + if (kCalendarYearEmpty == calendarYear) { + return QString(); // empty string + } else { + return QString::number(calendarYear); } + } else { + return year; // unmodified } - // unmodified - return year; +} + +QString TrackMetadata::reformatYear(QString year) { + const QDateTime dateTime(parseDateTime(year)); + if (dateTime.isValid()) { + // date/time + return formatDateTime(dateTime); + } + const QDate date(dateTime.date()); + if (date.isValid()) { + // only date + return formatDate(date); + } + bool calendarYearValid = false; + const QString calendarYear(formatCalendarYear(year, &calendarYearValid)); + if (calendarYearValid) { + // only calendar year + return calendarYear; + } + // just trim and simplify whitespaces + return year.simplified(); } TrackMetadata::TrackMetadata() : diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index b4bf35828ae..656dbc6ff46 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -189,13 +189,12 @@ class TrackMetadata { return dateTime.toString(Qt::ISODate); } - // Normalize a year string - static QString normalizeYear(QString year); - // Parse and format the calendar year (for simplified display) - static const uint kCalendarYearInvalid = 0; - static uint parseCalendarYear(QString year); - static QString formatCalendarYear(QString year); + static const int kCalendarYearEmpty; // displayed as an empty string + static int parseCalendarYear(QString year, bool* pValid = 0); + static QString formatCalendarYear(QString year, bool* pValid = 0); + + static QString reformatYear(QString year); private: QString m_artist; diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index bef375aa084..b1eb32a0e2f 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -143,7 +143,7 @@ TEST_F(MetadataTest, ID3v2Year) { EXPECT_EQ(Mixxx::TrackMetadata::formatDate(expectedDate), actualYear); } else { // numeric year (without month/day) - EXPECT_EQ(Mixxx::TrackMetadata::normalizeYear(year), actualYear); + EXPECT_EQ(Mixxx::TrackMetadata::reformatYear(year), actualYear); } } else { // ID3v2.4.0: currently unverified/unmodified @@ -155,12 +155,13 @@ TEST_F(MetadataTest, ID3v2Year) { TEST_F(MetadataTest, CalendarYear) { // Parsing - EXPECT_EQ(uint(2014), Mixxx::TrackMetadata::parseCalendarYear("2014-04-29T07:00:00Z")); - EXPECT_EQ(uint(2014), Mixxx::TrackMetadata::parseCalendarYear("2014-04-29")); - EXPECT_EQ(uint(2014), Mixxx::TrackMetadata::parseCalendarYear("2014")); - EXPECT_EQ(uint(0), Mixxx::TrackMetadata::parseCalendarYear("0")); - EXPECT_EQ(uint(0), Mixxx::TrackMetadata::parseCalendarYear("-1")); - EXPECT_EQ(uint(0), Mixxx::TrackMetadata::parseCalendarYear("year")); + EXPECT_EQ(2014, Mixxx::TrackMetadata::parseCalendarYear("2014-04-29T07:00:00Z")); + EXPECT_EQ(2014, Mixxx::TrackMetadata::parseCalendarYear("2014-04-29")); + EXPECT_EQ(2014, Mixxx::TrackMetadata::parseCalendarYear("2014")); + EXPECT_EQ(1, Mixxx::TrackMetadata::parseCalendarYear("1")); + EXPECT_EQ(0, Mixxx::TrackMetadata::parseCalendarYear("0")); + EXPECT_EQ(-1, Mixxx::TrackMetadata::parseCalendarYear("-1")); + EXPECT_EQ(Mixxx::TrackMetadata::kCalendarYearEmpty, Mixxx::TrackMetadata::parseCalendarYear("year")); // Formatting EXPECT_EQ("2014", Mixxx::TrackMetadata::formatCalendarYear("2014-04-29T07:00:00Z")); diff --git a/src/trackinfoobject.cpp b/src/trackinfoobject.cpp index 2ad475290f4..296e358249c 100644 --- a/src/trackinfoobject.cpp +++ b/src/trackinfoobject.cpp @@ -226,7 +226,7 @@ void TrackInfoObject::getMetadata(Mixxx::TrackMetadata* pTrackMetadata) { pTrackMetadata->setTitle(getTitle()); pTrackMetadata->setAlbum(getAlbum()); pTrackMetadata->setAlbumArtist(getAlbumArtist()); - pTrackMetadata->setYear(Mixxx::TrackMetadata::normalizeYear(getYear())); + pTrackMetadata->setYear(Mixxx::TrackMetadata::reformatYear(getYear())); pTrackMetadata->setGenre(getGenre()); pTrackMetadata->setComposer(getComposer()); pTrackMetadata->setGrouping(getGrouping()); From 97e764a43ee0856a6a5c7b7a19fd8fcb08cbd438 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 12 Feb 2015 17:00:50 +0100 Subject: [PATCH 183/481] Fix file header --- src/metadata/trackmetadatataglib.h | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/metadata/trackmetadatataglib.h b/src/metadata/trackmetadatataglib.h index 5100739f172..6773211a0c6 100644 --- a/src/metadata/trackmetadatataglib.h +++ b/src/metadata/trackmetadatataglib.h @@ -1,14 +1,5 @@ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -#ifndef SOUNDSOURCETAGLIB_H -#define SOUNDSOURCETAGLIB_H +#ifndef TRACKMETADATATAGLIB_H +#define TRACKMETADATATAGLIB_H #include "metadata/trackmetadata.h" #include "util/defs.h" // Result From ff6cf1d3693fb0435335e496ce8673cf6a96d294 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 12 Feb 2015 23:22:46 +0100 Subject: [PATCH 184/481] Parse the calendar year of incomplete dates --- src/metadata/trackmetadata.cpp | 19 +++++++++++++++---- src/test/metadatatest.cpp | 4 ++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index f1cf7e4b979..dc26a349faf 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -133,15 +133,26 @@ QString TrackMetadata::formatReplayGain(double replayGain) { } int TrackMetadata::parseCalendarYear(QString year, bool* pValid) { - const QDateTime yearDateTime(parseDateTime(year)); - if (yearDateTime.date().isValid()) { + const QDateTime dateTime(parseDateTime(year)); + if (0 < dateTime.date().year()) { if (pValid) { *pValid = true; } - return yearDateTime.date().year(); + return dateTime.date().year(); } else { bool calendarYearValid = false; - const int calendarYear = year.toInt(&calendarYearValid); + // Ignore everything beginning with the first dash '-' + // to successfully parse the calendar year of incomplete + // dates like yyyy-MM or 2015-W07. + const QString calendarYearSection(year.section('-', 0, 0).trimmed()); + int calendarYear = kCalendarYearEmpty; + if (calendarYearSection.isEmpty()) { + // leading '-' minus sign -> parse the whole string + calendarYear = year.toInt(&calendarYearValid); + } else { + // parse only the 1st non-empty section + calendarYear = calendarYearSection.toInt(&calendarYearValid); + } if (pValid) { *pValid = calendarYearValid; } diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index b1eb32a0e2f..15fed80716a 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -158,6 +158,8 @@ TEST_F(MetadataTest, CalendarYear) { EXPECT_EQ(2014, Mixxx::TrackMetadata::parseCalendarYear("2014-04-29T07:00:00Z")); EXPECT_EQ(2014, Mixxx::TrackMetadata::parseCalendarYear("2014-04-29")); EXPECT_EQ(2014, Mixxx::TrackMetadata::parseCalendarYear("2014")); + EXPECT_EQ(2015, Mixxx::TrackMetadata::parseCalendarYear("2015-02")); + EXPECT_EQ(1997, Mixxx::TrackMetadata::parseCalendarYear("1997-W43")); EXPECT_EQ(1, Mixxx::TrackMetadata::parseCalendarYear("1")); EXPECT_EQ(0, Mixxx::TrackMetadata::parseCalendarYear("0")); EXPECT_EQ(-1, Mixxx::TrackMetadata::parseCalendarYear("-1")); @@ -167,6 +169,8 @@ TEST_F(MetadataTest, CalendarYear) { EXPECT_EQ("2014", Mixxx::TrackMetadata::formatCalendarYear("2014-04-29T07:00:00Z")); EXPECT_EQ("2014", Mixxx::TrackMetadata::formatCalendarYear("2014-04-29")); EXPECT_EQ("2014", Mixxx::TrackMetadata::formatCalendarYear("2014")); + EXPECT_EQ("2015", Mixxx::TrackMetadata::formatCalendarYear("2015-02")); + EXPECT_EQ("1997", Mixxx::TrackMetadata::formatCalendarYear("1997-W43")); EXPECT_EQ("", Mixxx::TrackMetadata::formatCalendarYear("0")); EXPECT_EQ("-1", Mixxx::TrackMetadata::formatCalendarYear("-1")); EXPECT_EQ("year", Mixxx::TrackMetadata::formatCalendarYear("year")); From 373c4bf2ba9b5c065c24479c345c337a0f13d14b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 12 Feb 2015 23:34:46 +0100 Subject: [PATCH 185/481] Display empty year column if calendar year is not available --- src/metadata/trackmetadata.cpp | 22 +++++++--------------- src/metadata/trackmetadata.h | 2 +- src/test/metadatatest.cpp | 10 +++++----- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index dc26a349faf..f3b38fc2da0 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -12,7 +12,7 @@ namespace Mixxx { /*static*/ const double TrackMetadata::kReplayGainMin = 0.0; // lower bound (inclusive) /*static*/ const double TrackMetadata::kReplayGain0dB = 1.0; -/*static*/ const int TrackMetadata::kCalendarYearEmpty = 0; // displayed as an empty string +/*static*/ const int TrackMetadata::kCalendarYearInvalid = 0; double TrackMetadata::parseBpm(const QString& sBpm, bool* pValid) { if (pValid) { @@ -145,13 +145,9 @@ int TrackMetadata::parseCalendarYear(QString year, bool* pValid) { // to successfully parse the calendar year of incomplete // dates like yyyy-MM or 2015-W07. const QString calendarYearSection(year.section('-', 0, 0).trimmed()); - int calendarYear = kCalendarYearEmpty; - if (calendarYearSection.isEmpty()) { - // leading '-' minus sign -> parse the whole string - calendarYear = year.toInt(&calendarYearValid); - } else { - // parse only the 1st non-empty section - calendarYear = calendarYearSection.toInt(&calendarYearValid); + const int calendarYear = calendarYearSection.toInt(&calendarYearValid); + if (calendarYearValid) { + calendarYearValid = 0 < calendarYear; } if (pValid) { *pValid = calendarYearValid; @@ -159,7 +155,7 @@ int TrackMetadata::parseCalendarYear(QString year, bool* pValid) { if (calendarYearValid) { return calendarYear; } else { - return kCalendarYearEmpty; + return kCalendarYearInvalid; } } } @@ -171,13 +167,9 @@ QString TrackMetadata::formatCalendarYear(QString year, bool* pValid) { *pValid = calendarYearValid; } if (calendarYearValid) { - if (kCalendarYearEmpty == calendarYear) { - return QString(); // empty string - } else { - return QString::number(calendarYear); - } + return QString::number(calendarYear); } else { - return year; // unmodified + return QString(); // empty string } } diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 656dbc6ff46..9d7efc1734e 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -190,7 +190,7 @@ class TrackMetadata { } // Parse and format the calendar year (for simplified display) - static const int kCalendarYearEmpty; // displayed as an empty string + static const int kCalendarYearInvalid; static int parseCalendarYear(QString year, bool* pValid = 0); static QString formatCalendarYear(QString year, bool* pValid = 0); diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index 15fed80716a..60caf2068d7 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -161,9 +161,9 @@ TEST_F(MetadataTest, CalendarYear) { EXPECT_EQ(2015, Mixxx::TrackMetadata::parseCalendarYear("2015-02")); EXPECT_EQ(1997, Mixxx::TrackMetadata::parseCalendarYear("1997-W43")); EXPECT_EQ(1, Mixxx::TrackMetadata::parseCalendarYear("1")); - EXPECT_EQ(0, Mixxx::TrackMetadata::parseCalendarYear("0")); - EXPECT_EQ(-1, Mixxx::TrackMetadata::parseCalendarYear("-1")); - EXPECT_EQ(Mixxx::TrackMetadata::kCalendarYearEmpty, Mixxx::TrackMetadata::parseCalendarYear("year")); + EXPECT_EQ(Mixxx::TrackMetadata::kCalendarYearInvalid, Mixxx::TrackMetadata::parseCalendarYear("0")); + EXPECT_EQ(Mixxx::TrackMetadata::kCalendarYearInvalid, Mixxx::TrackMetadata::parseCalendarYear("-1")); + EXPECT_EQ(Mixxx::TrackMetadata::kCalendarYearInvalid, Mixxx::TrackMetadata::parseCalendarYear("year")); // Formatting EXPECT_EQ("2014", Mixxx::TrackMetadata::formatCalendarYear("2014-04-29T07:00:00Z")); @@ -172,8 +172,8 @@ TEST_F(MetadataTest, CalendarYear) { EXPECT_EQ("2015", Mixxx::TrackMetadata::formatCalendarYear("2015-02")); EXPECT_EQ("1997", Mixxx::TrackMetadata::formatCalendarYear("1997-W43")); EXPECT_EQ("", Mixxx::TrackMetadata::formatCalendarYear("0")); - EXPECT_EQ("-1", Mixxx::TrackMetadata::formatCalendarYear("-1")); - EXPECT_EQ("year", Mixxx::TrackMetadata::formatCalendarYear("year")); + EXPECT_EQ("", Mixxx::TrackMetadata::formatCalendarYear("-1")); + EXPECT_EQ("", Mixxx::TrackMetadata::formatCalendarYear("year")); } } // namespace From f19d98a70d2f9c391e52418049312f49d3ee2676 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 13 Feb 2015 14:23:38 +0100 Subject: [PATCH 186/481] Add fallback to calendar year when writing ID3v2.3.0 tag --- src/metadata/trackmetadatataglib.cpp | 13 +++++-------- src/test/metadatatest.cpp | 2 ++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 0a2cb1f3c0b..450dce2a428 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -672,14 +672,11 @@ bool writeTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, writeID3v2TextIdentificationFrame(pTag, "TYER", date.toString(ID3V2_TYER_FORMAT), true); writeID3v2TextIdentificationFrame(pTag, "TDAT", date.toString(ID3V2_TDAT_FORMAT), true); } else { - // Numeric year - bool yearUIntValid = false; - const int yearUInt = trackMetadata.getYear().toUInt(&yearUIntValid); - if (yearUIntValid) { - const QString tyer(QString::number(yearUInt)); - if (ID3V2_TYER_FORMAT.length() == tyer.length()) { - writeID3v2TextIdentificationFrame(pTag, "TYER", tyer, true); - } + // Fallback to calendar year + bool calendarYearValid = false; + const QString calendarYear(TrackMetadata::formatCalendarYear(trackMetadata.getYear(), &calendarYearValid)); + if (calendarYearValid) { + writeID3v2TextIdentificationFrame(pTag, "TYER", calendarYear, true); } } } diff --git a/src/test/metadatatest.cpp b/src/test/metadatatest.cpp index 60caf2068d7..2ea61f2309f 100644 --- a/src/test/metadatatest.cpp +++ b/src/test/metadatatest.cpp @@ -115,6 +115,8 @@ TEST_F(MetadataTest, ID3v2Year) { const char* kYears[] = { " 1987 ", " 2001-01-01", + "1997-12", // yyyy-MM + "1977-W43", // year + week "2002 -12 - 31 ", "2015 -02 - 04T 18:43", "2015 -02 - 04 18:43", From a6fa7b70f38720b3f5c12a38e894334ce2e14185 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 14 Feb 2015 18:42:13 +0100 Subject: [PATCH 187/481] Fix wrong warning log message when saving track metadata --- src/metadata/trackmetadatataglib.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 450dce2a428..88ee0911859 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -1044,7 +1044,7 @@ Result writeTrackMetadataIntoFile(const TrackMetadata& trackMetadata, QString fi qWarning() << "Failed to write track metadata into file" << fileName << "of type" << fileType; return ERR; } - if (pFile->save()) { + if (!pFile->save()) { qWarning() << "Failed to save file" << fileName; return ERR; } From 5daa0f52f19da2168a3a1b91eb7d3708bb84f731 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 19 Feb 2015 18:00:30 +0100 Subject: [PATCH 188/481] Finish reading/writing of replay gain in ID3v2 tags --- src/metadata/trackmetadatataglib.cpp | 118 +++++++++++++++++++-------- 1 file changed, 82 insertions(+), 36 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 88ee0911859..6a7ec88f707 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -114,9 +114,9 @@ inline QString toQString(const TagLib::APE::Item& apeItem) { return toQString(apeItem.toString()); } -inline TagLib::String toTagLibString(const QString& str, TagLib::String::Type textType = TagLib::String::UTF8) { +inline TagLib::String toTagLibString(const QString& str, TagLib::String::Type stringType = TagLib::String::UTF8) { const QByteArray qba(str.toUtf8()); - return TagLib::String(qba.constData(), textType); + return TagLib::String(qba.constData(), stringType); } inline bool parseBpm(TrackMetadata* pTrackMetadata, QString sBpm) { @@ -271,43 +271,94 @@ void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { pTag->addFrame(pFrame); } -void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, - const TagLib::ByteVector &id, const QString& text, bool isNumericOrURL = false) { - DEBUG_ASSERT(pTag); - - TagLib::String::Type textType; +TagLib::String::Type getID3v2StringType(const TagLib::ID3v2::Tag& tag, bool isNumericOrURL = false) { + TagLib::String::Type stringType; // For an overview of the character encodings supported by // the different ID3v2 versions please refer to the following // resources: // http://en.wikipedia.org/wiki/ID3#ID3v2 // http://id3.org/id3v2.3.0 // http://id3.org/id3v2.4.0-structure - if (4 <= pTag->header()->majorVersion()) { + if (4 <= tag.header()->majorVersion()) { // For ID3v2.4.0 or higher prefer UTF-8, because it is a // very compact representation for common cases and it is // independent of the byte order. - textType = TagLib::String::UTF8; + stringType = TagLib::String::UTF8; } else { if (isNumericOrURL) { // According to the ID3v2.3.0 specification: "All numeric // strings and URLs are always encoded as ISO-8859-1." - textType = TagLib::String::Latin1; + stringType = TagLib::String::Latin1; } else { // For ID3v2.3.0 use UCS-2 (UTF-16 encoded Unicode with BOM) // for arbitrary text, because UTF-8 and UTF-16BE are only // supported since ID3v2.4.0 and the alternative ISO-8859-1 // does not cover all Unicode characters. - textType = TagLib::String::UTF16; + stringType = TagLib::String::UTF16; + } + } + return stringType; +} + +TagLib::ID3v2::UserTextIdentificationFrame* findUserTextIdentificationFrame( + const TagLib::ID3v2::Tag& tag, QString description) { + TagLib::ID3v2::FrameList textFrames(tag.frameListMap()["TXXX"]); + for (TagLib::ID3v2::FrameList::ConstIterator it(textFrames.begin()); + it != textFrames.end(); ++it) { + TagLib::ID3v2::UserTextIdentificationFrame* pTextFrame = + dynamic_cast(*it); + if (pTextFrame) { + const QString textFrameDescription( + toQString(pTextFrame->description())); + if (0 == textFrameDescription.compare( + description, Qt::CaseInsensitive)) { + return pTextFrame; // found + } } } - QScopedPointer pNewFrame( - new TagLib::ID3v2::TextIdentificationFrame(id, textType)); - pNewFrame->setText(toTagLibString(text)); - replaceID3v2Frame(pTag, pNewFrame.data()); - // Now the plain pointer in pNewFrame is owned and + return 0; // not found +} + +void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, + const TagLib::ByteVector &id, const QString& text, bool isNumericOrURL = false) { + DEBUG_ASSERT(pTag); + + const TagLib::String::Type stringType = + getID3v2StringType(*pTag, isNumericOrURL); + QScopedPointer pTextFrame( + new TagLib::ID3v2::TextIdentificationFrame(id, stringType)); + pTextFrame->setText(toTagLibString(text, stringType)); + replaceID3v2Frame(pTag, pTextFrame.data()); + // Now the plain pointer in pTextFrame is owned and // managed by pTag. We need to release the ownership // to avoid double deletion! - pNewFrame.take(); + pTextFrame.take(); +} + +void writeID3v2UserTextIdentificationFrame(TagLib::ID3v2::Tag* pTag, + const QString& description, const QString& text, bool isNumericOrURL = false) { + TagLib::ID3v2::UserTextIdentificationFrame* pTextFrame = + findUserTextIdentificationFrame(*pTag, description); + if (pTextFrame) { + // Modify existing frame + const TagLib::String::Type stringType = + pTextFrame->textEncoding(); + pTextFrame->setDescription(toTagLibString(description, stringType)); + pTextFrame->setText(toTagLibString(text, stringType)); + } else { + // Add a new frame + const TagLib::String::Type stringType = + getID3v2StringType(*pTag, isNumericOrURL); + QScopedPointer pTextFrame( + new TagLib::ID3v2::UserTextIdentificationFrame(stringType)); + pTextFrame->setDescription(toTagLibString(description, stringType)); + pTextFrame->setText(toTagLibString(text, stringType)); + pTag->addFrame(pTextFrame.data()); + // Now the plain pointer in pTextFrame is owned and + // managed by pTag. We need to release the ownership + // to avoid double deletion! + pTextFrame.take(); + } } void writeTrackMetadataIntoTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { @@ -414,28 +465,19 @@ void readTrackMetadataFromID3v2Tag(TrackMetadata* pTrackMetadata, parseBpm(pTrackMetadata, toQStringFirst(bpmFrame)); } - // Foobar2000-style ID3v2.3.0 tags - // TODO: Check if everything is ok. - TagLib::ID3v2::FrameList textFrames(tag.frameListMap()["TXXX"]); - for (TagLib::ID3v2::FrameList::ConstIterator it(textFrames.begin()); - it != textFrames.end(); ++it) { - TagLib::ID3v2::UserTextIdentificationFrame* replaygainFrame = - dynamic_cast(*it); - if (replaygainFrame && replaygainFrame->fieldList().size() >= 2) { - const QString desc( - toQString(replaygainFrame->description())); - // Only read track gain (not album gain) - if (0 == desc.compare("REPLAYGAIN_TRACK_GAIN", Qt::CaseInsensitive)) { - parseReplayGain(pTrackMetadata, - toQString(replaygainFrame->fieldList()[1])); - } - } - } - const TagLib::ID3v2::FrameList keyFrame(tag.frameListMap()["TKEY"]); if (!keyFrame.isEmpty()) { pTrackMetadata->setKey(toQStringFirst(keyFrame)); } + + // Only read track gain (not album gain) + TagLib::ID3v2::UserTextIdentificationFrame* pReplayGainFrame = + findUserTextIdentificationFrame(tag, "REPLAYGAIN_TRACK_GAIN"); + if (pReplayGainFrame && (2 <= pReplayGainFrame->fieldList().size())) { + // The value is stored in the 2nd field + parseReplayGain(pTrackMetadata, + toQString(pReplayGainFrame->fieldList()[1])); + } } void readTrackMetadataFromAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { @@ -681,7 +723,11 @@ bool writeTrackMetadataIntoID3v2Tag(TagLib::ID3v2::Tag* pTag, } } - // TODO(uklotzde): Write TXXX - REPLAYGAIN_TRACK_GAIN + // Only write track gain (not album gain) + const QString replayGain( + TrackMetadata::formatReplayGain(trackMetadata.getReplayGain())); + writeID3v2UserTextIdentificationFrame( + pTag, "REPLAYGAIN_TRACK_GAIN", replayGain, true); return true; } From 8ed60afda2f4aa9070a858f7b2929261bcb71956 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 21 Feb 2015 14:17:19 +0100 Subject: [PATCH 189/481] Improve and extend seek tests --- src/test/soundproxy_test.cpp | 136 +++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 60 deletions(-) diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index a1f3b2385a9..3a3c3eee9d7 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -10,25 +10,31 @@ class SoundSourceProxyTest : public MixxxTest { protected: - Mixxx::SoundSourcePointer openSoundSource(const QString& fileName) { + static QStringList getFileExtensions() { + QStringList extensions; + extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; +#ifdef __OPUS__ + extensions << "opus"; +#endif + return extensions; + } + + static Mixxx::SoundSourcePointer openSoundSource(const QString& fileName) { return SoundSourceProxy(fileName, SecurityTokenPointer()).getSoundSource(); } - Mixxx::AudioSourcePointer openAudioSource(const QString& fileName) { + static Mixxx::AudioSourcePointer openAudioSource(const QString& fileName) { return SoundSourceProxy(fileName, SecurityTokenPointer()).openAudioSource(); } }; -TEST_F(SoundSourceProxyTest, ProxyCanOpen) { +TEST_F(SoundSourceProxyTest, open) { // This test piggy-backs off of the cover-test files. - const QString kCoverFilePath( + const QString kFilePathPrefix( QDir::currentPath() + "/src/test/id3-test-data/cover-test."); - QStringList extensions; - extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; - - foreach (const QString& extension, extensions) { - QString filePath = kCoverFilePath + extension; - EXPECT_TRUE(SoundSourceProxy::isFilenameSupported(filePath)); + foreach (const QString& fileExtension, getFileExtensions()) { + const QString filePath(kFilePathPrefix + fileExtension); + ASSERT_TRUE(SoundSourceProxy::isFilenameSupported(filePath)); Mixxx::AudioSourcePointer pAudioSource(openAudioSource(filePath)); ASSERT_TRUE(!pAudioSource.isNull()); @@ -59,65 +65,75 @@ TEST_F(SoundSourceProxyTest, TOAL_TPE2) { } TEST_F(SoundSourceProxyTest, seekForward) { - const unsigned int kSeekFrameIndex = 10000; - const unsigned int kTestFrameCount = 100; - - EXPECT_EQ(0, int(kSeekFrameIndex % kTestFrameCount)); - - const QString kFilePath( + const Mixxx::AudioSource::size_type kReadFrameCount = 10000; + + // According to API documentation of op_pcm_seek(): + // "...decoding after seeking may not return exactly the same + // values as would be obtained by decoding the stream straight + // through. However, such differences are expected to be smaller + // than the loss introduced by Opus's lossy compression." + // NOTE(uklotzde): The current version 0.6 of opusfile doesn't + // seem to support sample accurate seeking. The differences + // between the samples decoded with continuous reading and + // those samples decoded after seeking are quite noticeable! + const Mixxx::AudioSource::sample_type kOpusSeekDecodingError = 0.2f; + + const QString kFilePathPrefix( QDir::currentPath() + "/src/test/id3-test-data/cover-test."); - QStringList extensions; - extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; + foreach (const QString& fileExtension, getFileExtensions()) { + const QString filePath(kFilePathPrefix + fileExtension); + ASSERT_TRUE(SoundSourceProxy::isFilenameSupported(filePath)); - foreach (const QString& extension, extensions) { - QString filePath = kFilePath + extension; + Mixxx::AudioSourcePointer pContReadSource( + openAudioSource(filePath)); + ASSERT_FALSE(pContReadSource.isNull()); + const Mixxx::AudioSource::size_type readSampleCount = pContReadSource->frames2samples(kReadFrameCount); + std::vector contReadData(readSampleCount); + std::vector seekReadData(readSampleCount); - for (unsigned int seekFrameIndex = 0; ; seekFrameIndex += kSeekFrameIndex) { - qDebug() << "seekFrameIndex =" << seekFrameIndex; + for (Mixxx::AudioSource::diff_type contFrameIndex = 0; + pContReadSource->isValidFrameIndex(contFrameIndex); + contFrameIndex += kReadFrameCount) { - Mixxx::AudioSourcePointer pAudioSource1( - openAudioSource(filePath)); - EXPECT_FALSE(pAudioSource1.isNull()); - if ((seekFrameIndex + kTestFrameCount) > pAudioSource1->getFrameIndexMax()) { - break; // finished - } - const unsigned int sampleCount1 = pAudioSource1->frames2samples(kTestFrameCount); - CSAMPLE *pData1 = new CSAMPLE[sampleCount1]; - unsigned int frameIndex1 = 0; - while (frameIndex1 < seekFrameIndex) { - unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); - EXPECT_EQ(kTestFrameCount, readCount1); - frameIndex1 += readCount1; - } - EXPECT_EQ(seekFrameIndex, frameIndex1); - const unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); - EXPECT_EQ(kTestFrameCount, readCount1); + const Mixxx::AudioSource::size_type contReadFrameCount = + pContReadSource->readSampleFrames(kReadFrameCount, &contReadData[0]); - Mixxx::AudioSourcePointer pAudioSource2( + Mixxx::AudioSourcePointer pSeekReadSource( openAudioSource(filePath)); - EXPECT_FALSE(pAudioSource2.isNull()); - if ((seekFrameIndex + kTestFrameCount) > pAudioSource2->getFrameIndexMax()) { - break; // finished - } - const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); - CSAMPLE *pData2 = new CSAMPLE[sampleCount2]; - unsigned int frameIndex2 = pAudioSource2->seekSampleFrame(seekFrameIndex); - EXPECT_EQ(seekFrameIndex, frameIndex2); - const unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); - EXPECT_EQ(kTestFrameCount, readCount2); - - for (unsigned int i = 0; i < kTestFrameCount; i++) { - if (pData1[i] != pData2[i]) { - qDebug() << filePath; - qDebug() << "seekFrameIndex =" << seekFrameIndex; - qDebug() << "readFrameIndex =" << (seekFrameIndex + i); + ASSERT_FALSE(pSeekReadSource.isNull()); + ASSERT_EQ(pContReadSource->getChannelCount(), pSeekReadSource->getChannelCount()); + ASSERT_EQ(pContReadSource->getFrameCount(), pSeekReadSource->getFrameCount()); + + const Mixxx::AudioSource::diff_type seekFrameIndex = + pSeekReadSource->seekSampleFrame(contFrameIndex); + ASSERT_EQ(contFrameIndex, seekFrameIndex); + + const Mixxx::AudioSource::size_type seekReadFrameCount = + pSeekReadSource->readSampleFrames(kReadFrameCount, &seekReadData[0]); + + ASSERT_EQ(contReadFrameCount, seekReadFrameCount); + const Mixxx::AudioSource::size_type readSampleCount = + pContReadSource->frames2samples(contReadFrameCount); + for (Mixxx::AudioSource::size_type readSampleOffset = 0; + readSampleOffset < readSampleCount; + ++readSampleOffset) { + if ("opus" == fileExtension) { + EXPECT_NEAR(contReadData[readSampleOffset], seekReadData[readSampleOffset], kOpusSeekDecodingError) + << "Mismatch in " << filePath.toStdString() + << " at seek frame index " << seekFrameIndex + << " for read sample offset " << readSampleOffset; + } else { + // NOTE(uklotzde): The comparison EXPECT_EQ might be + // replaced with EXPECT_FLOAT_EQ to guarantee almost + // accurate seeking. Currently EXPECT_EQ works for all + // tested file formats except Opus. + EXPECT_EQ(contReadData[readSampleOffset], seekReadData[readSampleOffset]) + << "Mismatch in " << filePath.toStdString() + << " at seek frame index " << seekFrameIndex + << " for read sample offset " << readSampleOffset; } - EXPECT_EQ(pData1[i], pData2[i]); } - - delete[] pData1; - delete[] pData2; } } From 786e715ebbdf718de2d09958990ec56eb8b827e8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 22 Feb 2015 18:40:22 +0100 Subject: [PATCH 190/481] Add unit tests for decoding of M4A files --- src/test/soundproxy_test.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 3a3c3eee9d7..f066974a082 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -1,14 +1,13 @@ -#include -#include - -#include - #include "test/mixxxtest.h" #include "soundsourceproxy.h" #include "metadata/trackmetadata.h" -class SoundSourceProxyTest : public MixxxTest { +#include + +#include + +class SoundSourceProxyTest: public MixxxTest { protected: static QStringList getFileExtensions() { QStringList extensions; @@ -16,9 +15,16 @@ class SoundSourceProxyTest : public MixxxTest { #ifdef __OPUS__ extensions << "opus"; #endif + if (SoundSourceProxy::isFilenameSupported("filename.m4a")) { + extensions << "m4a"; + } return extensions; } + static void SetUpTestCase() { + SoundSourceProxy::loadPlugins(); + } + static Mixxx::SoundSourcePointer openSoundSource(const QString& fileName) { return SoundSourceProxy(fileName, SecurityTokenPointer()).getSoundSource(); } From 3a8f4265dd2b68be65ac741a9ad22e70ca3ef140 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 22 Feb 2015 18:41:38 +0100 Subject: [PATCH 191/481] Fix decoding of M4A files by adding temporary buffering --- plugins/soundsourcem4a/audiosourcem4a.cpp | 128 ++++++++++++++++------ plugins/soundsourcem4a/audiosourcem4a.h | 4 +- 2 files changed, 100 insertions(+), 32 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 41345802d3f..24f5ee0d433 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -1,5 +1,7 @@ #include "audiosourcem4a.h" +#include "sampleutil.h" + #ifdef __WINDOWS__ #include #include @@ -22,8 +24,9 @@ namespace Mixxx { namespace { -// AAC: "... each block decodes to 1024 time-domain samples." -const AudioSource::size_type kFramesPerSampleBlock = 1024; +// According to the specification each AAC sample block decodes +// to either 1024 or 960 sample frames. +const AudioSource::size_type kMaxFramesPerSampleBlock = 1024; // MP4SampleId is 1-based const MP4SampleId kSampleBlockIdMin = 1; @@ -83,6 +86,8 @@ AudioSourceM4A::AudioSourceM4A(QUrl url) m_inputBufferOffset(0), m_inputBufferLength(0), m_hDecoder(NULL), + m_decodeSampleBufferReadOffset(0), + m_decodeSampleBufferWriteOffset(0), m_curFrameIndex(kFrameIndexMin) { } @@ -153,8 +158,7 @@ Result AudioSourceM4A::postConstruct() { SAMPLERATE_TYPE sampleRate; unsigned char channelCount; - if (0 - > NeAACDecInit2(m_hDecoder, configBuffer, configBufferSize, + if (0 > NeAACDecInit2(m_hDecoder, configBuffer, configBufferSize, &sampleRate, &channelCount)) { free(configBuffer); qWarning() << "Failed to initialize the AAC decoder!"; @@ -165,14 +169,10 @@ Result AudioSourceM4A::postConstruct() { setChannelCount(channelCount); setFrameRate(sampleRate); - setFrameCount(m_maxSampleBlockId * kFramesPerSampleBlock); + setFrameCount(m_maxSampleBlockId * kMaxFramesPerSampleBlock); - // Allocate one block more than the number of sample blocks - // that are prefetched - const SampleBuffer::size_type prefetchSampleBufferSize = - (kNumberOfPrefetchSampleBlocks + 1) - * frames2samples(kFramesPerSampleBlock); - m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); + // Resize temporary buffer for decoded sample data + m_decodeSampleBuffer.resize(frames2samples(kMaxFramesPerSampleBlock)); // Invalidate current position to enforce the following // seek operation @@ -206,10 +206,14 @@ void AudioSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { NeAACDecPostSeekReset(m_hDecoder, sampleBlockId); m_curSampleBlockId = sampleBlockId; m_curFrameIndex = kFrameIndexMin + - (m_curSampleBlockId - kSampleBlockIdMin) * kFramesPerSampleBlock; + (m_curSampleBlockId - kSampleBlockIdMin) * kMaxFramesPerSampleBlock; // discard input buffer m_inputBufferOffset = 0; m_inputBufferLength = 0; + // discard previously decoded sample data + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + } AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { @@ -217,7 +221,7 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { if (m_curFrameIndex != frameIndex) { MP4SampleId sampleBlockId = kSampleBlockIdMin - + (frameIndex / kFramesPerSampleBlock); + + (frameIndex / kMaxFramesPerSampleBlock); DEBUG_ASSERT(isValidSampleBlockId(sampleBlockId)); if ((frameIndex < m_curFrameIndex) || // seeking backwards? !isValidSampleBlockId(m_curSampleBlockId) || // invalid seek position? @@ -238,11 +242,10 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { } // decoding starts before the actual target position DEBUG_ASSERT(m_curFrameIndex <= frameIndex); - const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; // prefetch (decode and discard) all samples up to the target position - DEBUG_ASSERT(frames2samples(prefetchFrameCount) <= m_prefetchSampleBuffer.size()); + const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; const size_type skipFrameCount = - readSampleFrames(prefetchFrameCount, &m_prefetchSampleBuffer[0]); + readSampleFrames(prefetchFrameCount, NULL); DEBUG_ASSERT(skipFrameCount <= prefetchFrameCount); if (skipFrameCount != prefetchFrameCount) { qWarning() << "Failed to skip over prefetched sample frames after seeking @" << m_curFrameIndex; @@ -264,6 +267,38 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( sample_type* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { + DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= + m_decodeSampleBufferWriteOffset); + if (m_decodeSampleBufferReadOffset < m_decodeSampleBufferWriteOffset) { + // Consume previously decoded sample data + const size_type numberOfSamplesDecoded = + m_decodeSampleBufferWriteOffset - + m_decodeSampleBufferReadOffset; + const size_type numberOfFramesDecoded = + samples2frames(numberOfSamplesDecoded); + const size_type numberOfFramesRead = + math_min(numberOfFramesRemaining, numberOfFramesDecoded); + const size_type numberOfSamplesRead = + frames2samples(numberOfFramesRead); + if (pSampleBuffer) { + const sample_type* const pDecodeBuffer = + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset]; + SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); + pSampleBuffer += numberOfSamplesRead; + m_decodeSampleBufferReadOffset += numberOfSamplesRead; + } + m_curFrameIndex += numberOfFramesRead; + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + numberOfFramesRemaining -= numberOfFramesRead; + if (0 == numberOfFramesRemaining) { + break; // exit loop + } + // All previously decoded sample data has been consumed + DEBUG_ASSERT(m_decodeSampleBufferReadOffset == m_decodeSampleBufferWriteOffset); + } + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); if (m_inputBufferOffset >= m_inputBufferLength) { // reset input buffer @@ -289,16 +324,35 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( } DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); - // decode samples into sampleBuffer - const size_type decodeBufferCapacityInBytes = frames2samples( - numberOfFramesRemaining) * sizeof(*sampleBuffer); - DEBUG_ASSERT(0 < decodeBufferCapacityInBytes); - void* pDecodeBuffer = pSampleBuffer; // in/out parameter + // NOTE(uklotzde): The sample buffer for NeAACDecDecode2 has to + // be big enough for a whole block of decoded samples, which + // contains up to kMaxFramesPerSampleBlock frames. Otherwise + // we need to use a temporary buffer. + sample_type* pDecodeBuffer; // in/out parameter + size_type decodeBufferCapacityInBytes; + if (pSampleBuffer && (numberOfFramesRemaining >= kMaxFramesPerSampleBlock)) { + // decode samples directly into sampleBuffer + pDecodeBuffer = pSampleBuffer; + decodeBufferCapacityInBytes = frames2samples( + numberOfFramesRemaining) * sizeof(*pSampleBuffer); + } else { + // decode next block of samples into temporary buffer + pDecodeBuffer = &m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset]; + const SampleBuffer::size_type decodeSampleBufferCapacity = + m_decodeSampleBuffer.size() - + m_decodeSampleBufferWriteOffset; + DEBUG_ASSERT(decodeSampleBufferCapacity >= + frames2samples(kMaxFramesPerSampleBlock)); + decodeBufferCapacityInBytes = + decodeSampleBufferCapacity * + sizeof(*pDecodeBuffer); + } NeAACDecFrameInfo decFrameInfo; void* pDecodeResult = NeAACDecDecode2(m_hDecoder, &decFrameInfo, &m_inputBuffer[m_inputBufferOffset], - m_inputBufferLength - m_inputBufferOffset, &pDecodeBuffer, + m_inputBufferLength - m_inputBufferOffset, + reinterpret_cast(&pDecodeBuffer), decodeBufferCapacityInBytes); // verify the decoding result if (0 != decFrameInfo.error) { @@ -308,10 +362,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( << getUrl(); break; // abort } - - // verify our assumptions about the decoding API - DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); // verify the in/out parameter - DEBUG_ASSERT(pSampleBuffer == pDecodeResult); // verify the result pointer + DEBUG_ASSERT(pDecodeResult == pDecodeBuffer); // verify the in/out parameter // verify the decoded sample data for consistency if (getChannelCount() != decFrameInfo.channels) { @@ -331,13 +382,28 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( m_inputBufferOffset += decFrameInfo.bytesconsumed; // consume decoded output data - pSampleBuffer += decFrameInfo.samples; - + const size_type numberOfSamplesDecoded = + decFrameInfo.samples; const size_type numberOfFramesDecoded = - samples2frames(decFrameInfo.samples); - m_curFrameIndex += numberOfFramesDecoded; + samples2frames(numberOfSamplesDecoded); + const size_type numberOfFramesRead = + math_min(numberOfFramesRemaining, numberOfFramesDecoded); + const size_type numberOfSamplesRead = + frames2samples(numberOfFramesRead); + if (pDecodeBuffer == pSampleBuffer) { + pSampleBuffer += numberOfSamplesRead; + } else { + m_decodeSampleBufferWriteOffset += numberOfSamplesDecoded; + if (pSampleBuffer) { + SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); + pSampleBuffer += numberOfSamplesRead; + } + m_decodeSampleBufferReadOffset += numberOfSamplesRead; + } + + m_curFrameIndex += numberOfFramesRead; DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - numberOfFramesRemaining -= numberOfFramesDecoded; + numberOfFramesRemaining -= numberOfFramesRead; } DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 5e10b3f8588..4ae9d33696b 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -57,7 +57,9 @@ class AudioSourceM4A: public AudioSource { NeAACDecHandle m_hDecoder; typedef std::vector SampleBuffer; - SampleBuffer m_prefetchSampleBuffer; + SampleBuffer m_decodeSampleBuffer; + SampleBuffer::size_type m_decodeSampleBufferReadOffset; + SampleBuffer::size_type m_decodeSampleBufferWriteOffset; diff_type m_curFrameIndex; }; From b06598e2a5c99a1d320213ee0fa62c42ddaac65b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 22 Feb 2015 18:42:28 +0100 Subject: [PATCH 192/481] Reduce number of prefetched blocks after seeking for M4A files --- plugins/soundsourcem4a/audiosourcem4a.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 24f5ee0d433..4f2f88f17b1 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -40,10 +40,9 @@ const MP4SampleId kSampleBlockIdMin = 1; // the AAC specification! This theoretical value has to be // confirmed practically by appropriate unit tests. // -// For the time being simply use the current value of 2 which -// seems to be enough as experimental listening tests with some -// M4A example files revealed. -const MP4SampleId kNumberOfPrefetchSampleBlocks = 2; +// For the time being simply use the experimental value of +// 1 bloack which has been confirmed by unit tests. +const MP4SampleId kNumberOfPrefetchSampleBlocks = 1; // Searches for the first audio track in the MP4 file that // suits our needs. From 7be728b6b7fd9a86da19ac9da671244ac6da9344 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 22 Feb 2015 21:19:59 +0100 Subject: [PATCH 193/481] Delete obsolete comment in MP3 decoder --- src/sources/audiosourcemp3.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 6aad1297ec5..c7fd9f74347 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -11,10 +11,6 @@ namespace { // In the worst case up to 29 MP3 frames need to be prefetched // for accurate seeking: // http://www.mars.org/mailman/public/mad-dev/2002-May/000634.html -// -// TODO (XXX): Fix the implementation and determine the minimum -// required value for passing all unit tests. Currently even the -// theoretical maximum of 29 is not sufficient!? const AudioSource::size_type kSeekFramePrefetchCount = 29; const AudioSource::sample_type kMadScale = AudioSource::kSampleValuePeak From daee555ecf8301374f1204b166488bcac409abb0 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 22 Feb 2015 22:19:52 +0100 Subject: [PATCH 194/481] Improve M4A sample block calculations and documentation --- plugins/soundsourcem4a/audiosourcem4a.cpp | 31 +++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 4f2f88f17b1..25f57e7e0ce 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -25,12 +25,24 @@ namespace Mixxx { namespace { // According to the specification each AAC sample block decodes -// to either 1024 or 960 sample frames. -const AudioSource::size_type kMaxFramesPerSampleBlock = 1024; +// to 1024 sample frames. The rarely used AAC-960 profile with +// a block size of 960 frames is not supported. +const AudioSource::size_type kFramesPerSampleBlock = 1024; // MP4SampleId is 1-based const MP4SampleId kSampleBlockIdMin = 1; +inline AudioSource::diff_type getFrameIndexForSampleBlockId( + MP4SampleId sampleBlockId) { + return AudioSource::kFrameIndexMin + + (sampleBlockId - kSampleBlockIdMin) * kFramesPerSampleBlock; +} + +inline AudioSource::size_type getFrameCountForSampleBlockId( + MP4SampleId sampleBlockId) { + return ((sampleBlockId - kSampleBlockIdMin) + 1) * kFramesPerSampleBlock; +} + // Decoding will be restarted one or more blocks of samples // before the actual position after seeking randomly in the // audio stream to avoid audible glitches. @@ -168,10 +180,10 @@ Result AudioSourceM4A::postConstruct() { setChannelCount(channelCount); setFrameRate(sampleRate); - setFrameCount(m_maxSampleBlockId * kMaxFramesPerSampleBlock); + setFrameCount(getFrameCountForSampleBlockId(m_maxSampleBlockId)); // Resize temporary buffer for decoded sample data - m_decodeSampleBuffer.resize(frames2samples(kMaxFramesPerSampleBlock)); + m_decodeSampleBuffer.resize(frames2samples(kFramesPerSampleBlock)); // Invalidate current position to enforce the following // seek operation @@ -204,8 +216,7 @@ void AudioSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { NeAACDecPostSeekReset(m_hDecoder, sampleBlockId); m_curSampleBlockId = sampleBlockId; - m_curFrameIndex = kFrameIndexMin + - (m_curSampleBlockId - kSampleBlockIdMin) * kMaxFramesPerSampleBlock; + m_curFrameIndex = getFrameIndexForSampleBlockId(m_curSampleBlockId); // discard input buffer m_inputBufferOffset = 0; m_inputBufferLength = 0; @@ -220,7 +231,7 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { if (m_curFrameIndex != frameIndex) { MP4SampleId sampleBlockId = kSampleBlockIdMin - + (frameIndex / kMaxFramesPerSampleBlock); + + (frameIndex / kFramesPerSampleBlock); DEBUG_ASSERT(isValidSampleBlockId(sampleBlockId)); if ((frameIndex < m_curFrameIndex) || // seeking backwards? !isValidSampleBlockId(m_curSampleBlockId) || // invalid seek position? @@ -325,11 +336,11 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( // NOTE(uklotzde): The sample buffer for NeAACDecDecode2 has to // be big enough for a whole block of decoded samples, which - // contains up to kMaxFramesPerSampleBlock frames. Otherwise + // contains up to kFramesPerSampleBlock frames. Otherwise // we need to use a temporary buffer. sample_type* pDecodeBuffer; // in/out parameter size_type decodeBufferCapacityInBytes; - if (pSampleBuffer && (numberOfFramesRemaining >= kMaxFramesPerSampleBlock)) { + if (pSampleBuffer && (numberOfFramesRemaining >= kFramesPerSampleBlock)) { // decode samples directly into sampleBuffer pDecodeBuffer = pSampleBuffer; decodeBufferCapacityInBytes = frames2samples( @@ -341,7 +352,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( m_decodeSampleBuffer.size() - m_decodeSampleBufferWriteOffset; DEBUG_ASSERT(decodeSampleBufferCapacity >= - frames2samples(kMaxFramesPerSampleBlock)); + frames2samples(kFramesPerSampleBlock)); decodeBufferCapacityInBytes = decodeSampleBufferCapacity * sizeof(*pDecodeBuffer); From 8414cb59503ca2e6c7fd353a5d4b81ff069f362a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 22 Feb 2015 22:22:11 +0100 Subject: [PATCH 195/481] "The 2112 Sample Assumption": Increase prefetch block count for M4A --- plugins/soundsourcem4a/audiosourcem4a.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 25f57e7e0ce..4d6fb14cf09 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -47,14 +47,18 @@ inline AudioSource::size_type getFrameCountForSampleBlockId( // before the actual position after seeking randomly in the // audio stream to avoid audible glitches. // -// TODO(XXX): Replace with the smallest possible value that -// allows accurate seeking in any audio stream compliant with -// the AAC specification! This theoretical value has to be -// confirmed practically by appropriate unit tests. +// "AAC Audio - Encoder Delay and Synchronization: The 2112 Sample Assumption" +// https://developer.apple.com/library/ios/technotes/tn2258/_index.html +// "It must also be assumed that without an explicit value, the playback +// system will trim 2112 samples from the AAC decoder output when starting +// playback from any point in the bistream." // -// For the time being simply use the experimental value of -// 1 bloack which has been confirmed by unit tests. -const MP4SampleId kNumberOfPrefetchSampleBlocks = 1; +// 2112 frames = 2 * 1024 + 64 < 3 * 1024 = 3 sample blocks +// +// Decoding 3 blocks of samples in advance after seeking should +// compensate for the encoding delay and allow sample accurate +// seeking. +const MP4SampleId kNumberOfPrefetchSampleBlocks = 3; // Searches for the first audio track in the MP4 file that // suits our needs. From 85c4716dd7a2a525b794208ba697e682f36bf7c5 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 22 Feb 2015 22:33:44 +0100 Subject: [PATCH 196/481] Fix return type in AudioTagger --- src/metadata/audiotagger.cpp | 4 ++-- src/metadata/audiotagger.h | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/metadata/audiotagger.cpp b/src/metadata/audiotagger.cpp index f4b7d8bee5c..53087af9a64 100644 --- a/src/metadata/audiotagger.cpp +++ b/src/metadata/audiotagger.cpp @@ -23,6 +23,6 @@ AudioTagger::AudioTagger(const QString& file, SecurityTokenPointer pToken) : AudioTagger::~AudioTagger() { } -bool AudioTagger::save(const Mixxx::TrackMetadata& trackMetadata) { - return OK == writeTrackMetadataIntoFile(trackMetadata, m_file.canonicalFilePath()); +Result AudioTagger::save(const Mixxx::TrackMetadata& trackMetadata) { + return writeTrackMetadataIntoFile(trackMetadata, m_file.canonicalFilePath()); } diff --git a/src/metadata/audiotagger.h b/src/metadata/audiotagger.h index a26af068bc1..01ac2b01325 100644 --- a/src/metadata/audiotagger.h +++ b/src/metadata/audiotagger.h @@ -3,6 +3,7 @@ #include "metadata/trackmetadata.h" #include "util/sandbox.h" +#include "util/defs.h" // Result #include @@ -11,7 +12,7 @@ class AudioTagger { AudioTagger(const QString& file, SecurityTokenPointer pToken); virtual ~AudioTagger(); - bool save(const Mixxx::TrackMetadata& trackMetadata); + Result save(const Mixxx::TrackMetadata& trackMetadata); private: QFileInfo m_file; From 947b3383da1e4302548ca238a04344eb952bb6a3 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 23 Feb 2015 18:35:36 +0100 Subject: [PATCH 197/481] Memory-aligned SampleBuffer to utilize SSE optimizations --- build/depends.py | 2 + src/samplebuffer.cpp | 21 +++++++++++ src/samplebuffer.h | 90 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 src/samplebuffer.cpp create mode 100644 src/samplebuffer.h diff --git a/build/depends.py b/build/depends.py index 23ab76c22de..1aba0183168 100644 --- a/build/depends.py +++ b/build/depends.py @@ -869,6 +869,8 @@ def sources(self, build): "skin/pixmapsource.cpp", "sampleutil.cpp", + "samplebuffer.cpp", + "trackinfoobject.cpp", "track/beatgrid.cpp", "track/beatmap.cpp", diff --git a/src/samplebuffer.cpp b/src/samplebuffer.cpp new file mode 100644 index 00000000000..74855366722 --- /dev/null +++ b/src/samplebuffer.cpp @@ -0,0 +1,21 @@ +#include "samplebuffer.h" + +#include "sampleutil.h" + + +SampleBuffer::SampleBuffer(size_type size) + : m_data(SampleUtil::alloc(size)), + m_size(m_data ? size : 0) { +} + +SampleBuffer::~SampleBuffer() { + SampleUtil::free(m_data); +} + +void SampleBuffer::clear() { + SampleUtil::clear(m_data, m_size); +} + +void SampleBuffer::fill(value_type value) { + SampleUtil::fill(m_data, value, m_size); +} diff --git a/src/samplebuffer.h b/src/samplebuffer.h new file mode 100644 index 00000000000..a511f4eb363 --- /dev/null +++ b/src/samplebuffer.h @@ -0,0 +1,90 @@ +#ifndef SAMPLEBUFFER_H +#define SAMPLEBUFFER_H + +#include "util/types.h" // CSAMPLE + +#include // std::swap + +// A sample buffer with properly aligned memory to enable SSE optimizations. +// +// No resize operation is provided intentionally for maximum efficiency! +// If the size of an existing sample buffer needs to be altered after +// construction this can simply be achieved by swapping the contents with +// a temporary sample buffer that has been constructed with the appropriate +// size: +// +// SampleBuffer sampleBuffer(oldSize); +// ... +// SampleBuffer(newSize).swap(sampleBuffer); +// // Now the data in sampleBuffer is uninitialized and +// // sampleBuffer.size() == newSize +// +// No sample data is copied when swapping the contents of two samples buffers +// for resizing! +class SampleBuffer { + Q_DISABLE_COPY(SampleBuffer); + + public: + typedef size_t size_type; + typedef CSAMPLE value_type; + + // random access iterators + typedef value_type* iterator; + typedef const value_type* const_iterator; + + SampleBuffer(): + m_data(NULL), + m_size(0) { + } + explicit SampleBuffer(size_type size); + virtual ~SampleBuffer(); + + void swap(SampleBuffer& other) { + std::swap(m_data, other.m_data); + std::swap(m_size, other.m_size); + } + + size_type size() const { + return m_size; + } + + value_type* data() { + return m_data; + } + const value_type* data() const { + return m_data; + } + + value_type& operator[](size_type index) { + return m_data[index]; + } + value_type operator[](size_type index) const { + return m_data[index]; + } + + iterator begin() { + return m_data; + } + const_iterator begin() const { + return m_data; + } + + iterator end() { + return m_data + m_size; + } + const_iterator end() const { + return m_data + m_size; + } + + // Fill the whole buffer with zeroes + void clear(); + + // Fill the whole buffer with the same value + void fill(value_type value); + + private: + value_type* m_data; + size_type m_size; +}; + +#endif /* SAMPLEBUFFER_H */ From 63558f4ef3c5ab18a579effd64c2573d5cc38a20 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 23 Feb 2015 23:08:50 +0100 Subject: [PATCH 198/481] Use SampleBuffer instead of std::vector for decoding sample data --- plugins/soundsourcem4a/SConscript | 2 ++ plugins/soundsourcem4a/audiosourcem4a.cpp | 4 +++- plugins/soundsourcem4a/audiosourcem4a.h | 2 +- src/sources/audiosource.cpp | 6 +++--- src/sources/audiosourceflac.cpp | 4 +++- src/sources/audiosourceflac.h | 6 ++---- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index f4a49fce288..bc2625d0ef4 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -16,6 +16,8 @@ m4a_sources = [ "sources/soundsource.cpp", "sources/soundsourceplugin.cpp", "sources/audiosource.cpp", + "samplebuffer.cpp", + "sampleutil.cpp", "metadata/trackmetadata.cpp", "metadata/trackmetadatataglib.cpp" ] diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 4d6fb14cf09..f02b020e7b0 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -187,7 +187,9 @@ Result AudioSourceM4A::postConstruct() { setFrameCount(getFrameCountForSampleBlockId(m_maxSampleBlockId)); // Resize temporary buffer for decoded sample data - m_decodeSampleBuffer.resize(frames2samples(kFramesPerSampleBlock)); + const SampleBuffer::size_type decodeSampleBufferSize = + frames2samples(kFramesPerSampleBlock); + SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); // Invalidate current position to enforce the following // seek operation diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 4ae9d33696b..efc243d887b 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -2,6 +2,7 @@ #define AUDIOSOURCEM4A_H #include "sources/audiosource.h" +#include "samplebuffer.h" #ifdef __MP4V2__ #include @@ -56,7 +57,6 @@ class AudioSourceM4A: public AudioSource { NeAACDecHandle m_hDecoder; - typedef std::vector SampleBuffer; SampleBuffer m_decodeSampleBuffer; SampleBuffer::size_type m_decodeSampleBufferReadOffset; SampleBuffer::size_type m_decodeSampleBufferWriteOffset; diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index eb2ad5c3d14..b543a3d476b 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -1,5 +1,6 @@ #include "sources/audiosource.h" +#include "samplebuffer.h" #include "sampleutil.h" #include @@ -85,11 +86,10 @@ AudioSource::size_type AudioSource::readSampleFramesStereo( << numberOfSamplesToRead << "for reading stereo samples." << "The size of the provided sample buffer is" << sampleBufferSize; - typedef std::vector SampleBuffer; SampleBuffer tempBuffer(numberOfSamplesToRead); const AudioSource::size_type readFrameCount = readSampleFrames( - numberOfFrames, &tempBuffer[0]); - SampleUtil::copyMultiToStereo(sampleBuffer, &tempBuffer[0], + numberOfFrames, tempBuffer.data()); + SampleUtil::copyMultiToStereo(sampleBuffer, tempBuffer.data(), readFrameCount, getChannelCount()); return readFrameCount; } diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index db72111d669..0e505445f3e 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -397,7 +397,9 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { m_maxFramesize = metadata->data.stream_info.max_framesize; m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; - m_decodeSampleBuffer.resize(m_maxBlocksize * getChannelCount()); + const SampleBuffer::size_type decodeSampleBufferSize = + m_maxBlocksize * getChannelCount(); + SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); break; } default: diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 79de20be369..cc7a459d4b5 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -2,13 +2,12 @@ #define AUDIOSOURCEFLAC_H #include "sources/audiosource.h" +#include "samplebuffer.h" #include #include -#include - namespace Mixxx { class AudioSourceFLAC: public AudioSource { @@ -62,8 +61,7 @@ class AudioSourceFLAC: public AudioSource { sample_type m_sampleScale; - typedef std::vector SampleBuffer; - std::vector m_decodeSampleBuffer; + SampleBuffer m_decodeSampleBuffer; SampleBuffer::size_type m_decodeSampleBufferReadOffset; SampleBuffer::size_type m_decodeSampleBufferWriteOffset; From d969cb78206ecbe345356daac3b590f9cc5b9b8b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 24 Feb 2015 01:05:45 +0100 Subject: [PATCH 199/481] Fix TagLib version dependencies --- src/metadata/trackmetadatataglib.cpp | 70 +++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 6a7ec88f707..3a3835331b6 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -10,6 +10,14 @@ #define TAGLIB_HAS_WAV_ID3V2TAG \ (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) +// TagLib has full support for MP4 atom types since version 1.8 +#define TAGLIB_HAS_MP4_ATOM_TYPES \ + (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 8)) + +// TagLib has support for has() style functions since version 1.9 +#define TAGLIB_HAS_TAG_CHECK \ + (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) + #include #include #include @@ -42,6 +50,46 @@ const QString kFileTypeOggOpus("opus"); const QString kFileTypeWAV("wav"); const QString kFileTypeWavPack("wv"); +inline bool hasID3v2Tag(const TagLib::MPEG::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasID3v2Tag(); +#else + return NULL != file.ID3v2Tag(); +#endif +} + +inline bool hasAPETag(const TagLib::MPEG::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasAPETag(); +#else + return NULL != file.APETag(); +#endif +} + +inline bool hasID3v2Tag(const TagLib::FLAC::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasID3v2Tag(); +#else + return NULL != file.ID3v2Tag(); +#endif +} + +inline bool hasXiphComment(const TagLib::FLAC::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasXiphComment(); +#else + return NULL != file.xiphComment(); +#endif +} + +inline bool hasAPETag(const TagLib::WavPack::File& file) { +#if TAGLIB_HAS_TAG_CHECK + return file.hasAPETag(); +#else + return NULL != file.APETag(); +#endif +} + // Deduce the file type from the file name QString getFileTypeFromFileName(QString fileName) { const QString fileType(fileName.section(".", -1).toLower().trimmed()); @@ -639,9 +687,13 @@ void readTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP if (getItemListMap(tag).contains("tmpo")) { // Read the BPM as an integer value. const TagLib::MP4::Item& item = getItemListMap(tag)["tmpo"]; +#if TAGLIB_HAS_MP4_ATOM_TYPES if (item.atomDataType() == TagLib::MP4::TypeInteger) { pTrackMetadata->setBpm(getItemListMap(tag)["tmpo"].toInt()); } +#else + pTrackMetadata->setBpm(getItemListMap(tag)["tmpo"].toInt()); +#endif } if (getItemListMap(tag).contains("----:com.apple.iTunes:BPM")) { // This is the preferred field for storing the BPM @@ -843,13 +895,15 @@ Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImag if (kFileTypeMP3 == fileType) { TagLib::MPEG::File file(fileName.toLocal8Bit().constData()); if (readAudioProperties(pTrackMetadata, file)) { - const TagLib::ID3v2::Tag* pID3v2Tag = file.ID3v2Tag(); + const TagLib::ID3v2::Tag* pID3v2Tag = + hasID3v2Tag(file) ? file.ID3v2Tag() : NULL; if (pID3v2Tag) { readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); return OK; } else { - const TagLib::APE::Tag* pAPETag = file.APETag(); + const TagLib::APE::Tag* pAPETag = + hasAPETag(file) ? file.APETag() : NULL; if (pAPETag) { readTrackMetadataFromAPETag(pTrackMetadata, *pAPETag); readCoverArtFromAPETag(pCoverArt, *pAPETag); @@ -897,14 +951,14 @@ Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImag } if (readAudioProperties(pTrackMetadata, file)) { const TagLib::Ogg::XiphComment* pXiphComment = - file.hasXiphComment() ? file.xiphComment() : NULL; + hasXiphComment(file) ? file.xiphComment() : NULL; if (pXiphComment) { readTrackMetadataFromXiphComment(pTrackMetadata, *pXiphComment); readCoverArtFromXiphComment(pCoverArt, *pXiphComment); return OK; } else { const TagLib::ID3v2::Tag* pID3v2Tag = - file.hasID3v2Tag() ? file.ID3v2Tag() : NULL; + hasID3v2Tag(file) ? file.ID3v2Tag() : NULL; if (pID3v2Tag) { readTrackMetadataFromID3v2Tag(pTrackMetadata, *pID3v2Tag); readCoverArtFromID3v2Tag(pCoverArt, *pID3v2Tag); @@ -958,7 +1012,8 @@ Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImag } else if (kFileTypeWavPack == fileType) { TagLib::WavPack::File file(fileName.toLocal8Bit().constData()); if (readAudioProperties(pTrackMetadata, file)) { - const TagLib::APE::Tag* pAPETag = file.APETag(); + const TagLib::APE::Tag* pAPETag = + hasAPETag(file) ? file.APETag() : NULL; if (pAPETag) { readTrackMetadataFromAPETag(pTrackMetadata, *pAPETag); readCoverArtFromAPETag(pCoverArt, *pAPETag); @@ -976,7 +1031,8 @@ Result readTrackMetadataAndCoverArtFromFile(TrackMetadata* pTrackMetadata, QImag TagLib::RIFF::WAV::File file(fileName.toLocal8Bit().constData()); if (readAudioProperties(pTrackMetadata, file)) { #if TAGLIB_HAS_WAV_ID3V2TAG - const TagLib::ID3v2::Tag* pID3v2Tag = file.hasID3v2Tag() ? file.ID3v2Tag() : NULL; + const TagLib::ID3v2::Tag* pID3v2Tag = + file.hasID3v2Tag() ? file.ID3v2Tag() : NULL; #else const TagLib::ID3v2::Tag* pID3v2Tag = file.tag(); #endif @@ -1077,7 +1133,7 @@ Result writeTrackMetadataIntoFile(const TrackMetadata& trackMetadata, QString fi #if TAGLIB_HAS_WAV_ID3V2TAG writeTrackMetadataIntoID3v2Tag(pWAVFile->ID3v2Tag(), trackMetadata); #else - writeTrackMetadataIntoID3v2Tag(pWavFile->tag(), trackMetadata); + writeTrackMetadataIntoID3v2Tag(pWAVFile->tag(), trackMetadata); #endif pFile.reset(pWAVFile.take()); // transfer ownership } else if (kFileTypeAIFF == fileType) { From aadfc64158a63d055b2d3d5ef0bcd357eca6ca97 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 24 Feb 2015 12:23:24 +0100 Subject: [PATCH 200/481] Move utility functions for SampleBuffer into separate file --- src/samplebuffer.cpp | 8 -------- src/samplebuffer.h | 8 +------- src/samplebufferutil.h | 17 +++++++++++++++++ 3 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 src/samplebufferutil.h diff --git a/src/samplebuffer.cpp b/src/samplebuffer.cpp index 74855366722..91908ffe7db 100644 --- a/src/samplebuffer.cpp +++ b/src/samplebuffer.cpp @@ -11,11 +11,3 @@ SampleBuffer::SampleBuffer(size_type size) SampleBuffer::~SampleBuffer() { SampleUtil::free(m_data); } - -void SampleBuffer::clear() { - SampleUtil::clear(m_data, m_size); -} - -void SampleBuffer::fill(value_type value) { - SampleUtil::fill(m_data, value, m_size); -} diff --git a/src/samplebuffer.h b/src/samplebuffer.h index a511f4eb363..3aeedb86e8c 100644 --- a/src/samplebuffer.h +++ b/src/samplebuffer.h @@ -76,15 +76,9 @@ class SampleBuffer { return m_data + m_size; } - // Fill the whole buffer with zeroes - void clear(); - - // Fill the whole buffer with the same value - void fill(value_type value); - private: value_type* m_data; size_type m_size; }; -#endif /* SAMPLEBUFFER_H */ +#endif // SAMPLEBUFFER_H diff --git a/src/samplebufferutil.h b/src/samplebufferutil.h new file mode 100644 index 00000000000..091ac3fe064 --- /dev/null +++ b/src/samplebufferutil.h @@ -0,0 +1,17 @@ +#ifndef SAMPLEBUFFERUTIL_H +#define SAMPLEBUFFERUTIL_H + +#include "samplebuffer.h" +#include "sampleutil.h" + +// Fills the whole buffer with zeroes +inline void clear(SampleBuffer* pBuffer) { + SampleUtil::clear(pBuffer->data(), pBuffer->size()); +} + +// Fills the whole buffer with the same value +inline void fill(SampleBuffer* pBuffer, SampleBuffer::value_type value) { + SampleUtil::fill(pBuffer->data(), value, pBuffer->size()); +} + +#endif // SAMPLEBUFFERUTIL_H From a7ffef5c881815106e0ce03444331aace502b4df Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 24 Feb 2015 10:18:48 +0100 Subject: [PATCH 201/481] Align SampleBuffer with std::vector --- src/samplebuffer.h | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/samplebuffer.h b/src/samplebuffer.h index 3aeedb86e8c..cc19b963a42 100644 --- a/src/samplebuffer.h +++ b/src/samplebuffer.h @@ -6,21 +6,28 @@ #include // std::swap // A sample buffer with properly aligned memory to enable SSE optimizations. +// The public interface closely follows that of std::vector. // // No resize operation is provided intentionally for maximum efficiency! // If the size of an existing sample buffer needs to be altered after // construction this can simply be achieved by swapping the contents with -// a temporary sample buffer that has been constructed with the appropriate +// a temporary sample buffer that has been constructed with the desired // size: // // SampleBuffer sampleBuffer(oldSize); // ... +// SampleBuffer tempBuffer(newSize) +// ... copy data from sampleBuffer to tempBuffer... +// sampleBuffer.swap(sampleBuffer); +// +// The local variable tempBuffer can be omitted if no data needs to be +// copied from the existing sampleBuffer: +// +// SampleBuffer sampleBuffer(oldSize); +// ... // SampleBuffer(newSize).swap(sampleBuffer); -// // Now the data in sampleBuffer is uninitialized and -// // sampleBuffer.size() == newSize // -// No sample data is copied when swapping the contents of two samples buffers -// for resizing! +// After construction the content of the buffer is uninitialized. class SampleBuffer { Q_DISABLE_COPY(SampleBuffer); @@ -28,9 +35,15 @@ class SampleBuffer { typedef size_t size_type; typedef CSAMPLE value_type; + typedef value_type& reference; + typedef const value_type& const_reference; + + typedef value_type* pointer; + typedef const value_type* const_pointer; + // random access iterators - typedef value_type* iterator; - typedef const value_type* const_iterator; + typedef pointer iterator; + typedef const_pointer const_iterator; SampleBuffer(): m_data(NULL), @@ -48,17 +61,17 @@ class SampleBuffer { return m_size; } - value_type* data() { + pointer data() { return m_data; } - const value_type* data() const { + const_pointer data() const { return m_data; } - value_type& operator[](size_type index) { + reference operator[](size_type index) { return m_data[index]; } - value_type operator[](size_type index) const { + const_reference operator[](size_type index) const { return m_data[index]; } @@ -77,7 +90,7 @@ class SampleBuffer { } private: - value_type* m_data; + pointer m_data; size_type m_size; }; From e7680cfd4057511f4d337ae6fe00135ca536cb55 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 24 Feb 2015 15:33:26 +0100 Subject: [PATCH 202/481] Fix TagLib version dependencies - 2nd trial --- src/metadata/trackmetadatataglib.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 3a3835331b6..57f82e854c8 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -50,7 +50,7 @@ const QString kFileTypeOggOpus("opus"); const QString kFileTypeWAV("wav"); const QString kFileTypeWavPack("wv"); -inline bool hasID3v2Tag(const TagLib::MPEG::File& file) { +inline bool hasID3v2Tag(TagLib::MPEG::File& file) { #if TAGLIB_HAS_TAG_CHECK return file.hasID3v2Tag(); #else @@ -58,7 +58,7 @@ inline bool hasID3v2Tag(const TagLib::MPEG::File& file) { #endif } -inline bool hasAPETag(const TagLib::MPEG::File& file) { +inline bool hasAPETag(TagLib::MPEG::File& file) { #if TAGLIB_HAS_TAG_CHECK return file.hasAPETag(); #else @@ -66,7 +66,7 @@ inline bool hasAPETag(const TagLib::MPEG::File& file) { #endif } -inline bool hasID3v2Tag(const TagLib::FLAC::File& file) { +inline bool hasID3v2Tag(TagLib::FLAC::File& file) { #if TAGLIB_HAS_TAG_CHECK return file.hasID3v2Tag(); #else @@ -74,7 +74,7 @@ inline bool hasID3v2Tag(const TagLib::FLAC::File& file) { #endif } -inline bool hasXiphComment(const TagLib::FLAC::File& file) { +inline bool hasXiphComment(TagLib::FLAC::File& file) { #if TAGLIB_HAS_TAG_CHECK return file.hasXiphComment(); #else @@ -82,7 +82,7 @@ inline bool hasXiphComment(const TagLib::FLAC::File& file) { #endif } -inline bool hasAPETag(const TagLib::WavPack::File& file) { +inline bool hasAPETag(TagLib::WavPack::File& file) { #if TAGLIB_HAS_TAG_CHECK return file.hasAPETag(); #else @@ -689,10 +689,10 @@ void readTrackMetadataFromMP4Tag(TrackMetadata* pTrackMetadata, const TagLib::MP const TagLib::MP4::Item& item = getItemListMap(tag)["tmpo"]; #if TAGLIB_HAS_MP4_ATOM_TYPES if (item.atomDataType() == TagLib::MP4::TypeInteger) { - pTrackMetadata->setBpm(getItemListMap(tag)["tmpo"].toInt()); + pTrackMetadata->setBpm(item.toInt()); } #else - pTrackMetadata->setBpm(getItemListMap(tag)["tmpo"].toInt()); + pTrackMetadata->setBpm(item.toInt()); #endif } if (getItemListMap(tag).contains("----:com.apple.iTunes:BPM")) { @@ -1083,12 +1083,12 @@ Result writeTrackMetadataIntoFile(const TrackMetadata& trackMetadata, QString fi QScopedPointer pMPEGFile( new TagLib::MPEG::File(fileName.toLocal8Bit().constData())); bool defaultID3V2 = true; - if (pMPEGFile->hasAPETag()) { + if (hasAPETag(*pMPEGFile)) { writeTrackMetadataIntoAPETag(pMPEGFile->APETag(), trackMetadata); // Only write ID3v2 tag if it already exists defaultID3V2 = false; } - if (defaultID3V2 || pMPEGFile->hasID3v2Tag()) { + if (defaultID3V2 || hasID3v2Tag(*pMPEGFile)) { writeTrackMetadataIntoID3v2Tag(pMPEGFile->ID3v2Tag(defaultID3V2), trackMetadata); } pFile.reset(pMPEGFile.take()); // transfer ownership @@ -1101,12 +1101,12 @@ Result writeTrackMetadataIntoFile(const TrackMetadata& trackMetadata, QString fi QScopedPointer pFLACFile( new TagLib::FLAC::File(fileName.toLocal8Bit().constData())); bool defaultXiphComment = true; - if (pFLACFile->hasID3v2Tag()) { + if (hasID3v2Tag(*pFLACFile)) { writeTrackMetadataIntoID3v2Tag(pFLACFile->ID3v2Tag(), trackMetadata); // Only write Xiph Comment if it already exists defaultXiphComment = false; } - if (defaultXiphComment || pFLACFile->hasXiphComment()) { + if (defaultXiphComment || hasXiphComment(*pFLACFile)) { writeTrackMetadataIntoXiphComment(pFLACFile->xiphComment(defaultXiphComment), trackMetadata); } pFile.reset(pFLACFile.take()); // transfer ownership From 7ae73ec0bf592523792ed954ee4968c2c8595ca0 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 25 Feb 2015 22:41:46 +0100 Subject: [PATCH 203/481] Review: Strip down SampleBuffer code --- src/samplebuffer.h | 36 ++++++------------------------------ 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/src/samplebuffer.h b/src/samplebuffer.h index cc19b963a42..cc6497946d3 100644 --- a/src/samplebuffer.h +++ b/src/samplebuffer.h @@ -32,18 +32,8 @@ class SampleBuffer { Q_DISABLE_COPY(SampleBuffer); public: - typedef size_t size_type; typedef CSAMPLE value_type; - - typedef value_type& reference; - typedef const value_type& const_reference; - - typedef value_type* pointer; - typedef const value_type* const_pointer; - - // random access iterators - typedef pointer iterator; - typedef const_pointer const_iterator; + typedef int size_type; SampleBuffer(): m_data(NULL), @@ -61,36 +51,22 @@ class SampleBuffer { return m_size; } - pointer data() { + value_type* data() { return m_data; } - const_pointer data() const { + const value_type* data() const { return m_data; } - reference operator[](size_type index) { + value_type& operator[](size_type index) { return m_data[index]; } - const_reference operator[](size_type index) const { + const value_type& operator[](size_type index) const { return m_data[index]; } - iterator begin() { - return m_data; - } - const_iterator begin() const { - return m_data; - } - - iterator end() { - return m_data + m_size; - } - const_iterator end() const { - return m_data + m_size; - } - private: - pointer m_data; + value_type* m_data; size_type m_size; }; From 3dd8a05713c6c573afe00ac5eabd2830a9b5ebe2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 25 Feb 2015 22:44:06 +0100 Subject: [PATCH 204/481] Replace std::vector with SampleBuffer --- src/analyserqueue.h | 2 +- src/musicbrainz/chromaprinter.cpp | 4 ++-- src/test/soundproxy_test.cpp | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/analyserqueue.h b/src/analyserqueue.h index c6787f47f2e..0ca1bd89642 100644 --- a/src/analyserqueue.h +++ b/src/analyserqueue.h @@ -5,6 +5,7 @@ #include "analyser.h" #include "trackinfoobject.h" #include "sources/audiosource.h" +#include "samplebuffer.h" #include #include @@ -66,7 +67,6 @@ class AnalyserQueue : public QThread { bool m_exit; QAtomicInt m_aiCheckPriorities; - typedef std::vector SampleBuffer; SampleBuffer m_sampleBuffer; // The processing queue and associated mutex diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 39d0bbc1ccc..87aae346374 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -1,7 +1,7 @@ #include "musicbrainz/chromaprinter.h" #include "soundsourceproxy.h" -#include "sampleutil.h" +#include "samplebufferutil.h" #include @@ -34,7 +34,7 @@ namespace // Allocate a sample buffer with maximum size to avoid the // implicit allocation of a temporary buffer when reducing // the audio signal to stereo. - std::vector sampleBuffer( + SampleBuffer sampleBuffer( math_max(numFrames * kFingerprintChannels, pAudioSource->frames2samples(numFrames))); DEBUG_ASSERT(2 == kFingerprintChannels); // implicit assumption of the next line diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index f066974a082..3848b863552 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -2,6 +2,7 @@ #include "soundsourceproxy.h" #include "metadata/trackmetadata.h" +#include "samplebuffer.h" #include @@ -95,8 +96,8 @@ TEST_F(SoundSourceProxyTest, seekForward) { openAudioSource(filePath)); ASSERT_FALSE(pContReadSource.isNull()); const Mixxx::AudioSource::size_type readSampleCount = pContReadSource->frames2samples(kReadFrameCount); - std::vector contReadData(readSampleCount); - std::vector seekReadData(readSampleCount); + SampleBuffer contReadData(readSampleCount); + SampleBuffer seekReadData(readSampleCount); for (Mixxx::AudioSource::diff_type contFrameIndex = 0; pContReadSource->isValidFrameIndex(contFrameIndex); From 295e4e5b6fa84e054b756d7a58e9ad40a38e44d9 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 25 Feb 2015 22:45:06 +0100 Subject: [PATCH 205/481] Add SampleBuffer to the AudioSource API --- plugins/soundsourcem4a/audiosourcem4a.cpp | 2 +- src/analyserqueue.cpp | 3 +- src/musicbrainz/chromaprinter.cpp | 3 +- src/sources/audiosource.h | 41 ++++++++++++++++++++--- src/sources/audiosourcemp3.cpp | 2 +- src/sources/audiosourcemp3.h | 3 -- 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index f02b020e7b0..d23abe7420b 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -261,7 +261,7 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { // prefetch (decode and discard) all samples up to the target position const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; const size_type skipFrameCount = - readSampleFrames(prefetchFrameCount, NULL); + skipSampleFrames(prefetchFrameCount); DEBUG_ASSERT(skipFrameCount <= prefetchFrameCount); if (skipFrameCount != prefetchFrameCount) { qWarning() << "Failed to skip over prefetched sample frames after seeking @" << m_curFrameIndex; diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 2825d40ae40..27bff31965e 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -182,8 +182,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi const Mixxx::AudioSource::size_type framesRead = pAudioSource->readSampleFramesStereo( kAnalysisFramesPerBlock, - &m_sampleBuffer[0], - m_sampleBuffer.size()); + &m_sampleBuffer); DEBUG_ASSERT(framesRead <= framesToRead); frameIndex += framesRead; DEBUG_ASSERT(pAudioSource->isValidFrameIndex(frameIndex)); diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 87aae346374..1627fa7b163 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -39,8 +39,7 @@ namespace DEBUG_ASSERT(2 == kFingerprintChannels); // implicit assumption of the next line const Mixxx::AudioSource::size_type readFrames = - pAudioSource->readSampleFramesStereo( - numFrames, &sampleBuffer[0], sampleBuffer.size()); + pAudioSource->readSampleFramesStereo(numFrames, &sampleBuffer); if (readFrames != numFrames) { qDebug() << "oh that's embarrassing I couldn't read the track"; return QString(); diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index fdfb56df23a..dfd5992ff17 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -3,8 +3,9 @@ #include "urlresource.h" +#include "samplebuffer.h" + #include "util/assert.h" -#include "util/types.h" // CSAMPLE #include "util/defs.h" // Result #include @@ -36,7 +37,7 @@ class AudioSource: public UrlResource { public: typedef std::size_t size_type; typedef std::ptrdiff_t diff_type; - typedef CSAMPLE sample_type; + typedef SampleBuffer::value_type sample_type; static const size_type kChannelCountZero = 0; static const size_type kChannelCountMono = 1; @@ -174,9 +175,26 @@ class AudioSource: public UrlResource { // might be lower than the requested number of frames when the end // of the audio stream has been reached. The current frame seek // position is moved forward towards the next unread frame. - virtual size_type readSampleFrames(size_type numberOfFrames, + virtual size_type readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) = 0; + inline size_type skipSampleFrames( + size_type numberOfFrames) { + return readSampleFrames(numberOfFrames, static_cast(NULL)); + } + + inline size_type readSampleFrames( + size_type numberOfFrames, + SampleBuffer* pSampleBuffer) { + if (pSampleBuffer) { + DEBUG_ASSERT(frames2samples(numberOfFrames) <= size_type(pSampleBuffer->size())); + return readSampleFrames(numberOfFrames, pSampleBuffer->data()); + } else { + return skipSampleFrames(numberOfFrames); + } + } + // Specialized function for explicitly reading stereo (= 2 channels) // frames from an AudioSource. This is the preferred method in Mixxx // to read a stereo signal. @@ -208,8 +226,21 @@ class AudioSource: public UrlResource { // They may also have reduced space requirements on sampleBuffer, // i.e. only the minimum size is required for an in-place // transformation without temporary allocations. - virtual size_type readSampleFramesStereo(size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize); + virtual size_type readSampleFramesStereo( + size_type numberOfFrames, + sample_type* sampleBuffer, + size_type sampleBufferSize); + + inline size_type readSampleFramesStereo( + size_type numberOfFrames, + SampleBuffer* pSampleBuffer) { + if (pSampleBuffer) { + return readSampleFramesStereo(numberOfFrames, + pSampleBuffer->data(), pSampleBuffer->size()); + } else { + return skipSampleFrames(numberOfFrames); + } + } protected: explicit AudioSource(QUrl url); diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index c7fd9f74347..2a1476b4430 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -399,7 +399,7 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { // Skip (= decode and discard) prefetch data const size_type skipFrameCount = frameIndex - m_curFrameIndex; - skipFrameSamples(skipFrameCount); + skipSampleFrames(skipFrameCount); DEBUG_ASSERT(m_curFrameIndex == frameIndex); DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index ec604a611f5..2ad519b80b7 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -37,9 +37,6 @@ class AudioSourceMp3: public AudioSource { void preDestroy(); - inline size_type skipFrameSamples(size_type numberOfFrames) { - return readSampleFrames(numberOfFrames, NULL); - } size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); From 990edfba068a9f1beaa36050913a247312a33b1b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 26 Feb 2015 12:27:42 +0100 Subject: [PATCH 206/481] Update SampleBuffer --- plugins/soundsourcem4a/audiosourcem4a.cpp | 6 ++-- plugins/soundsourcem4a/audiosourcem4a.h | 4 +-- src/musicbrainz/chromaprinter.cpp | 3 +- src/samplebuffer.cpp | 12 +++++-- src/samplebuffer.h | 38 +++++++++++++---------- src/samplebufferutil.h | 17 ---------- src/sources/audiosource.h | 2 +- src/sources/audiosourceflac.cpp | 4 +-- src/sources/audiosourceflac.h | 4 +-- 9 files changed, 43 insertions(+), 47 deletions(-) delete mode 100644 src/samplebufferutil.h diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index d23abe7420b..6d9bc5efb7b 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -187,7 +187,7 @@ Result AudioSourceM4A::postConstruct() { setFrameCount(getFrameCountForSampleBlockId(m_maxSampleBlockId)); // Resize temporary buffer for decoded sample data - const SampleBuffer::size_type decodeSampleBufferSize = + const size_type decodeSampleBufferSize = frames2samples(kFramesPerSampleBlock); SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); @@ -354,11 +354,11 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( } else { // decode next block of samples into temporary buffer pDecodeBuffer = &m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset]; - const SampleBuffer::size_type decodeSampleBufferCapacity = + const int decodeSampleBufferCapacity = m_decodeSampleBuffer.size() - m_decodeSampleBufferWriteOffset; DEBUG_ASSERT(decodeSampleBufferCapacity >= - frames2samples(kFramesPerSampleBlock)); + int(frames2samples(kFramesPerSampleBlock))); decodeBufferCapacityInBytes = decodeSampleBufferCapacity * sizeof(*pDecodeBuffer); diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index efc243d887b..f767f50f0fa 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -58,8 +58,8 @@ class AudioSourceM4A: public AudioSource { NeAACDecHandle m_hDecoder; SampleBuffer m_decodeSampleBuffer; - SampleBuffer::size_type m_decodeSampleBufferReadOffset; - SampleBuffer::size_type m_decodeSampleBufferWriteOffset; + int m_decodeSampleBufferReadOffset; + int m_decodeSampleBufferWriteOffset; diff_type m_curFrameIndex; }; diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 1627fa7b163..e08873c2468 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -1,7 +1,8 @@ #include "musicbrainz/chromaprinter.h" #include "soundsourceproxy.h" -#include "samplebufferutil.h" +#include "samplebuffer.h" +#include "sampleutil.h" #include diff --git a/src/samplebuffer.cpp b/src/samplebuffer.cpp index 91908ffe7db..edcba5f05fc 100644 --- a/src/samplebuffer.cpp +++ b/src/samplebuffer.cpp @@ -2,8 +2,7 @@ #include "sampleutil.h" - -SampleBuffer::SampleBuffer(size_type size) +SampleBuffer::SampleBuffer(int size) : m_data(SampleUtil::alloc(size)), m_size(m_data ? size : 0) { } @@ -11,3 +10,12 @@ SampleBuffer::SampleBuffer(size_type size) SampleBuffer::~SampleBuffer() { SampleUtil::free(m_data); } + +void SampleBuffer::clear() { + SampleUtil::clear(data(), size()); +} + +// Fills the whole buffer with the same value +void SampleBuffer::fill(CSAMPLE value) { + SampleUtil::fill(data(), value, size()); +} diff --git a/src/samplebuffer.h b/src/samplebuffer.h index cc6497946d3..a07f485ed82 100644 --- a/src/samplebuffer.h +++ b/src/samplebuffer.h @@ -29,17 +29,15 @@ // // After construction the content of the buffer is uninitialized. class SampleBuffer { - Q_DISABLE_COPY(SampleBuffer); + Q_DISABLE_COPY(SampleBuffer) + ; - public: - typedef CSAMPLE value_type; - typedef int size_type; - - SampleBuffer(): - m_data(NULL), - m_size(0) { +public: + SampleBuffer() + : m_data(NULL), + m_size(0) { } - explicit SampleBuffer(size_type size); + explicit SampleBuffer(int size); virtual ~SampleBuffer(); void swap(SampleBuffer& other) { @@ -47,27 +45,33 @@ class SampleBuffer { std::swap(m_size, other.m_size); } - size_type size() const { + int size() const { return m_size; } - value_type* data() { + CSAMPLE* data() { return m_data; } - const value_type* data() const { + const CSAMPLE* data() const { return m_data; } - value_type& operator[](size_type index) { + CSAMPLE& operator[](int index) { return m_data[index]; } - const value_type& operator[](size_type index) const { + const CSAMPLE& operator[](int index) const { return m_data[index]; } - private: - value_type* m_data; - size_type m_size; + // Fills the whole buffer with zeroes + void clear(); + + // Fills the whole buffer with the same value + void fill(CSAMPLE value); + +private: + CSAMPLE* m_data; + int m_size; }; #endif // SAMPLEBUFFER_H diff --git a/src/samplebufferutil.h b/src/samplebufferutil.h deleted file mode 100644 index 091ac3fe064..00000000000 --- a/src/samplebufferutil.h +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef SAMPLEBUFFERUTIL_H -#define SAMPLEBUFFERUTIL_H - -#include "samplebuffer.h" -#include "sampleutil.h" - -// Fills the whole buffer with zeroes -inline void clear(SampleBuffer* pBuffer) { - SampleUtil::clear(pBuffer->data(), pBuffer->size()); -} - -// Fills the whole buffer with the same value -inline void fill(SampleBuffer* pBuffer, SampleBuffer::value_type value) { - SampleUtil::fill(pBuffer->data(), value, pBuffer->size()); -} - -#endif // SAMPLEBUFFERUTIL_H diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index dfd5992ff17..8e55f8af762 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -37,7 +37,7 @@ class AudioSource: public UrlResource { public: typedef std::size_t size_type; typedef std::ptrdiff_t diff_type; - typedef SampleBuffer::value_type sample_type; + typedef CSAMPLE sample_type; static const size_type kChannelCountZero = 0; static const size_type kChannelCountMono = 1; diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 0e505445f3e..d21337a471d 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -290,7 +290,7 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( << frame->header.sample_rate << "<>" << getFrameRate(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - const SampleBuffer::size_type maxBlocksize = samples2frames( + const unsigned maxBlocksize = samples2frames( m_decodeSampleBuffer.size()); if (maxBlocksize < frame->header.blocksize) { qWarning() << "Corrupt or unsupported FLAC file:" @@ -397,7 +397,7 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { m_maxFramesize = metadata->data.stream_info.max_framesize; m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; - const SampleBuffer::size_type decodeSampleBufferSize = + const unsigned decodeSampleBufferSize = m_maxBlocksize * getChannelCount(); SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); break; diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index cc7a459d4b5..80629a5e509 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -62,8 +62,8 @@ class AudioSourceFLAC: public AudioSource { sample_type m_sampleScale; SampleBuffer m_decodeSampleBuffer; - SampleBuffer::size_type m_decodeSampleBufferReadOffset; - SampleBuffer::size_type m_decodeSampleBufferWriteOffset; + int m_decodeSampleBufferReadOffset; + int m_decodeSampleBufferWriteOffset; diff_type m_curFrameIndex; }; From d5a0513830a0a03e8a6e4c8090689611b80edbaa Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 26 Feb 2015 12:28:12 +0100 Subject: [PATCH 207/481] Replace C array with SampleBuffer --- src/cachingreader.cpp | 10 +++------- src/cachingreader.h | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/cachingreader.cpp b/src/cachingreader.cpp index f67683c57ba..38765b5670e 100644 --- a/src/cachingreader.cpp +++ b/src/cachingreader.cpp @@ -24,14 +24,12 @@ CachingReader::CachingReader(QString group, m_readerStatus(INVALID), m_mruChunk(NULL), m_lruChunk(NULL), - m_pRawMemoryBuffer(NULL), + m_sampleBuffer(CachingReaderWorker::kSamplesPerChunk * maximumChunksInMemory), m_iTrackNumFramesCallbackSafe(0) { - int rawMemoryBufferLength = CachingReaderWorker::kSamplesPerChunk * maximumChunksInMemory; - m_pRawMemoryBuffer = new CSAMPLE[rawMemoryBufferLength]; - m_allocatedChunks.reserve(maximumChunksInMemory); + m_allocatedChunks.reserve(m_sampleBuffer.size()); - CSAMPLE* bufferStart = m_pRawMemoryBuffer; + CSAMPLE* bufferStart = m_sampleBuffer.data(); // Divide up the allocated raw memory buffer into total_chunks // chunks. Initialize each chunk to hold nothing and add it to the free @@ -78,8 +76,6 @@ CachingReader::~CachingReader() { m_allocatedChunks.clear(); m_lruChunk = m_mruChunk = NULL; qDeleteAll(m_chunks); - delete [] m_pRawMemoryBuffer; - m_pRawMemoryBuffer = NULL; } // static diff --git a/src/cachingreader.h b/src/cachingreader.h index 16daf52e354..131ecee399c 100644 --- a/src/cachingreader.h +++ b/src/cachingreader.h @@ -79,7 +79,7 @@ class CachingReader : public QObject { // Read num_samples from the SoundSource starting with sample into // buffer. Returns the total number of samples actually written to buffer. - virtual int read(int sample, int num_samples, Mixxx::AudioSource::sample_type* buffer); + virtual int read(int sample, int num_samples, CSAMPLE* buffer); // Issue a list of hints, but check whether any of the hints request a chunk // that is not in the cache. If any hints do request a chunk not in cache, @@ -164,7 +164,7 @@ class CachingReader : public QObject { Chunk* m_lruChunk; // The raw memory buffer which is divided up into chunks. - Mixxx::AudioSource::sample_type* m_pRawMemoryBuffer; + SampleBuffer m_sampleBuffer; int m_iTrackNumFramesCallbackSafe; From 226fb9d1096751699638a62f801a630c4cb1efe1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 26 Feb 2015 12:28:29 +0100 Subject: [PATCH 208/481] Replace some typedefs --- src/cachingreaderworker.h | 2 +- src/sources/audiosourceflac.h | 8 ++++---- src/test/soundproxy_test.cpp | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cachingreaderworker.h b/src/cachingreaderworker.h index df5feb1adad..5df31ce906d 100644 --- a/src/cachingreaderworker.h +++ b/src/cachingreaderworker.h @@ -22,7 +22,7 @@ class AudioSourceProxy; typedef struct Chunk { int chunk_number; int frameCount; - Mixxx::AudioSource::sample_type* stereoSamples; + CSAMPLE* stereoSamples; Chunk* prev_lru; Chunk* next_lru; diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 80629a5e509..7eb120bba23 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -53,10 +53,10 @@ class AudioSourceFLAC: public AudioSource { // subblocks (one for each chan) // flac stores in 'frames', each of which has a header and a certain number // of subframes (one for each channel) - size_type m_minBlocksize; // in time samples (audio samples = time samples * chanCount) - size_type m_maxBlocksize; - size_type m_minFramesize; - size_type m_maxFramesize; + unsigned m_minBlocksize; // in time samples (audio samples = time samples * chanCount) + unsigned m_maxBlocksize; + unsigned m_minFramesize; + unsigned m_maxFramesize; unsigned m_bitsPerSample; sample_type m_sampleScale; diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 3848b863552..99745637c37 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -83,7 +83,7 @@ TEST_F(SoundSourceProxyTest, seekForward) { // seem to support sample accurate seeking. The differences // between the samples decoded with continuous reading and // those samples decoded after seeking are quite noticeable! - const Mixxx::AudioSource::sample_type kOpusSeekDecodingError = 0.2f; + const CSAMPLE kOpusSeekDecodingError = 0.2f; const QString kFilePathPrefix( QDir::currentPath() + "/src/test/id3-test-data/cover-test."); From 603b28413f20fdb4ee0701e79e103421b5385344 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 26 Feb 2015 16:08:34 +0100 Subject: [PATCH 209/481] Replace typedef sample_type with CSAMPLE --- plugins/soundsourcem4a/audiosourcem4a.cpp | 8 ++++---- plugins/soundsourcem4a/audiosourcem4a.h | 2 +- .../audiosourcemediafoundation.cpp | 18 +++++++++--------- .../audiosourcemediafoundation.h | 6 +++--- plugins/soundsourcewv/audiosourcewv.cpp | 8 ++++---- plugins/soundsourcewv/audiosourcewv.h | 4 ++-- src/sources/audiosource.cpp | 6 +++--- src/sources/audiosource.h | 11 +++++------ src/sources/audiosourcecoreaudio.cpp | 2 +- src/sources/audiosourcecoreaudio.h | 2 +- src/sources/audiosourceffmpeg.cpp | 4 ++-- src/sources/audiosourceffmpeg.h | 2 +- src/sources/audiosourceflac.cpp | 10 +++++----- src/sources/audiosourceflac.h | 8 ++++---- src/sources/audiosourcemodplug.cpp | 4 ++-- src/sources/audiosourcemodplug.h | 2 +- src/sources/audiosourcemp3.cpp | 16 ++++++++-------- src/sources/audiosourcemp3.h | 6 +++--- src/sources/audiosourceoggvorbis.cpp | 8 ++++---- src/sources/audiosourceoggvorbis.h | 6 +++--- src/sources/audiosourceopus.cpp | 8 ++++---- src/sources/audiosourceopus.h | 4 ++-- src/sources/audiosourcesndfile.cpp | 2 +- src/sources/audiosourcesndfile.h | 2 +- 24 files changed, 74 insertions(+), 75 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 6d9bc5efb7b..2287344533e 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -274,13 +274,13 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { } AudioSource::size_type AudioSourceM4A::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, CSAMPLE* sampleBuffer) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); - sample_type* pSampleBuffer = sampleBuffer; + CSAMPLE* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= @@ -297,7 +297,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( const size_type numberOfSamplesRead = frames2samples(numberOfFramesRead); if (pSampleBuffer) { - const sample_type* const pDecodeBuffer = + const CSAMPLE* const pDecodeBuffer = &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset]; SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); pSampleBuffer += numberOfSamplesRead; @@ -344,7 +344,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( // be big enough for a whole block of decoded samples, which // contains up to kFramesPerSampleBlock frames. Otherwise // we need to use a temporary buffer. - sample_type* pDecodeBuffer; // in/out parameter + CSAMPLE* pDecodeBuffer; // in/out parameter size_type decodeBufferCapacityInBytes; if (pSampleBuffer && (numberOfFramesRemaining >= kFramesPerSampleBlock)) { // decode samples directly into sampleBuffer diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index f767f50f0fa..637544f2c67 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -32,7 +32,7 @@ class AudioSourceM4A: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) /*override*/; + CSAMPLE* sampleBuffer) /*override*/; private: explicit AudioSourceM4A(QUrl url); diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index afea4bb0a2f..1a4ad65105d 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -23,7 +23,7 @@ const bool sDebug = false; const int kNumChannels = 2; const int kSampleRate = 44100; -const int kLeftoverSize = 4096; // in sample_type's, this seems to be the size MF AAC +const int kLeftoverSize = 4096; // in CSAMPLE's, this seems to be the size MF AAC const int kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give /** @@ -219,7 +219,7 @@ Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekSampleFrame( } Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, CSAMPLE* sampleBuffer) { if (sDebug) { qDebug() << "read()" << numberOfFrames; } @@ -293,7 +293,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readSampleFrames( error = true; goto releaseSample; } - sample_type *buffer(NULL); + CSAMPLE *buffer(NULL); DWORD bufferLengthInBytes(0); hr = pMBuffer->Lock(reinterpret_cast(&buffer), NULL, &bufferLengthInBytes); if (FAILED(hr)) { @@ -312,7 +312,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readSampleFrames( if (m_nextFrame < bufferPosition) { // Uh oh. We are farther forward than our seek target. Emit // silence? We can't seek backwards here. - sample_type* pBufferCurpos = sampleBuffer + CSAMPLE* pBufferCurpos = sampleBuffer + frames2samples(numberOfFrames - framesNeeded); qint64 offshootFrames = bufferPosition - m_nextFrame; @@ -361,7 +361,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readSampleFrames( while (newSize < frames2samples(bufferLength)) { newSize *= 2; } - sample_type* newBuffer = new sample_type[newSize]; + CSAMPLE* newBuffer = new CSAMPLE[newSize]; memcpy(newBuffer, m_leftoverBuffer, sizeof(m_leftoverBuffer[0]) * m_leftoverBufferSize); delete[] m_leftoverBuffer; @@ -533,8 +533,8 @@ bool AudioSourceMediaFoundation::configureAudioStream() { return false; } m_leftoverBufferSize = leftoverBufferSize; - m_leftoverBufferSize /= sizeof(sample_type); // convert size in bytes to sizeof(sample_type) - m_leftoverBuffer = new sample_type[m_leftoverBufferSize]; + m_leftoverBufferSize /= sizeof(CSAMPLE); // convert size in bytes to sizeof(CSAMPLE) + m_leftoverBuffer = new CSAMPLE[m_leftoverBufferSize]; return true; } @@ -544,8 +544,8 @@ bool AudioSourceMediaFoundation::configureAudioStream() { * is moved to the beginning of m_leftoverBuffer, so empty it first (possibly * with this method). If src and dest overlap, I'll hurt you. */ -void AudioSourceMediaFoundation::copyFrames(sample_type *dest, size_t *destFrames, - const sample_type *src, size_t srcFrames) { +void AudioSourceMediaFoundation::copyFrames(CSAMPLE *dest, size_t *destFrames, + const CSAMPLE *src, size_t srcFrames) { if (srcFrames > *destFrames) { int samplesToCopy(*destFrames * kNumChannels); memcpy(dest, src, samplesToCopy * sizeof(*src)); diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index cff63d7688b..15daaad38c7 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -31,7 +31,7 @@ class AudioSourceMediaFoundation : public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: explicit AudioSourceMediaFoundation(QUrl url); @@ -42,7 +42,7 @@ class AudioSourceMediaFoundation : public AudioSource { bool configureAudioStream(); - void copyFrames(sample_type *dest, size_t *destFrames, const sample_type *src, + void copyFrames(CSAMPLE *dest, size_t *destFrames, const CSAMPLE *src, size_t srcFrames); HRESULT m_hrCoInitialize; @@ -51,7 +51,7 @@ class AudioSourceMediaFoundation : public AudioSource { IMFMediaType *m_pAudioType; wchar_t *m_wcFilename; int m_nextFrame; - sample_type *m_leftoverBuffer; + CSAMPLE *m_leftoverBuffer; size_t m_leftoverBufferSize; size_t m_leftoverBufferLength; int m_leftoverBufferPosition; diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index e2989cd79ed..40a1485bad9 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -36,7 +36,7 @@ Result AudioSourceWV::postConstruct() { const int bitsPerSample = WavpackGetBitsPerSample(m_wpc); const uint32_t wavpackPeakSampleValue = uint32_t(1) << (bitsPerSample - 1); - m_sampleScale = kSampleValuePeak / sample_type(wavpackPeakSampleValue); + m_sampleScale = kSampleValuePeak / CSAMPLE(wavpackPeakSampleValue); } return OK; @@ -60,8 +60,8 @@ AudioSource::diff_type AudioSourceWV::seekSampleFrame(diff_type frameIndex) { } AudioSource::size_type AudioSourceWV::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { - // static assert: sizeof(sample_type) == sizeof(int32_t) + size_type numberOfFrames, CSAMPLE* sampleBuffer) { + // static assert: sizeof(CSAMPLE) == sizeof(int32_t) size_type unpackCount = WavpackUnpackSamples(m_wpc, reinterpret_cast(sampleBuffer), numberOfFrames); if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { @@ -70,7 +70,7 @@ AudioSource::size_type AudioSourceWV::readSampleFrames( for (size_type i = 0; i < sampleCount; ++i) { const int32_t sampleValue = reinterpret_cast(sampleBuffer)[i]; - sampleBuffer[i] = sample_type(sampleValue) * m_sampleScale; + sampleBuffer[i] = CSAMPLE(sampleValue) * m_sampleScale; } } return unpackCount; diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index 4f9fb109312..54b8d3a62e4 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -22,7 +22,7 @@ class AudioSourceWV: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) /*override*/; + CSAMPLE* sampleBuffer) /*override*/; private: explicit AudioSourceWV(QUrl url); @@ -33,7 +33,7 @@ class AudioSourceWV: public AudioSource { WavpackContext* m_wpc; - sample_type m_sampleScale; + CSAMPLE m_sampleScale; }; } // namespace Mixxx diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index b543a3d476b..2ccf30c049c 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -7,9 +7,9 @@ namespace Mixxx { -/*static*/const AudioSource::sample_type AudioSource::kSampleValueZero = +/*static*/const CSAMPLE AudioSource::kSampleValueZero = CSAMPLE_ZERO; -/*static*/const AudioSource::sample_type AudioSource::kSampleValuePeak = +/*static*/const CSAMPLE AudioSource::kSampleValuePeak = CSAMPLE_PEAK; AudioSourcePointer AudioSource::onCreate(AudioSource* pNewAudioSource) { @@ -53,7 +53,7 @@ AudioSource::size_type AudioSource::getSampleBufferSize( AudioSource::size_type AudioSource::readSampleFramesStereo( size_type numberOfFrames, - sample_type* sampleBuffer, + CSAMPLE* sampleBuffer, size_type sampleBufferSize) { DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) <= sampleBufferSize); diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 8e55f8af762..c0d164d30c7 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -37,7 +37,6 @@ class AudioSource: public UrlResource { public: typedef std::size_t size_type; typedef std::ptrdiff_t diff_type; - typedef CSAMPLE sample_type; static const size_type kChannelCountZero = 0; static const size_type kChannelCountMono = 1; @@ -53,8 +52,8 @@ class AudioSource: public UrlResource { // 0-based indexing of sample frames static const diff_type kFrameIndexMin = 0; - static const sample_type kSampleValueZero; - static const sample_type kSampleValuePeak; + static const CSAMPLE kSampleValueZero; + static const CSAMPLE kSampleValuePeak; static const size_type kBitrateZero = 0; static const size_type kBitrateDefault = kBitrateZero; @@ -177,11 +176,11 @@ class AudioSource: public UrlResource { // position is moved forward towards the next unread frame. virtual size_type readSampleFrames( size_type numberOfFrames, - sample_type* sampleBuffer) = 0; + CSAMPLE* sampleBuffer) = 0; inline size_type skipSampleFrames( size_type numberOfFrames) { - return readSampleFrames(numberOfFrames, static_cast(NULL)); + return readSampleFrames(numberOfFrames, static_cast(NULL)); } inline size_type readSampleFrames( @@ -228,7 +227,7 @@ class AudioSource: public UrlResource { // transformation without temporary allocations. virtual size_type readSampleFramesStereo( size_type numberOfFrames, - sample_type* sampleBuffer, + CSAMPLE* sampleBuffer, size_type sampleBufferSize); inline size_type readSampleFramesStereo( diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index 843298d3da4..cd0a870c9ba 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -140,7 +140,7 @@ AudioSource::diff_type AudioSourceCoreAudio::seekSampleFrame( } AudioSource::size_type AudioSourceCoreAudio::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, CSAMPLE* sampleBuffer) { //if (!m_decoder) return 0; size_type numFramesRead = 0; diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index afa329626c4..dfa8f2c0791 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -28,7 +28,7 @@ class AudioSourceCoreAudio: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) /*override*/; + CSAMPLE* sampleBuffer) /*override*/; private: explicit AudioSourceCoreAudio(QUrl url); diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 1c67b725581..51ea3f39469 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -485,7 +485,7 @@ unsigned int AudioSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { } AudioSource::size_type AudioSourceFFmpeg::readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) { + CSAMPLE* sampleBuffer) { // This is just a hack that simply reuses existing // functionality. Sample data should be resampled // directly into AV_SAMPLE_FMT_FLT instead of @@ -494,7 +494,7 @@ AudioSource::size_type AudioSourceFFmpeg::readSampleFrames(size_type numberOfFra TempBuffer tempBuffer(frames2samples(numberOfFrames)); const size_type readSamples = read(tempBuffer.size(), &tempBuffer[0]); for (size_type i = 0; i < readSamples; ++i) { - sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / sample_type(SAMPLE_MAX); + sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / CSAMPLE(SAMPLE_MAX); } return samples2frames(readSamples); } diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index 6f877318b82..8edd1536856 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -48,7 +48,7 @@ class AudioSourceFFmpeg : public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: explicit AudioSourceFFmpeg(QUrl url); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index d21337a471d..d64b870ce35 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -142,27 +142,27 @@ Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame( } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, CSAMPLE* sampleBuffer) { return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, CSAMPLE* sampleBuffer, size_type sampleBufferSize) { return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, CSAMPLE* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); const size_type numberOfFramesTotal = numberOfFrames; - sample_type* outBuffer = sampleBuffer; + CSAMPLE* outBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { DEBUG_ASSERT( @@ -383,7 +383,7 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { // not set before m_bitsPerSample = bitsPerSample; m_sampleScale = kSampleValuePeak - / sample_type(FLAC__int32(1) << bitsPerSample); + / CSAMPLE(FLAC__int32(1) << bitsPerSample); } else { // already set before -> check for consistency if (bitsPerSample != m_bitsPerSample) { diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 7eb120bba23..4df6eea7460 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -19,9 +19,9 @@ class AudioSourceFLAC: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) /*override*/; + CSAMPLE* sampleBuffer) /*override*/; size_type readSampleFramesStereo(size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; + CSAMPLE* sampleBuffer, size_type sampleBufferSize) /*override*/; // callback methods FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t* bytes); @@ -42,7 +42,7 @@ class AudioSourceFLAC: public AudioSource { void preDestroy(); size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize, + CSAMPLE* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); QFile m_file; @@ -59,7 +59,7 @@ class AudioSourceFLAC: public AudioSource { unsigned m_maxFramesize; unsigned m_bitsPerSample; - sample_type m_sampleScale; + CSAMPLE m_sampleScale; SampleBuffer m_decodeSampleBuffer; int m_decodeSampleBufferReadOffset; diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index 8db692d0231..e6807a3caef 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -131,7 +131,7 @@ AudioSource::diff_type AudioSourceModPlug::seekSampleFrame( } AudioSource::size_type AudioSourceModPlug::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, CSAMPLE* sampleBuffer) { const size_type maxFrames = samples2frames(m_sampleBuf.size()); const size_type readFrames = math_min(maxFrames - m_seekPos, numberOfFrames); @@ -139,7 +139,7 @@ AudioSource::size_type AudioSourceModPlug::readSampleFrames( const size_type readOffset = frames2samples(m_seekPos); for (size_type i = 0; i < readSamples; ++i) { sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) - / sample_type(SAMPLE_MAX); + / CSAMPLE(SAMPLE_MAX); } m_seekPos += readFrames; diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index 692b203f533..8f272853a4b 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -30,7 +30,7 @@ class AudioSourceModPlug: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) /*override*/; + CSAMPLE* sampleBuffer) /*override*/; private: static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 2a1476b4430..84cb78c0a0d 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -13,10 +13,10 @@ namespace { // http://www.mars.org/mailman/public/mad-dev/2002-May/000634.html const AudioSource::size_type kSeekFramePrefetchCount = 29; -const AudioSource::sample_type kMadScale = AudioSource::kSampleValuePeak - / AudioSource::sample_type(MAD_F_ONE); +const CSAMPLE kMadScale = AudioSource::kSampleValuePeak + / CSAMPLE(MAD_F_ONE); -inline AudioSource::sample_type madScale(mad_fixed_t sample) { +inline CSAMPLE madScale(mad_fixed_t sample) { return sample * kMadScale; } // anonymous namespace @@ -407,14 +407,14 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { } AudioSource::size_type AudioSourceMp3::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, CSAMPLE* sampleBuffer) { return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } AudioSource::size_type AudioSourceMp3::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, CSAMPLE* sampleBuffer, size_type sampleBufferSize) { return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, @@ -422,7 +422,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFramesStereo( } AudioSource::size_type AudioSourceMp3::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, CSAMPLE* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); @@ -430,7 +430,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); - sample_type* pSampleBuffer = sampleBuffer; + CSAMPLE* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { if (0 >= m_madSynthCount) { @@ -496,7 +496,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( DEBUG_ASSERT(madSynthOffset < m_madSynth.pcm.length); if (isChannelCountMono()) { for (size_type i = 0; i < synthReadCount; ++i) { - const sample_type sampleValue = madScale( + const CSAMPLE sampleValue = madScale( m_madSynth.pcm.samples[0][madSynthOffset + i]); *pSampleBuffer = sampleValue; ++pSampleBuffer; diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index 2ad519b80b7..c1ac6d0d96a 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -26,9 +26,9 @@ class AudioSourceMp3: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) /*override*/; + CSAMPLE* sampleBuffer) /*override*/; size_type readSampleFramesStereo(size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; + CSAMPLE* sampleBuffer, size_type sampleBufferSize) /*override*/; private: explicit AudioSourceMp3(QUrl url); @@ -38,7 +38,7 @@ class AudioSourceMp3: public AudioSource { void preDestroy(); size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize, + CSAMPLE* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); QFile m_file; diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index e30edbcb733..6d17173eecc 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -98,20 +98,20 @@ AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( } AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, CSAMPLE* sampleBuffer) { return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } AudioSource::size_type AudioSourceOggVorbis::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, CSAMPLE* sampleBuffer, size_type sampleBufferSize) { return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, CSAMPLE* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); @@ -119,7 +119,7 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); - sample_type* pSampleBuffer = sampleBuffer; + CSAMPLE* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { float** pcmChannels; diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index bc0f63e500b..dc28dd9df60 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -17,9 +17,9 @@ class AudioSourceOggVorbis: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) /*override*/; + CSAMPLE* sampleBuffer) /*override*/; size_type readSampleFramesStereo(size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; + CSAMPLE* sampleBuffer, size_type sampleBufferSize) /*override*/; private: explicit AudioSourceOggVorbis(QUrl url); @@ -29,7 +29,7 @@ class AudioSourceOggVorbis: public AudioSource { void preDestroy(); size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize, + CSAMPLE* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); OggVorbis_File m_vf; diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 34d256b81b1..c01b358be92 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -105,13 +105,13 @@ AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { } AudioSource::size_type AudioSourceOpus::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, CSAMPLE* sampleBuffer) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); - sample_type* pSampleBuffer = sampleBuffer; + CSAMPLE* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { int readResult = op_read_float(m_pOggOpusFile, @@ -134,7 +134,7 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( } AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, CSAMPLE* sampleBuffer, size_type sampleBufferSize) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) <= sampleBufferSize); @@ -142,7 +142,7 @@ AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( const size_type numberOfFramesTotal = math_min(numberOfFrames, size_type(getFrameIndexMax() - m_curFrameIndex)); - sample_type* pSampleBuffer = sampleBuffer; + CSAMPLE* pSampleBuffer = sampleBuffer; size_type numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { int readResult = op_read_float_stereo(m_pOggOpusFile, diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 59e3bc362ae..1d6853b6282 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -19,9 +19,9 @@ class AudioSourceOpus: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) /*override*/; + CSAMPLE* sampleBuffer) /*override*/; size_type readSampleFramesStereo(size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; + CSAMPLE* sampleBuffer, size_type sampleBufferSize) /*override*/; private: explicit AudioSourceOpus(QUrl url); diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index e0ac2aa23f0..21ad805487b 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -73,7 +73,7 @@ AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( } AudioSource::size_type AudioSourceSndFile::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, CSAMPLE* sampleBuffer) { const sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); if (0 <= readCount) { diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 6eb830ba8d3..5fba71e5341 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -22,7 +22,7 @@ class AudioSourceSndFile: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) /*override*/; + CSAMPLE* sampleBuffer) /*override*/; private: explicit AudioSourceSndFile(QUrl url); From 2fe6cd37f6b32493071de545455c80437c762e66 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 26 Feb 2015 22:18:47 +0100 Subject: [PATCH 210/481] Update SampleBuffer: Use SINT --- src/samplebuffer.cpp | 2 +- src/samplebuffer.h | 16 ++++++++-------- src/util/types.h | 5 +++++ 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/samplebuffer.cpp b/src/samplebuffer.cpp index edcba5f05fc..a3b0e00b274 100644 --- a/src/samplebuffer.cpp +++ b/src/samplebuffer.cpp @@ -2,7 +2,7 @@ #include "sampleutil.h" -SampleBuffer::SampleBuffer(int size) +SampleBuffer::SampleBuffer(SINT size) : m_data(SampleUtil::alloc(size)), m_size(m_data ? size : 0) { } diff --git a/src/samplebuffer.h b/src/samplebuffer.h index a07f485ed82..8222013507a 100644 --- a/src/samplebuffer.h +++ b/src/samplebuffer.h @@ -1,14 +1,14 @@ #ifndef SAMPLEBUFFER_H #define SAMPLEBUFFER_H -#include "util/types.h" // CSAMPLE +#include "util/types.h" #include // std::swap // A sample buffer with properly aligned memory to enable SSE optimizations. -// The public interface closely follows that of std::vector. +// The public SINTerface closely follows that of std::vector. // -// No resize operation is provided intentionally for maximum efficiency! +// No resize operation is provided SINTentionally for maximum efficiency! // If the size of an existing sample buffer needs to be altered after // construction this can simply be achieved by swapping the contents with // a temporary sample buffer that has been constructed with the desired @@ -37,7 +37,7 @@ class SampleBuffer { : m_data(NULL), m_size(0) { } - explicit SampleBuffer(int size); + explicit SampleBuffer(SINT size); virtual ~SampleBuffer(); void swap(SampleBuffer& other) { @@ -45,7 +45,7 @@ class SampleBuffer { std::swap(m_size, other.m_size); } - int size() const { + SINT size() const { return m_size; } @@ -56,10 +56,10 @@ class SampleBuffer { return m_data; } - CSAMPLE& operator[](int index) { + CSAMPLE& operator[](SINT index) { return m_data[index]; } - const CSAMPLE& operator[](int index) const { + const CSAMPLE& operator[](SINT index) const { return m_data[index]; } @@ -71,7 +71,7 @@ class SampleBuffer { private: CSAMPLE* m_data; - int m_size; + SINT m_size; }; #endif // SAMPLEBUFFER_H diff --git a/src/util/types.h b/src/util/types.h index 3a28c698e1f..f181c57fda5 100644 --- a/src/util/types.h +++ b/src/util/types.h @@ -3,8 +3,13 @@ #include "util/math.h" +#include #include +// Signed integer type for all kinds of sizes, array indices and pointer +// arithmetic. Its size (32-/64-bit) depends on the CPU architecture. +typedef std::ptrdiff_t SINT; + // 16-bit integer sample data within the asymmetric // range [SHRT_MIN, SHRT_MAX]. typedef short int SAMPLE; From 9781290169e7e3e6bdb796a50600b35d16112f4b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 27 Feb 2015 00:26:42 +0100 Subject: [PATCH 211/481] Replace typedefs in AudioSource with SINT --- plugins/soundsourcem4a/audiosourcem4a.cpp | 44 ++++----- plugins/soundsourcem4a/audiosourcem4a.h | 10 +-- .../audiosourcemediafoundation.cpp | 14 +-- .../audiosourcemediafoundation.h | 4 +- plugins/soundsourcewv/audiosourcewv.cpp | 12 +-- plugins/soundsourcewv/audiosourcewv.h | 4 +- src/analyserqueue.cpp | 14 +-- src/cachingreaderworker.cpp | 18 ++-- src/cachingreaderworker.h | 8 +- src/musicbrainz/chromaprinter.cpp | 8 +- src/sources/audiosource.cpp | 24 ++--- src/sources/audiosource.h | 89 +++++++++---------- src/sources/audiosourcecoreaudio.cpp | 14 +-- src/sources/audiosourcecoreaudio.h | 4 +- src/sources/audiosourceffmpeg.cpp | 10 +-- src/sources/audiosourceffmpeg.h | 4 +- src/sources/audiosourceflac.cpp | 38 ++++---- src/sources/audiosourceflac.h | 14 +-- src/sources/audiosourcemodplug.cpp | 18 ++-- src/sources/audiosourcemodplug.h | 12 +-- src/sources/audiosourcemp3.cpp | 82 ++++++++--------- src/sources/audiosourcemp3.h | 26 +++--- src/sources/audiosourceoggvorbis.cpp | 28 +++--- src/sources/audiosourceoggvorbis.h | 14 +-- src/sources/audiosourceopus.cpp | 26 +++--- src/sources/audiosourceopus.h | 12 +-- src/sources/audiosourcesndfile.cpp | 8 +- src/sources/audiosourcesndfile.h | 4 +- src/test/soundproxy_test.cpp | 22 ++--- 29 files changed, 290 insertions(+), 295 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 2287344533e..86a9b098c4b 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -27,18 +27,18 @@ namespace { // According to the specification each AAC sample block decodes // to 1024 sample frames. The rarely used AAC-960 profile with // a block size of 960 frames is not supported. -const AudioSource::size_type kFramesPerSampleBlock = 1024; +const SINT kFramesPerSampleBlock = 1024; // MP4SampleId is 1-based const MP4SampleId kSampleBlockIdMin = 1; -inline AudioSource::diff_type getFrameIndexForSampleBlockId( +inline SINT getFrameIndexForSampleBlockId( MP4SampleId sampleBlockId) { return AudioSource::kFrameIndexMin + (sampleBlockId - kSampleBlockIdMin) * kFramesPerSampleBlock; } -inline AudioSource::size_type getFrameCountForSampleBlockId( +inline SINT getFrameCountForSampleBlockId( MP4SampleId sampleBlockId) { return ((sampleBlockId - kSampleBlockIdMin) + 1) * kFramesPerSampleBlock; } @@ -187,7 +187,7 @@ Result AudioSourceM4A::postConstruct() { setFrameCount(getFrameCountForSampleBlockId(m_maxSampleBlockId)); // Resize temporary buffer for decoded sample data - const size_type decodeSampleBufferSize = + const SINT decodeSampleBufferSize = frames2samples(kFramesPerSampleBlock); SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); @@ -232,7 +232,7 @@ void AudioSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { } -AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { +SINT AudioSourceM4A::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (m_curFrameIndex != frameIndex) { @@ -259,8 +259,8 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { // decoding starts before the actual target position DEBUG_ASSERT(m_curFrameIndex <= frameIndex); // prefetch (decode and discard) all samples up to the target position - const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; - const size_type skipFrameCount = + const SINT prefetchFrameCount = frameIndex - m_curFrameIndex; + const SINT skipFrameCount = skipSampleFrames(prefetchFrameCount); DEBUG_ASSERT(skipFrameCount <= prefetchFrameCount); if (skipFrameCount != prefetchFrameCount) { @@ -273,28 +273,28 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { return m_curFrameIndex; } -AudioSource::size_type AudioSourceM4A::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { +SINT AudioSourceM4A::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - const size_type numberOfFramesTotal = math_min(numberOfFrames, - size_type(getFrameIndexMax() - m_curFrameIndex)); + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); CSAMPLE* pSampleBuffer = sampleBuffer; - size_type numberOfFramesRemaining = numberOfFramesTotal; + SINT numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); if (m_decodeSampleBufferReadOffset < m_decodeSampleBufferWriteOffset) { // Consume previously decoded sample data - const size_type numberOfSamplesDecoded = + const SINT numberOfSamplesDecoded = m_decodeSampleBufferWriteOffset - m_decodeSampleBufferReadOffset; - const size_type numberOfFramesDecoded = + const SINT numberOfFramesDecoded = samples2frames(numberOfSamplesDecoded); - const size_type numberOfFramesRead = + const SINT numberOfFramesRead = math_min(numberOfFramesRemaining, numberOfFramesDecoded); - const size_type numberOfSamplesRead = + const SINT numberOfSamplesRead = frames2samples(numberOfFramesRead); if (pSampleBuffer) { const CSAMPLE* const pDecodeBuffer = @@ -345,7 +345,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( // contains up to kFramesPerSampleBlock frames. Otherwise // we need to use a temporary buffer. CSAMPLE* pDecodeBuffer; // in/out parameter - size_type decodeBufferCapacityInBytes; + SINT decodeBufferCapacityInBytes; if (pSampleBuffer && (numberOfFramesRemaining >= kFramesPerSampleBlock)) { // decode samples directly into sampleBuffer pDecodeBuffer = pSampleBuffer; @@ -387,7 +387,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( << "<>" << getChannelCount(); break; // abort } - if (getFrameRate() != decFrameInfo.samplerate) { + if (getFrameRate() != SINT(decFrameInfo.samplerate)) { qWarning() << "Corrupt or unsupported AAC file:" << "Unexpected sample rate" << decFrameInfo.samplerate << "<>" << getFrameRate(); @@ -398,13 +398,13 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( m_inputBufferOffset += decFrameInfo.bytesconsumed; // consume decoded output data - const size_type numberOfSamplesDecoded = + const SINT numberOfSamplesDecoded = decFrameInfo.samples; - const size_type numberOfFramesDecoded = + const SINT numberOfFramesDecoded = samples2frames(numberOfSamplesDecoded); - const size_type numberOfFramesRead = + const SINT numberOfFramesRead = math_min(numberOfFramesRemaining, numberOfFramesDecoded); - const size_type numberOfSamplesRead = + const SINT numberOfSamplesRead = frames2samples(numberOfFramesRead); if (pDecodeBuffer == pSampleBuffer) { pSampleBuffer += numberOfSamplesRead; diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 637544f2c67..8473b8299a3 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -29,9 +29,9 @@ class AudioSourceM4A: public AudioSource { ~AudioSourceM4A(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: @@ -52,8 +52,8 @@ class AudioSourceM4A: public AudioSource { typedef std::vector InputBuffer; InputBuffer m_inputBuffer; - InputBuffer::size_type m_inputBufferOffset; - InputBuffer::size_type m_inputBufferLength; + SINT m_inputBufferOffset; + SINT m_inputBufferLength; NeAACDecHandle m_hDecoder; @@ -61,7 +61,7 @@ class AudioSourceM4A: public AudioSource { int m_decodeSampleBufferReadOffset; int m_decodeSampleBufferWriteOffset; - diff_type m_curFrameIndex; + SINT m_curFrameIndex; }; } diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index 1a4ad65105d..ef400bc0356 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -169,8 +169,8 @@ void AudioSourceMediaFoundation::preDestroy() { } } -Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekSampleFrame( - diff_type frameIndex) { +SINT AudioSourceMediaFoundation::seekSampleFrame( + SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (sDebug) { qDebug() << "seekSampleFrame()" << frameIndex; @@ -218,12 +218,12 @@ Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekSampleFrame( return result; } -Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { +SINT AudioSourceMediaFoundation::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { if (sDebug) { qDebug() << "read()" << numberOfFrames; } - size_type framesNeeded(numberOfFrames); + SINT framesNeeded(numberOfFrames); // first, copy frames from leftover buffer IF the leftover buffer is at // the correct frame @@ -300,7 +300,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readSampleFrames( error = true; goto releaseMBuffer; } - size_type bufferLength = samples2frames(bufferLengthInBytes / sizeof(buffer[0])); + SINT bufferLength = samples2frames(bufferLengthInBytes / sizeof(buffer[0])); if (m_seeking) { qint64 bufferPosition(frameFromMF(timestamp)); @@ -384,7 +384,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readSampleFrames( break; } - size_type framesRead = numberOfFrames - framesNeeded; + SINT framesRead = numberOfFrames - framesNeeded; m_iCurrentPosition += framesRead; m_nextFrame += framesRead; if (m_leftoverBufferLength > 0) { diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index 15daaad38c7..3733bd89793 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -29,9 +29,9 @@ class AudioSourceMediaFoundation : public AudioSource { ~AudioSourceMediaFoundation(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: explicit AudioSourceMediaFoundation(QUrl url); diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 40a1485bad9..dba3f830cad 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -49,7 +49,7 @@ void AudioSourceWV::preDestroy() { } } -AudioSource::diff_type AudioSourceWV::seekSampleFrame(diff_type frameIndex) { +SINT AudioSourceWV::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (WavpackSeekSample(m_wpc, frameIndex) == TRUE) { return frameIndex; @@ -59,15 +59,15 @@ AudioSource::diff_type AudioSourceWV::seekSampleFrame(diff_type frameIndex) { } } -AudioSource::size_type AudioSourceWV::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { +SINT AudioSourceWV::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { // static assert: sizeof(CSAMPLE) == sizeof(int32_t) - size_type unpackCount = WavpackUnpackSamples(m_wpc, + SINT unpackCount = WavpackUnpackSamples(m_wpc, reinterpret_cast(sampleBuffer), numberOfFrames); if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { // signed integer -> float - const size_type sampleCount = frames2samples(unpackCount); - for (size_type i = 0; i < sampleCount; ++i) { + const SINT sampleCount = frames2samples(unpackCount); + for (SINT i = 0; i < sampleCount; ++i) { const int32_t sampleValue = reinterpret_cast(sampleBuffer)[i]; sampleBuffer[i] = CSAMPLE(sampleValue) * m_sampleScale; diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index 54b8d3a62e4..74c59f9a44c 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -19,9 +19,9 @@ class AudioSourceWV: public AudioSource { ~AudioSourceWV(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 27bff31965e..13cd3b3bc02 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -31,9 +31,9 @@ namespace { // We need to use a smaller block size, because on Linux the AnalyserQueue // can starve the CPU of its resources, resulting in xruns. A block size // of 4096 frames per block seems to do fine. - const Mixxx::AudioSource::size_type kAnalysisChannels = 2; // stereo - const Mixxx::AudioSource::size_type kAnalysisFramesPerBlock = 4096; - const Mixxx::AudioSource::size_type kAnalysisSamplesPerBlock = + const SINT kAnalysisChannels = 2; // stereo + const SINT kAnalysisFramesPerBlock = 4096; + const SINT kAnalysisSamplesPerBlock = kAnalysisFramesPerBlock * kAnalysisChannels; } // anonymous namespace @@ -166,20 +166,20 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi QTime progressUpdateInhibitTimer; progressUpdateInhibitTimer.start(); // Inhibit Updates for 60 milliseconds - Mixxx::AudioSource::diff_type frameIndex = pAudioSource->getFrameIndexMin(); + SINT frameIndex = pAudioSource->getFrameIndexMin(); bool dieflag = false; bool cancelled = false; do { ScopedTimer t("AnalyserQueue::doAnalysis block"); DEBUG_ASSERT(frameIndex < pAudioSource->getFrameIndexMax()); - const Mixxx::AudioSource::size_type framesRemaining = + const SINT framesRemaining = pAudioSource->getFrameIndexMax() - frameIndex; - const Mixxx::AudioSource::size_type framesToRead = + const SINT framesToRead = math_min(kAnalysisFramesPerBlock, framesRemaining); DEBUG_ASSERT(0 < framesToRead); - const Mixxx::AudioSource::size_type framesRead = + const SINT framesRead = pAudioSource->readSampleFramesStereo( kAnalysisFramesPerBlock, &m_sampleBuffer); diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 641eca8c6a8..8cb4979b140 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -21,9 +21,9 @@ // TODO(XXX): The optimum value of the "constant" kFramesPerChunk // depends on the properties of the AudioSource as the remarks // above suggest! -const Mixxx::AudioSource::size_type CachingReaderWorker::kChunkChannels = 2; // stereo -const Mixxx::AudioSource::size_type CachingReaderWorker::kFramesPerChunk = 8192; // ~ 170 ms at 48 kHz -const Mixxx::AudioSource::size_type CachingReaderWorker::kSamplesPerChunk = kFramesPerChunk * kChunkChannels; +const SINT CachingReaderWorker::kChunkChannels = 2; // stereo +const SINT CachingReaderWorker::kFramesPerChunk = 8192; // ~ 170 ms at 48 kHz +const SINT CachingReaderWorker::kSamplesPerChunk = kFramesPerChunk * kChunkChannels; CachingReaderWorker::CachingReaderWorker(QString group, FIFO* pChunkReadRequestFIFO, @@ -53,7 +53,7 @@ void CachingReaderWorker::processChunkReadRequest( return; } - const Mixxx::AudioSource::size_type chunkFrameIndex = + const SINT chunkFrameIndex = frameForChunk(chunk_number); if (!m_pAudioSource->isValidFrameIndex(chunkFrameIndex)) { // Frame index out of range @@ -61,7 +61,7 @@ void CachingReaderWorker::processChunkReadRequest( return; } - const Mixxx::AudioSource::size_type seekFrameIndex = + const SINT seekFrameIndex = m_pAudioSource->seekSampleFrame(chunkFrameIndex); if (seekFrameIndex != chunkFrameIndex) { // Failed to seek to the requested index. @@ -71,9 +71,9 @@ void CachingReaderWorker::processChunkReadRequest( return; } - const Mixxx::AudioSource::size_type framesRemaining = + const SINT framesRemaining = m_pAudioSource->getFrameIndexMax() - seekFrameIndex; - const Mixxx::AudioSource::size_type framesToRead = + const SINT framesToRead = math_min(kFramesPerChunk, framesRemaining); if (0 >= framesToRead) { // No more data available for reading @@ -81,7 +81,7 @@ void CachingReaderWorker::processChunkReadRequest( return; } - const Mixxx::AudioSource::size_type framesRead = + const SINT framesRead = m_pAudioSource->readSampleFramesStereo( framesToRead, request->chunk->stereoSamples, kSamplesPerChunk); DEBUG_ASSERT(framesRead <= framesToRead); @@ -194,7 +194,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { } // Emit that the track is loaded. - const Mixxx::AudioSource::size_type sampleCount = + const SINT sampleCount = m_pAudioSource->getFrameCount() * kChunkChannels; emit(trackLoaded(pTrack, m_pAudioSource->getFrameRate(), sampleCount)); } diff --git a/src/cachingreaderworker.h b/src/cachingreaderworker.h index 5df31ce906d..0144740540d 100644 --- a/src/cachingreaderworker.h +++ b/src/cachingreaderworker.h @@ -82,12 +82,12 @@ class CachingReaderWorker : public EngineWorker { // A Chunk is a memory-resident section of audio that has been cached. Each // chunk holds a fixed number of stereo frames given by kFramesPerChunk. - static const Mixxx::AudioSource::size_type kChunkChannels; - static const Mixxx::AudioSource::size_type kFramesPerChunk; - static const Mixxx::AudioSource::size_type kSamplesPerChunk; // = kFramesPerChunk * kChunkChannels + static const SINT kChunkChannels; + static const SINT kFramesPerChunk; + static const SINT kSamplesPerChunk; // = kFramesPerChunk * kChunkChannels // Given a chunk number, return the start sample number for the chunk. - static Mixxx::AudioSource::size_type frameForChunk(Mixxx::AudioSource::size_type chunk_number) { + static SINT frameForChunk(SINT chunk_number) { return chunk_number * kFramesPerChunk; } diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index e08873c2468..35678917c64 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -17,12 +17,12 @@ namespace // AcoustID only stores a fingerprint for the first two minutes of a song // on their server so we need only a fingerprint of the first two minutes // --kain88 July 2012 - const Mixxx::AudioSource::size_type kFingerprintDuration = 120; // in seconds - const Mixxx::AudioSource::size_type kFingerprintChannels = 2; // stereo + const SINT kFingerprintDuration = 120; // in seconds + const SINT kFingerprintChannels = 2; // stereo QString calcFingerprint(const Mixxx::AudioSourcePointer& pAudioSource) { - Mixxx::AudioSource::size_type numFrames = + SINT numFrames = kFingerprintDuration * pAudioSource->getFrameRate(); // check that the song is actually longer then the amount of audio we use if (numFrames > pAudioSource->getFrameCount()) { @@ -39,7 +39,7 @@ namespace math_max(numFrames * kFingerprintChannels, pAudioSource->frames2samples(numFrames))); DEBUG_ASSERT(2 == kFingerprintChannels); // implicit assumption of the next line - const Mixxx::AudioSource::size_type readFrames = + const SINT readFrames = pAudioSource->readSampleFramesStereo(numFrames, &sampleBuffer); if (readFrames != numFrames) { qDebug() << "oh that's embarrassing I couldn't read the track"; diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index 2ccf30c049c..b43f7a05ce1 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -31,18 +31,18 @@ AudioSource::AudioSource(QUrl url) m_bitrate(kBitrateDefault) { } -void AudioSource::setChannelCount(size_type channelCount) { +void AudioSource::setChannelCount(SINT channelCount) { m_channelCount = channelCount; } -void AudioSource::setFrameRate(size_type frameRate) { +void AudioSource::setFrameRate(SINT frameRate) { m_frameRate = frameRate; } -void AudioSource::setFrameCount(size_type frameCount) { +void AudioSource::setFrameCount(SINT frameCount) { m_frameCount = frameCount; } -AudioSource::size_type AudioSource::getSampleBufferSize( - size_type numberOfFrames, +SINT AudioSource::getSampleBufferSize( + SINT numberOfFrames, bool readStereoSamples) const { if (readStereoSamples) { return numberOfFrames * 2; @@ -51,16 +51,16 @@ AudioSource::size_type AudioSource::getSampleBufferSize( } } -AudioSource::size_type AudioSource::readSampleFramesStereo( - size_type numberOfFrames, +SINT AudioSource::readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, - size_type sampleBufferSize) { + SINT sampleBufferSize) { DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) <= sampleBufferSize); switch (getChannelCount()) { case 1: // mono channel { - const AudioSource::size_type readFrameCount = readSampleFrames( + const SINT readFrameCount = readSampleFrames( numberOfFrames, sampleBuffer); SampleUtil::doubleMonoToDualMono(sampleBuffer, readFrameCount); return readFrameCount; @@ -71,10 +71,10 @@ AudioSource::size_type AudioSource::readSampleFramesStereo( } default: // multiple (3 or more) channels { - const size_type numberOfSamplesToRead = frames2samples(numberOfFrames); + const SINT numberOfSamplesToRead = frames2samples(numberOfFrames); if (numberOfSamplesToRead <= sampleBufferSize) { // efficient in-place transformation - const AudioSource::size_type readFrameCount = readSampleFrames( + const SINT readFrameCount = readSampleFrames( numberOfFrames, sampleBuffer); SampleUtil::copyMultiToStereo(sampleBuffer, sampleBuffer, readFrameCount, getChannelCount()); @@ -87,7 +87,7 @@ AudioSource::size_type AudioSource::readSampleFramesStereo( << "The size of the provided sample buffer is" << sampleBufferSize; SampleBuffer tempBuffer(numberOfSamplesToRead); - const AudioSource::size_type readFrameCount = readSampleFrames( + const SINT readFrameCount = readSampleFrames( numberOfFrames, tempBuffer.data()); SampleUtil::copyMultiToStereo(sampleBuffer, tempBuffer.data(), readFrameCount, getChannelCount()); diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index c0d164d30c7..4e3d3c0490d 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -10,8 +10,6 @@ #include -#include // size_t / diff_t - namespace Mixxx { class AudioSource; @@ -35,32 +33,29 @@ typedef QSharedPointer AudioSourcePointer; // closed upon destruction. class AudioSource: public UrlResource { public: - typedef std::size_t size_type; - typedef std::ptrdiff_t diff_type; - - static const size_type kChannelCountZero = 0; - static const size_type kChannelCountMono = 1; - static const size_type kChannelCountStereo = 2; - static const size_type kChannelCountDefault = kChannelCountZero; + static const SINT kChannelCountZero = 0; + static const SINT kChannelCountMono = 1; + static const SINT kChannelCountStereo = 2; + static const SINT kChannelCountDefault = kChannelCountZero; - static const size_type kFrameRateZero = 0; - static const size_type kFrameRateDefault = kFrameRateZero; + static const SINT kFrameRateZero = 0; + static const SINT kFrameRateDefault = kFrameRateZero; - static const size_type kFrameCountZero = 0; - static const size_type kFrameCountDefault = kFrameCountZero; + static const SINT kFrameCountZero = 0; + static const SINT kFrameCountDefault = kFrameCountZero; // 0-based indexing of sample frames - static const diff_type kFrameIndexMin = 0; + static const SINT kFrameIndexMin = 0; static const CSAMPLE kSampleValueZero; static const CSAMPLE kSampleValuePeak; - static const size_type kBitrateZero = 0; - static const size_type kBitrateDefault = kBitrateZero; + static const SINT kBitrateZero = 0; + static const SINT kBitrateDefault = kBitrateZero; // Returns the number of channels. The number of channels // must be constant over time. - inline size_type getChannelCount() const { + inline SINT getChannelCount() const { return m_channelCount; } inline bool isChannelCountValid() const { @@ -77,7 +72,7 @@ class AudioSource: public UrlResource { // the number samples for each channel per second, which // must be uniform among all channels. The frame rate // must be constant over time. - inline size_type getFrameRate() const { + inline SINT getFrameRate() const { return m_frameRate; } inline bool isFrameRateValid() const { @@ -85,7 +80,7 @@ class AudioSource: public UrlResource { } // Returns the total number of frames. - inline size_type getFrameCount() const { + inline SINT getFrameCount() const { return m_frameCount; } inline bool isFrameCountEmpty() const { @@ -107,7 +102,7 @@ class AudioSource: public UrlResource { inline bool hasBitrate() const { return kBitrateZero < m_bitrate; } - inline size_type getBitrate() const { + inline SINT getBitrate() const { return m_bitrate; } @@ -116,7 +111,7 @@ class AudioSource: public UrlResource { inline bool hasDuration() const { return isValid(); } - inline size_type getDuration() const { + inline SINT getDuration() const { DEBUG_ASSERT(hasDuration()); // prevents division by zero return getFrameCount() / getFrameRate(); } @@ -136,19 +131,19 @@ class AudioSource: public UrlResource { } // Index of the first sample frame. - diff_type getFrameIndexMin() const { + SINT getFrameIndexMin() const { return kFrameIndexMin; } // Index of the sample frame following the last // sample frame. - diff_type getFrameIndexMax() const { + SINT getFrameIndexMax() const { return kFrameIndexMin + getFrameCount(); } // The sample frame index is valid in the range // [getFrameIndexMin(), getFrameIndexMax()]. - inline bool isValidFrameIndex(diff_type frameIndex) const { + inline bool isValidFrameIndex(SINT frameIndex) const { return (getFrameIndexMin() <= frameIndex) && (getFrameIndexMax() >= frameIndex); } @@ -160,7 +155,7 @@ class AudioSource: public UrlResource { // - The seek position in seconds is frameIndex / frameRate() // Returns the actual current frame index which may differ from the // requested index if the source does not support accurate seeking. - virtual diff_type seekSampleFrame(diff_type frameIndex) = 0; + virtual SINT seekSampleFrame(SINT frameIndex) = 0; // Fills the buffer with samples from each channel starting // at the current frame seek position. @@ -174,20 +169,20 @@ class AudioSource: public UrlResource { // might be lower than the requested number of frames when the end // of the audio stream has been reached. The current frame seek // position is moved forward towards the next unread frame. - virtual size_type readSampleFrames( - size_type numberOfFrames, + virtual SINT readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) = 0; - inline size_type skipSampleFrames( - size_type numberOfFrames) { + inline SINT skipSampleFrames( + SINT numberOfFrames) { return readSampleFrames(numberOfFrames, static_cast(NULL)); } - inline size_type readSampleFrames( - size_type numberOfFrames, + inline SINT readSampleFrames( + SINT numberOfFrames, SampleBuffer* pSampleBuffer) { if (pSampleBuffer) { - DEBUG_ASSERT(frames2samples(numberOfFrames) <= size_type(pSampleBuffer->size())); + DEBUG_ASSERT(frames2samples(numberOfFrames) <= SINT(pSampleBuffer->size())); return readSampleFrames(numberOfFrames, pSampleBuffer->data()); } else { return skipSampleFrames(numberOfFrames); @@ -225,13 +220,13 @@ class AudioSource: public UrlResource { // They may also have reduced space requirements on sampleBuffer, // i.e. only the minimum size is required for an in-place // transformation without temporary allocations. - virtual size_type readSampleFramesStereo( - size_type numberOfFrames, + virtual SINT readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, - size_type sampleBufferSize); + SINT sampleBufferSize); - inline size_type readSampleFramesStereo( - size_type numberOfFrames, + inline SINT readSampleFramesStereo( + SINT numberOfFrames, SampleBuffer* pSampleBuffer) { if (pSampleBuffer) { return readSampleFramesStereo(numberOfFrames, @@ -248,24 +243,24 @@ class AudioSource: public UrlResource { virtual Result postConstruct() /*override*/ = 0; - void setChannelCount(size_type channelCount); - void setFrameRate(size_type frameRate); - void setFrameCount(size_type frameCount); + void setChannelCount(SINT channelCount); + void setFrameRate(SINT frameRate); + void setFrameCount(SINT frameCount); - inline void setBitrate(size_type bitrate) { + inline void setBitrate(SINT bitrate) { m_bitrate = bitrate; } - size_type getSampleBufferSize( - size_type numberOfFrames, + SINT getSampleBufferSize( + SINT numberOfFrames, bool readStereoSamples = false) const; private: - size_type m_channelCount; - size_type m_frameRate; - size_type m_frameCount; + SINT m_channelCount; + SINT m_frameRate; + SINT m_frameCount; - size_type m_bitrate; + SINT m_bitrate; }; } // namespace Mixxx diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index cd0a870c9ba..9d0ffbc13ee 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -7,7 +7,7 @@ namespace Mixxx { namespace { -AudioSource::size_type kChannelCount = 2; +SINT kChannelCount = 2; } AudioSourceCoreAudio::AudioSourceCoreAudio(QUrl url) @@ -127,8 +127,8 @@ void AudioSourceCoreAudio::preDestroy() { ExtAudioFileDispose(m_audioFile); } -AudioSource::diff_type AudioSourceCoreAudio::seekSampleFrame( - diff_type frameIndex) { +SINT AudioSourceCoreAudio::seekSampleFrame( + SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); //_ThrowExceptionIfErr(@"ExtAudioFileSeek", err); @@ -139,13 +139,13 @@ AudioSource::diff_type AudioSourceCoreAudio::seekSampleFrame( return frameIndex; } -AudioSource::size_type AudioSourceCoreAudio::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { +SINT AudioSourceCoreAudio::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { //if (!m_decoder) return 0; - size_type numFramesRead = 0; + SINT numFramesRead = 0; while (numFramesRead < numberOfFrames) { - size_type numFramesToRead = numberOfFrames - numFramesRead; + SINT numFramesToRead = numberOfFrames - numFramesRead; AudioBufferList fillBufList; fillBufList.mNumberBuffers = 1; diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index dfa8f2c0791..03df23fc9a7 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -26,8 +26,8 @@ class AudioSourceCoreAudio: public AudioSource { ~AudioSourceCoreAudio(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, + SINT seekSampleFrame(SINT frameIndex) /*override*/; + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 51ea3f39469..fdd0cb53b59 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -400,10 +400,10 @@ bool AudioSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, return false; } -AudioSource::diff_type AudioSourceFFmpeg::seekSampleFrame(diff_type frameIndex) { +SINT AudioSourceFFmpeg::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - const diff_type filepos = frames2samples(frameIndex); + const SINT filepos = frames2samples(frameIndex); int ret = 0; qint64 i = 0; @@ -484,7 +484,7 @@ unsigned int AudioSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { return size; } -AudioSource::size_type AudioSourceFFmpeg::readSampleFrames(size_type numberOfFrames, +SINT AudioSourceFFmpeg::readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) { // This is just a hack that simply reuses existing // functionality. Sample data should be resampled @@ -492,8 +492,8 @@ AudioSource::size_type AudioSourceFFmpeg::readSampleFrames(size_type numberOfFra // AV_SAMPLE_FMT_S16! typedef std::vector TempBuffer; TempBuffer tempBuffer(frames2samples(numberOfFrames)); - const size_type readSamples = read(tempBuffer.size(), &tempBuffer[0]); - for (size_type i = 0; i < readSamples; ++i) { + const SINT readSamples = read(tempBuffer.size(), &tempBuffer[0]); + for (SINT i = 0; i < readSamples; ++i) { sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / CSAMPLE(SAMPLE_MAX); } return samples2frames(readSamples); diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index 8edd1536856..437974abc6d 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -46,9 +46,9 @@ class AudioSourceFFmpeg : public AudioSource { ~AudioSourceFFmpeg(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: explicit AudioSourceFFmpeg(QUrl url); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index d64b870ce35..9f48d5db89a 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -120,8 +120,8 @@ void AudioSourceFLAC::preDestroy() { m_file.close(); } -Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame( - diff_type frameIndex) { +SINT AudioSourceFLAC::seekSampleFrame( + SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); @@ -141,29 +141,29 @@ Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame( return m_curFrameIndex; } -Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { +SINT AudioSourceFLAC::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } -Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFramesStereo( - size_type numberOfFrames, CSAMPLE* sampleBuffer, - size_type sampleBufferSize) { +SINT AudioSourceFLAC::readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize) { return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } -Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer, - size_type sampleBufferSize, bool readStereoSamples) { +SINT AudioSourceFLAC::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); - const size_type numberOfFramesTotal = numberOfFrames; + const SINT numberOfFramesTotal = numberOfFrames; CSAMPLE* outBuffer = sampleBuffer; - size_type numberOfFramesRemaining = numberOfFramesTotal; + SINT numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); @@ -188,13 +188,13 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( } DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - const size_type decodeBufferSamples = m_decodeSampleBufferWriteOffset + const SINT decodeBufferSamples = m_decodeSampleBufferWriteOffset - m_decodeSampleBufferReadOffset; - const size_type decodeBufferFrames = samples2frames( + const SINT decodeBufferFrames = samples2frames( decodeBufferSamples); - const size_type framesToCopy = + const SINT framesToCopy = math_min(decodeBufferFrames, numberOfFramesRemaining); - const size_type samplesToCopy = frames2samples(framesToCopy); + const SINT samplesToCopy = frames2samples(framesToCopy); if (readStereoSamples && !isChannelCountStereo()) { if (isChannelCountMono()) { SampleUtil::copyMonoToDualMono(outBuffer, @@ -341,7 +341,7 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { switch (metadata->type) { case FLAC__METADATA_TYPE_STREAMINFO: { - const size_type channelCount = metadata->data.stream_info.channels; + const SINT channelCount = metadata->data.stream_info.channels; DEBUG_ASSERT(kChannelCountDefault != channelCount); if (getChannelCount() == kChannelCountDefault) { // not set before @@ -353,7 +353,7 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { << channelCount << " <> " << getChannelCount(); } } - const size_type frameRate = metadata->data.stream_info.sample_rate; + const SINT frameRate = metadata->data.stream_info.sample_rate; DEBUG_ASSERT(kFrameRateDefault != frameRate); if (getFrameRate() == kFrameRateDefault) { // not set before @@ -365,7 +365,7 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { << frameRate << " <> " << getFrameRate(); } } - const size_type frameCount = metadata->data.stream_info.total_samples; + const SINT frameCount = metadata->data.stream_info.total_samples; DEBUG_ASSERT(kFrameCountDefault != frameCount); if (getFrameCount() == kFrameCountDefault) { // not set before diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 4df6eea7460..9c42104682b 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -16,12 +16,12 @@ class AudioSourceFLAC: public AudioSource { ~AudioSourceFLAC(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, - CSAMPLE* sampleBuffer, size_type sampleBufferSize) /*override*/; + SINT readSampleFramesStereo(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; // callback methods FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t* bytes); @@ -41,8 +41,8 @@ class AudioSourceFLAC: public AudioSource { void preDestroy(); - size_type readSampleFrames(size_type numberOfFrames, - CSAMPLE* sampleBuffer, size_type sampleBufferSize, + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize, bool readStereoSamples); QFile m_file; @@ -65,7 +65,7 @@ class AudioSourceFLAC: public AudioSource { int m_decodeSampleBufferReadOffset; int m_decodeSampleBufferWriteOffset; - diff_type m_curFrameIndex; + SINT m_curFrameIndex; }; } diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index e6807a3caef..191e84d8bf5 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -125,19 +125,19 @@ void AudioSourceModPlug::preDestroy() { } } -AudioSource::diff_type AudioSourceModPlug::seekSampleFrame( - diff_type frameIndex) { +SINT AudioSourceModPlug::seekSampleFrame( + SINT frameIndex) { return m_seekPos = frameIndex; } -AudioSource::size_type AudioSourceModPlug::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { - const size_type maxFrames = samples2frames(m_sampleBuf.size()); - const size_type readFrames = math_min(maxFrames - m_seekPos, numberOfFrames); +SINT AudioSourceModPlug::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + const SINT maxFrames = samples2frames(m_sampleBuf.size()); + const SINT readFrames = math_min(maxFrames - m_seekPos, numberOfFrames); - const size_type readSamples = frames2samples(readFrames); - const size_type readOffset = frames2samples(m_seekPos); - for (size_type i = 0; i < readSamples; ++i) { + const SINT readSamples = frames2samples(readFrames); + const SINT readOffset = frames2samples(m_seekPos); + for (SINT i = 0; i < readSamples; ++i) { sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) / CSAMPLE(SAMPLE_MAX); } diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index 8f272853a4b..4546a3add28 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -16,8 +16,8 @@ namespace Mixxx { // in RAM to allow seeking and smooth operation in Mixxx. class AudioSourceModPlug: public AudioSource { public: - static const size_type kChannelCount = 2; // always stereo - static const size_type kFrameRate = 44100; // always 44.1kHz + static const SINT kChannelCount = 2; // always stereo + static const SINT kFrameRate = 44100; // always 44.1kHz // apply settings for decoding static void configure(unsigned int bufferSizeLimit, @@ -27,9 +27,9 @@ class AudioSourceModPlug: public AudioSource { ~AudioSourceModPlug(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: @@ -42,8 +42,8 @@ class AudioSourceModPlug: public AudioSource { void preDestroy(); ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor - unsigned long m_fileLength; // length of file in samples - unsigned long m_seekPos; // current read position + SINT m_fileLength; // length of file in samples + SINT m_seekPos; // current read position QByteArray m_fileBuf; // original module file data std::vector m_sampleBuf; // 16bit stereo samples, 44.1kHz }; diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 84cb78c0a0d..6089ade5ae9 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -11,7 +11,7 @@ namespace { // In the worst case up to 29 MP3 frames need to be prefetched // for accurate seeking: // http://www.mars.org/mailman/public/mad-dev/2002-May/000634.html -const AudioSource::size_type kSeekFramePrefetchCount = 29; +const SINT kSeekFramePrefetchCount = 29; const CSAMPLE kMadScale = AudioSource::kSampleValuePeak / CSAMPLE(MAD_F_ONE); @@ -22,10 +22,10 @@ inline CSAMPLE madScale(mad_fixed_t sample) { } // anonymous namespace // Optimization: Reserve initial capacity for seek frame list -const AudioSource::size_type kMinutesPerFile = 10; // enough for the majority of files (tunable) -const AudioSource::size_type kSecondsPerMinute = 60; // fixed -const AudioSource::size_type kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 -const AudioSource::size_type kSeekFrameListCapacity = kMinutesPerFile +const SINT kMinutesPerFile = 10; // enough for the majority of files (tunable) +const SINT kSecondsPerMinute = 60; // fixed +const SINT kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 +const SINT kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; int decodeFrameHeader( @@ -121,7 +121,7 @@ Result AudioSourceMp3::postConstruct() { } // Grab data from madHeader - const size_type madChannelCount = MAD_NCHANNELS(&madHeader); + const SINT madChannelCount = MAD_NCHANNELS(&madHeader); if (kChannelCountDefault == getChannelCount()) { // initially set the number of channels setChannelCount(madChannelCount); @@ -137,7 +137,7 @@ Result AudioSourceMp3::postConstruct() { return ERR; } } - const size_type madSampleRate = madHeader.samplerate; + const SINT madSampleRate = madHeader.samplerate; if (kFrameRateDefault == getFrameRate()) { // initially set the frame/sample rate setFrameRate(madSampleRate); @@ -257,7 +257,7 @@ void AudioSourceMp3::preDestroy() { } } -AudioSource::diff_type AudioSourceMp3::restartDecoding( +SINT AudioSourceMp3::restartDecoding( const SeekFrameType& seekFrame) { qDebug() << "restartDecoding @" << seekFrame.frameIndex; @@ -297,7 +297,7 @@ AudioSource::diff_type AudioSourceMp3::restartDecoding( } void AudioSourceMp3::addSeekFrame( - diff_type frameIndex, + SINT frameIndex, const unsigned char* pInputData) { DEBUG_ASSERT(m_seekFrameList.empty() || (m_seekFrameList.back().frameIndex < frameIndex)); @@ -310,22 +310,22 @@ void AudioSourceMp3::addSeekFrame( m_seekFrameList.push_back(seekFrame); } -AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( - diff_type frameIndex) const { +SINT AudioSourceMp3::findSeekFrameIndex( + SINT frameIndex) const { // Check preconditions DEBUG_ASSERT(0 < m_avgSeekFrameCount); DEBUG_ASSERT(!m_seekFrameList.empty()); DEBUG_ASSERT(kFrameIndexMin == m_seekFrameList.front().frameIndex); - DEBUG_ASSERT(diff_type(kFrameIndexMin + getFrameIndexMax()) == m_seekFrameList.back().frameIndex); + DEBUG_ASSERT(SINT(kFrameIndexMin + getFrameIndexMax()) == m_seekFrameList.back().frameIndex); - SeekFrameList::size_type lowerBound = + SINT lowerBound = 0; - SeekFrameList::size_type upperBound = + SINT upperBound = m_seekFrameList.size(); DEBUG_ASSERT(lowerBound < upperBound); // Initial guess based on average frame size - SeekFrameList::size_type seekFrameIndex = + SINT seekFrameIndex = frameIndex / m_avgSeekFrameCount; if (seekFrameIndex >= upperBound) { seekFrameIndex = upperBound - 1; @@ -346,24 +346,24 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( // Check postconditions DEBUG_ASSERT(seekFrameIndex == lowerBound); - DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); + DEBUG_ASSERT(SINT(m_seekFrameList.size()) > seekFrameIndex); DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); - DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || + DEBUG_ASSERT(((seekFrameIndex + 1) >= SINT(m_seekFrameList.size())) || (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); return seekFrameIndex; } -AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { +SINT AudioSourceMp3::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - SeekFrameList::size_type seekFrameIndex = findSeekFrameIndex( + SINT seekFrameIndex = findSeekFrameIndex( frameIndex); - DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); - const SeekFrameList::size_type curSeekFrameIndex = findSeekFrameIndex( + DEBUG_ASSERT(SINT(m_seekFrameList.size()) > seekFrameIndex); + const SINT curSeekFrameIndex = findSeekFrameIndex( m_curFrameIndex); - DEBUG_ASSERT(m_seekFrameList.size() > curSeekFrameIndex); + DEBUG_ASSERT(SINT(m_seekFrameList.size()) > curSeekFrameIndex); // some consistency checks DEBUG_ASSERT((curSeekFrameIndex >= seekFrameIndex) || (m_curFrameIndex < frameIndex)); DEBUG_ASSERT((curSeekFrameIndex <= seekFrameIndex) || (m_curFrameIndex > frameIndex)); @@ -375,7 +375,7 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { m_madSynthCount = 0; // Adjust the seek frame index for prefetching - // Implementation note: The type size_type is unsigned so + // Implementation note: The type SINT is unsigned so // need to be careful when subtracting! if (kSeekFramePrefetchCount < seekFrameIndex) { // Restart decoding kSeekFramePrefetchCount seek frames @@ -398,7 +398,7 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { DEBUG_ASSERT(m_curFrameIndex <= frameIndex); // Skip (= decode and discard) prefetch data - const size_type skipFrameCount = frameIndex - m_curFrameIndex; + const SINT skipFrameCount = frameIndex - m_curFrameIndex; skipSampleFrames(skipFrameCount); DEBUG_ASSERT(m_curFrameIndex == frameIndex); @@ -406,32 +406,32 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { return m_curFrameIndex; } -AudioSource::size_type AudioSourceMp3::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { +SINT AudioSourceMp3::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } -AudioSource::size_type AudioSourceMp3::readSampleFramesStereo( - size_type numberOfFrames, CSAMPLE* sampleBuffer, - size_type sampleBufferSize) { +SINT AudioSourceMp3::readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize) { return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } -AudioSource::size_type AudioSourceMp3::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer, - size_type sampleBufferSize, bool readStereoSamples) { +SINT AudioSourceMp3::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); - const size_type numberOfFramesTotal = math_min(numberOfFrames, - size_type(getFrameIndexMax() - m_curFrameIndex)); + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); CSAMPLE* pSampleBuffer = sampleBuffer; - size_type numberOfFramesRemaining = numberOfFramesTotal; + SINT numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { if (0 >= m_madSynthCount) { // When all decoded output data has been consumed... @@ -487,15 +487,15 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( DEBUG_ASSERT(0 < m_madSynthCount); } - const size_type synthReadCount = math_min( + const SINT synthReadCount = math_min( m_madSynthCount, numberOfFramesRemaining); if (NULL != pSampleBuffer) { DEBUG_ASSERT(m_madSynthCount <= m_madSynth.pcm.length); - const size_type madSynthOffset = + const SINT madSynthOffset = m_madSynth.pcm.length - m_madSynthCount; DEBUG_ASSERT(madSynthOffset < m_madSynth.pcm.length); if (isChannelCountMono()) { - for (size_type i = 0; i < synthReadCount; ++i) { + for (SINT i = 0; i < synthReadCount; ++i) { const CSAMPLE sampleValue = madScale( m_madSynth.pcm.samples[0][madSynthOffset + i]); *pSampleBuffer = sampleValue; @@ -506,7 +506,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( } } } else if (isChannelCountStereo() || readStereoSamples) { - for (size_type i = 0; i < synthReadCount; ++i) { + for (SINT i = 0; i < synthReadCount; ++i) { *pSampleBuffer = madScale( m_madSynth.pcm.samples[0][madSynthOffset + i]); ++pSampleBuffer; @@ -515,8 +515,8 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( ++pSampleBuffer; } } else { - for (size_type i = 0; i < synthReadCount; ++i) { - for (size_type j = 0; j < getChannelCount(); ++j) { + for (SINT i = 0; i < synthReadCount; ++i) { + for (SINT j = 0; j < getChannelCount(); ++j) { *pSampleBuffer = madScale( m_madSynth.pcm.samples[j][madSynthOffset + i]); ++pSampleBuffer; diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index c1ac6d0d96a..590295673f2 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -23,12 +23,12 @@ class AudioSourceMp3: public AudioSource { ~AudioSourceMp3(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, - CSAMPLE* sampleBuffer, size_type sampleBufferSize) /*override*/; + SINT readSampleFramesStereo(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; private: explicit AudioSourceMp3(QUrl url); @@ -37,8 +37,8 @@ class AudioSourceMp3: public AudioSource { void preDestroy(); - size_type readSampleFrames(size_type numberOfFrames, - CSAMPLE* sampleBuffer, size_type sampleBufferSize, + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize, bool readStereoSamples); QFile m_file; @@ -49,7 +49,7 @@ class AudioSourceMp3: public AudioSource { /** Struct used to store mad frames for seeking */ struct SeekFrameType { - diff_type frameIndex; + SINT frameIndex; const unsigned char* pInputData; }; @@ -60,21 +60,21 @@ class AudioSourceMp3: public AudioSource { */ typedef std::vector SeekFrameList; SeekFrameList m_seekFrameList; // ordered-by frameIndex - size_type m_avgSeekFrameCount; // avg. sample frames per MP3 frame + SINT m_avgSeekFrameCount; // avg. sample frames per MP3 frame - void addSeekFrame(diff_type frameIndex, const unsigned char* pInputData); + void addSeekFrame(SINT frameIndex, const unsigned char* pInputData); /** Returns the position in m_seekFrameList of the requested frame index. */ - SeekFrameList::size_type findSeekFrameIndex(diff_type frameIndex) const; + SINT findSeekFrameIndex(SINT frameIndex) const; - diff_type m_curFrameIndex; + SINT m_curFrameIndex; - diff_type restartDecoding(const SeekFrameType& seekFrame); + SINT restartDecoding(const SeekFrameType& seekFrame); // current play position mad_frame m_madFrame; mad_synth m_madSynth; - size_type m_madSynthCount; // left overs from the previous read + SINT m_madSynthCount; // left overs from the previous read }; } diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 6d17173eecc..e240965ea25 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -74,8 +74,8 @@ void AudioSourceOggVorbis::preDestroy() { } } -AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( - diff_type frameIndex) { +SINT AudioSourceOggVorbis::seekSampleFrame( + SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); @@ -97,30 +97,30 @@ AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( return m_curFrameIndex; } -AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { +SINT AudioSourceOggVorbis::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } -AudioSource::size_type AudioSourceOggVorbis::readSampleFramesStereo( - size_type numberOfFrames, CSAMPLE* sampleBuffer, - size_type sampleBufferSize) { +SINT AudioSourceOggVorbis::readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize) { return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } -AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer, - size_type sampleBufferSize, bool readStereoSamples) { +SINT AudioSourceOggVorbis::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); - const size_type numberOfFramesTotal = math_min(numberOfFrames, - size_type(getFrameIndexMax() - m_curFrameIndex)); + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); CSAMPLE* pSampleBuffer = sampleBuffer; - size_type numberOfFramesRemaining = numberOfFramesTotal; + SINT numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { float** pcmChannels; int currentSection; @@ -150,7 +150,7 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( } } else { for (long i = 0; i < readResult; ++i) { - for (size_type j = 0; j < getChannelCount(); ++j) { + for (SINT j = 0; j < getChannelCount(); ++j) { *pSampleBuffer++ = pcmChannels[j][i]; } } diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index dc28dd9df60..c1db19bece1 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -14,12 +14,12 @@ class AudioSourceOggVorbis: public AudioSource { ~AudioSourceOggVorbis(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, - CSAMPLE* sampleBuffer, size_type sampleBufferSize) /*override*/; + SINT readSampleFramesStereo(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; private: explicit AudioSourceOggVorbis(QUrl url); @@ -28,13 +28,13 @@ class AudioSourceOggVorbis: public AudioSource { void preDestroy(); - size_type readSampleFrames(size_type numberOfFrames, - CSAMPLE* sampleBuffer, size_type sampleBufferSize, + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize, bool readStereoSamples); OggVorbis_File m_vf; - diff_type m_curFrameIndex; + SINT m_curFrameIndex; }; } diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index c01b358be92..448dc23ab7a 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -15,7 +15,7 @@ const int kEntireStreamLink = -1; // get ... of the whole/entire stream } // anonymous namespace // Decoded output of opusfile has a fixed sample rate of 48 kHz -const AudioSource::size_type AudioSourceOpus::kFrameRate = 48000; +const SINT AudioSourceOpus::kFrameRate = 48000; AudioSourceOpus::AudioSourceOpus(QUrl url) : AudioSource(url), @@ -82,7 +82,7 @@ void AudioSourceOpus::preDestroy() { } } -AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { +SINT AudioSourceOpus::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); @@ -104,15 +104,15 @@ AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { return m_curFrameIndex; } -AudioSource::size_type AudioSourceOpus::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { +SINT AudioSourceOpus::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - const size_type numberOfFramesTotal = math_min(numberOfFrames, - size_type(getFrameIndexMax() - m_curFrameIndex)); + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); CSAMPLE* pSampleBuffer = sampleBuffer; - size_type numberOfFramesRemaining = numberOfFramesTotal; + SINT numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { int readResult = op_read_float(m_pOggOpusFile, pSampleBuffer, @@ -133,17 +133,17 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( return numberOfFramesTotal - numberOfFramesRemaining; } -AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( - size_type numberOfFrames, CSAMPLE* sampleBuffer, - size_type sampleBufferSize) { +SINT AudioSourceOpus::readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) <= sampleBufferSize); - const size_type numberOfFramesTotal = math_min(numberOfFrames, - size_type(getFrameIndexMax() - m_curFrameIndex)); + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); CSAMPLE* pSampleBuffer = sampleBuffer; - size_type numberOfFramesRemaining = numberOfFramesTotal; + SINT numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { int readResult = op_read_float_stereo(m_pOggOpusFile, pSampleBuffer, diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 1d6853b6282..e35a2181010 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -10,18 +10,18 @@ namespace Mixxx { class AudioSourceOpus: public AudioSource { public: - static const size_type kFrameRate; + static const SINT kFrameRate; static AudioSourcePointer create(QUrl url); ~AudioSourceOpus(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, - CSAMPLE* sampleBuffer, size_type sampleBufferSize) /*override*/; + SINT readSampleFramesStereo(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; private: explicit AudioSourceOpus(QUrl url); @@ -32,7 +32,7 @@ class AudioSourceOpus: public AudioSource { OggOpusFile *m_pOggOpusFile; - diff_type m_curFrameIndex; + SINT m_curFrameIndex; }; } diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index 21ad805487b..b9cacd026d0 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -58,8 +58,8 @@ void AudioSourceSndFile::preDestroy() { } } -AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( - diff_type frameIndex) { +SINT AudioSourceSndFile::seekSampleFrame( + SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); @@ -72,8 +72,8 @@ AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( } } -AudioSource::size_type AudioSourceSndFile::readSampleFrames( - size_type numberOfFrames, CSAMPLE* sampleBuffer) { +SINT AudioSourceSndFile::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { const sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); if (0 <= readCount) { diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 5fba71e5341..2a622f7556b 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -19,9 +19,9 @@ class AudioSourceSndFile: public AudioSource { ~AudioSourceSndFile(); - diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 99745637c37..91b726a437e 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -45,9 +45,9 @@ TEST_F(SoundSourceProxyTest, open) { Mixxx::AudioSourcePointer pAudioSource(openAudioSource(filePath)); ASSERT_TRUE(!pAudioSource.isNull()); - EXPECT_LT(0UL, pAudioSource->getChannelCount()); - EXPECT_LT(0UL, pAudioSource->getFrameRate()); - EXPECT_LT(0UL, pAudioSource->getFrameCount()); + EXPECT_LT(0, pAudioSource->getChannelCount()); + EXPECT_LT(0, pAudioSource->getFrameRate()); + EXPECT_LT(0, pAudioSource->getFrameCount()); } } @@ -72,7 +72,7 @@ TEST_F(SoundSourceProxyTest, TOAL_TPE2) { } TEST_F(SoundSourceProxyTest, seekForward) { - const Mixxx::AudioSource::size_type kReadFrameCount = 10000; + const SINT kReadFrameCount = 10000; // According to API documentation of op_pcm_seek(): // "...decoding after seeking may not return exactly the same @@ -95,15 +95,15 @@ TEST_F(SoundSourceProxyTest, seekForward) { Mixxx::AudioSourcePointer pContReadSource( openAudioSource(filePath)); ASSERT_FALSE(pContReadSource.isNull()); - const Mixxx::AudioSource::size_type readSampleCount = pContReadSource->frames2samples(kReadFrameCount); + const SINT readSampleCount = pContReadSource->frames2samples(kReadFrameCount); SampleBuffer contReadData(readSampleCount); SampleBuffer seekReadData(readSampleCount); - for (Mixxx::AudioSource::diff_type contFrameIndex = 0; + for (SINT contFrameIndex = 0; pContReadSource->isValidFrameIndex(contFrameIndex); contFrameIndex += kReadFrameCount) { - const Mixxx::AudioSource::size_type contReadFrameCount = + const SINT contReadFrameCount = pContReadSource->readSampleFrames(kReadFrameCount, &contReadData[0]); Mixxx::AudioSourcePointer pSeekReadSource( @@ -112,17 +112,17 @@ TEST_F(SoundSourceProxyTest, seekForward) { ASSERT_EQ(pContReadSource->getChannelCount(), pSeekReadSource->getChannelCount()); ASSERT_EQ(pContReadSource->getFrameCount(), pSeekReadSource->getFrameCount()); - const Mixxx::AudioSource::diff_type seekFrameIndex = + const SINT seekFrameIndex = pSeekReadSource->seekSampleFrame(contFrameIndex); ASSERT_EQ(contFrameIndex, seekFrameIndex); - const Mixxx::AudioSource::size_type seekReadFrameCount = + const SINT seekReadFrameCount = pSeekReadSource->readSampleFrames(kReadFrameCount, &seekReadData[0]); ASSERT_EQ(contReadFrameCount, seekReadFrameCount); - const Mixxx::AudioSource::size_type readSampleCount = + const SINT readSampleCount = pContReadSource->frames2samples(contReadFrameCount); - for (Mixxx::AudioSource::size_type readSampleOffset = 0; + for (SINT readSampleOffset = 0; readSampleOffset < readSampleCount; ++readSampleOffset) { if ("opus" == fileExtension) { From 2c5880100ac849fda4775ac6cc21cd8eb6125d2b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 27 Feb 2015 09:08:09 +0100 Subject: [PATCH 212/481] Update SampleBuffer/SINT --- src/samplebuffer.h | 35 ++++++++++++++++++++++++----------- src/util/types.h | 5 ++++- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/samplebuffer.h b/src/samplebuffer.h index 8222013507a..6b6415a6043 100644 --- a/src/samplebuffer.h +++ b/src/samplebuffer.h @@ -6,12 +6,12 @@ #include // std::swap // A sample buffer with properly aligned memory to enable SSE optimizations. -// The public SINTerface closely follows that of std::vector. +// After construction the content of the buffer is uninitialized. No resize +// operation is provided intentionally because malloc might block! // -// No resize operation is provided SINTentionally for maximum efficiency! -// If the size of an existing sample buffer needs to be altered after -// construction this can simply be achieved by swapping the contents with -// a temporary sample buffer that has been constructed with the desired +// Hint: If the size of an existing sample buffer ever needs to be altered +// after construction this can simply be achieved by swapping the contents +// with a temporary sample buffer that has been constructed with the desired // size: // // SampleBuffer sampleBuffer(oldSize); @@ -27,7 +27,6 @@ // ... // SampleBuffer(newSize).swap(sampleBuffer); // -// After construction the content of the buffer is uninitialized. class SampleBuffer { Q_DISABLE_COPY(SampleBuffer) ; @@ -40,11 +39,6 @@ class SampleBuffer { explicit SampleBuffer(SINT size); virtual ~SampleBuffer(); - void swap(SampleBuffer& other) { - std::swap(m_data, other.m_data); - std::swap(m_size, other.m_size); - } - SINT size() const { return m_size; } @@ -63,6 +57,15 @@ class SampleBuffer { return m_data[index]; } + // Exchanges the members of two buffers in conformance with the + // implementation of all STL containers. Required for exception + // safe programming and as a workaround for the missing resize + // operation. + void swap(SampleBuffer& other) { + std::swap(m_data, other.m_data); + std::swap(m_size, other.m_size); + } + // Fills the whole buffer with zeroes void clear(); @@ -74,4 +77,14 @@ class SampleBuffer { SINT m_size; }; +namespace std +{ +// Template specialization of std::swap for SampleBuffer. +template<> +inline void swap(SampleBuffer& lhs, SampleBuffer& rhs) +{ + lhs.swap(rhs); +} +} + #endif // SAMPLEBUFFER_H diff --git a/src/util/types.h b/src/util/types.h index f181c57fda5..3a27c1e6f17 100644 --- a/src/util/types.h +++ b/src/util/types.h @@ -6,8 +6,11 @@ #include #include -// Signed integer type for all kinds of sizes, array indices and pointer +// Signed integer type for POT array indices, sizes and pointer // arithmetic. Its size (32-/64-bit) depends on the CPU architecture. +// This should be used for all CSAMLE operations since it is fast and +// allows compiler auto vectorizing. For Qt container operations use +// just int as before. typedef std::ptrdiff_t SINT; // 16-bit integer sample data within the asymmetric From ca21d94521f21ba6be1138d7e0ed3601b36f3548 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 27 Feb 2015 16:27:47 +0100 Subject: [PATCH 213/481] Some minor changes --- src/sources/audiosource.cpp | 3 --- src/sources/audiosourcemp3.cpp | 9 +++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index b43f7a05ce1..f477f2051e8 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -1,10 +1,7 @@ #include "sources/audiosource.h" -#include "samplebuffer.h" #include "sampleutil.h" -#include - namespace Mixxx { /*static*/const CSAMPLE AudioSource::kSampleValueZero = diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 6089ade5ae9..2d879974bfb 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -252,16 +252,17 @@ void AudioSourceMp3::preDestroy() { if (NULL != m_pFileData) { m_file.unmap(m_pFileData); - m_file.close(); m_pFileData = NULL; } + + m_file.close(); } SINT AudioSourceMp3::restartDecoding( const SeekFrameType& seekFrame) { qDebug() << "restartDecoding @" << seekFrame.frameIndex; - if (0 == seekFrame.frameIndex) { + if (kFrameIndexMin == seekFrame.frameIndex) { mad_frame_finish(&m_madFrame); mad_synth_finish(&m_madSynth); } @@ -269,7 +270,7 @@ SINT AudioSourceMp3::restartDecoding( mad_stream_init(&m_madStream); mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - if (0 == seekFrame.frameIndex) { + if (kFrameIndexMin == seekFrame.frameIndex) { mad_synth_init(&m_madSynth); mad_frame_init(&m_madFrame); } @@ -278,7 +279,7 @@ SINT AudioSourceMp3::restartDecoding( mad_stream_buffer(&m_madStream, seekFrame.pInputData, m_fileSize - (seekFrame.pInputData - m_pFileData)); - if (0 < seekFrame.frameIndex) { + if (kFrameIndexMin < seekFrame.frameIndex) { // Muting is done here to eliminate potential pops/clicks // from skipping Rob Leslie explains why here: // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html From 647c53b2e0439120866dfa94c4e5722df0bdebfa Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 27 Feb 2015 20:49:09 +0100 Subject: [PATCH 214/481] Move code from specialized AudioSources back into corresponding SoundSources --- build/depends.py | 6 +- build/features.py | 9 +- plugins/soundsourcem4a/SConscript | 1 - plugins/soundsourcem4a/audiosourcem4a.cpp | 430 ------------- plugins/soundsourcem4a/audiosourcem4a.h | 69 --- plugins/soundsourcem4a/soundsourcem4a.cpp | 441 +++++++++++++- plugins/soundsourcem4a/soundsourcem4a.h | 66 +- plugins/soundsourcemediafoundation/SConscript | 2 +- .../audiosourcemediafoundation.cpp | 567 ------------------ .../audiosourcemediafoundation.h | 66 -- .../soundsourcemediafoundation.cpp | 554 ++++++++++++++++- .../soundsourcemediafoundation.h | 38 +- plugins/soundsourcewv/SConscript | 1 - plugins/soundsourcewv/audiosourcewv.cpp | 79 --- plugins/soundsourcewv/audiosourcewv.h | 41 -- plugins/soundsourcewv/soundsourcewv.cpp | 76 ++- plugins/soundsourcewv/soundsourcewv.h | 23 +- src/analyserqueue.cpp | 2 +- src/cachingreaderworker.cpp | 2 +- src/dlgprefmodplug.cpp | 4 +- src/library/coverartutils.h | 6 +- src/library/dao/trackdao.cpp | 7 +- src/musicbrainz/chromaprinter.cpp | 2 +- src/soundsourceproxy.cpp | 217 +++---- src/soundsourceproxy.h | 56 +- src/sources/audiosource.cpp | 11 - src/sources/audiosource.h | 4 - src/sources/audiosourcecoreaudio.cpp | 170 ------ src/sources/audiosourcecoreaudio.h | 48 -- src/sources/audiosourceffmpeg.cpp | 502 ---------------- src/sources/audiosourceffmpeg.h | 90 --- src/sources/audiosourceflac.cpp | 436 -------------- src/sources/audiosourceflac.h | 73 --- src/sources/audiosourcemodplug.cpp | 149 ----- src/sources/audiosourcemodplug.h | 53 -- src/sources/audiosourcemp3.cpp | 539 ----------------- src/sources/audiosourcemp3.h | 82 --- src/sources/audiosourceoggvorbis.cpp | 170 ------ src/sources/audiosourceoggvorbis.h | 42 -- src/sources/audiosourceopus.cpp | 167 ------ src/sources/audiosourceopus.h | 41 -- src/sources/audiosourcesndfile.cpp | 88 --- src/sources/audiosourcesndfile.h | 40 -- src/sources/metadatasource.h | 28 + src/sources/soundsource.cpp | 6 +- src/sources/soundsource.h | 45 +- src/sources/soundsourcecoreaudio.cpp | 159 ++++- src/sources/soundsourcecoreaudio.h | 34 +- src/sources/soundsourceffmpeg.cpp | 497 ++++++++++++++- src/sources/soundsourceffmpeg.h | 83 ++- src/sources/soundsourceflac.cpp | 434 +++++++++++++- src/sources/soundsourceflac.h | 67 ++- src/sources/soundsourcemodplug.cpp | 150 ++++- src/sources/soundsourcemodplug.h | 43 +- src/sources/soundsourcemp3.cpp | 546 ++++++++++++++++- src/sources/soundsourcemp3.h | 75 ++- src/sources/soundsourceoggvorbis.cpp | 167 +++++- src/sources/soundsourceoggvorbis.h | 35 +- src/sources/soundsourceopus.cpp | 186 +++++- src/sources/soundsourceopus.h | 33 +- src/sources/soundsourceplugin.h | 8 + src/sources/soundsourcesndfile.cpp | 87 ++- src/sources/soundsourcesndfile.h | 31 +- src/test/coverartcache_test.cpp | 4 +- src/test/soundproxy_test.cpp | 29 +- src/trackinfoobject.cpp | 11 +- 66 files changed, 3908 insertions(+), 4320 deletions(-) delete mode 100644 plugins/soundsourcem4a/audiosourcem4a.cpp delete mode 100644 plugins/soundsourcem4a/audiosourcem4a.h delete mode 100644 plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp delete mode 100644 plugins/soundsourcemediafoundation/audiosourcemediafoundation.h delete mode 100644 plugins/soundsourcewv/audiosourcewv.cpp delete mode 100644 plugins/soundsourcewv/audiosourcewv.h delete mode 100644 src/sources/audiosourcecoreaudio.cpp delete mode 100644 src/sources/audiosourcecoreaudio.h delete mode 100644 src/sources/audiosourceffmpeg.cpp delete mode 100644 src/sources/audiosourceffmpeg.h delete mode 100644 src/sources/audiosourceflac.cpp delete mode 100644 src/sources/audiosourceflac.h delete mode 100644 src/sources/audiosourcemodplug.cpp delete mode 100644 src/sources/audiosourcemodplug.h delete mode 100644 src/sources/audiosourcemp3.cpp delete mode 100644 src/sources/audiosourcemp3.h delete mode 100644 src/sources/audiosourceoggvorbis.cpp delete mode 100644 src/sources/audiosourceoggvorbis.h delete mode 100644 src/sources/audiosourceopus.cpp delete mode 100644 src/sources/audiosourceopus.h delete mode 100644 src/sources/audiosourcesndfile.cpp delete mode 100644 src/sources/audiosourcesndfile.h create mode 100644 src/sources/metadatasource.h diff --git a/build/depends.py b/build/depends.py index 1aba0183168..48c1be4effc 100644 --- a/build/depends.py +++ b/build/depends.py @@ -126,7 +126,7 @@ def configure(self, build, conf): 'Did not find libvorbisenc.a, libvorbisenc.lib, or the libvorbisenc development headers.') def sources(self, build): - return ['sources/audiosourceoggvorbis.cpp','sources/soundsourceoggvorbis.cpp'] + return ['sources/soundsourceoggvorbis.cpp'] class SndFile(Dependence): @@ -139,7 +139,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='__SNDFILE__') def sources(self, build): - return ['sources/audiosourcesndfile.cpp','sources/soundsourcesndfile.cpp'] + return ['sources/soundsourcesndfile.cpp'] class FLAC(Dependence): @@ -154,7 +154,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='FLAC__NO_DLL') def sources(self, build): - return ['sources/audiosourceflac.cpp','sources/soundsourceflac.cpp',] + return ['sources/soundsourceflac.cpp',] class Qt(Dependence): diff --git a/build/features.py b/build/features.py index f309882cae4..003bad43098 100644 --- a/build/features.py +++ b/build/features.py @@ -179,7 +179,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='__MAD__') def sources(self, build): - return ['sources/soundsourcemp3.cpp','sources/audiosourcemp3.cpp'] + return ['sources/soundsourcemp3.cpp'] class CoreAudio(Feature): @@ -214,7 +214,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='__COREAUDIO__') def sources(self, build): - return ['sources/soundsourcecoreaudio.cpp','sources/audiosourcecoreaudio.cpp', + return ['sources/soundsourcecoreaudio.cpp', '#lib/apple/CAStreamBasicDescription.cpp'] @@ -429,7 +429,7 @@ def configure(self, build, conf): def sources(self, build): depends.Qt.uic(build)('dlgprefmodplugdlg.ui') - return ['sources/soundsourcemodplug.cpp', 'sources/audiosourcemodplug.cpp', 'dlgprefmodplug.cpp'] + return ['sources/soundsourcemodplug.cpp', 'dlgprefmodplug.cpp'] class FAAD(Feature): @@ -819,7 +819,7 @@ def configure(self, build, conf): build.env.ParseConfig('pkg-config opusfile opus --silence-errors --cflags --libs') def sources(self, build): - return ['sources/soundsourceopus.cpp','sources/audiosourceopus.cpp'] + return ['sources/soundsourceopus.cpp'] class FFMPEG(Feature): @@ -948,7 +948,6 @@ def configure(self, build, conf): def sources(self, build): return ['sources/soundsourceffmpeg.cpp', - 'sources/audiosourceffmpeg.cpp', 'encoder/encoderffmpegresample.cpp', 'encoder/encoderffmpegcore.cpp', 'encoder/encoderffmpegmp3.cpp', diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index bc2625d0ef4..8c5893fc1ae 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -12,7 +12,6 @@ Import('build') m4a_sources = [ "soundsourcem4a.cpp", - "audiosourcem4a.cpp", "sources/soundsource.cpp", "sources/soundsourceplugin.cpp", "sources/audiosource.cpp", diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp deleted file mode 100644 index 86a9b098c4b..00000000000 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ /dev/null @@ -1,430 +0,0 @@ -#include "audiosourcem4a.h" - -#include "sampleutil.h" - -#ifdef __WINDOWS__ -#include -#include -#endif - -#ifdef _MSC_VER -#define S_ISDIR(mode) (mode & _S_IFDIR) -#define strcasecmp stricmp -#define strncasecmp strnicmp -#endif - -// TODO(XXX): Do we still need this "hack" on the supported platforms? -#ifdef __M4AHACK__ -typedef uint32_t SAMPLERATE_TYPE; -#else -typedef unsigned long SAMPLERATE_TYPE; -#endif - -namespace Mixxx { - -namespace { - -// According to the specification each AAC sample block decodes -// to 1024 sample frames. The rarely used AAC-960 profile with -// a block size of 960 frames is not supported. -const SINT kFramesPerSampleBlock = 1024; - -// MP4SampleId is 1-based -const MP4SampleId kSampleBlockIdMin = 1; - -inline SINT getFrameIndexForSampleBlockId( - MP4SampleId sampleBlockId) { - return AudioSource::kFrameIndexMin + - (sampleBlockId - kSampleBlockIdMin) * kFramesPerSampleBlock; -} - -inline SINT getFrameCountForSampleBlockId( - MP4SampleId sampleBlockId) { - return ((sampleBlockId - kSampleBlockIdMin) + 1) * kFramesPerSampleBlock; -} - -// Decoding will be restarted one or more blocks of samples -// before the actual position after seeking randomly in the -// audio stream to avoid audible glitches. -// -// "AAC Audio - Encoder Delay and Synchronization: The 2112 Sample Assumption" -// https://developer.apple.com/library/ios/technotes/tn2258/_index.html -// "It must also be assumed that without an explicit value, the playback -// system will trim 2112 samples from the AAC decoder output when starting -// playback from any point in the bistream." -// -// 2112 frames = 2 * 1024 + 64 < 3 * 1024 = 3 sample blocks -// -// Decoding 3 blocks of samples in advance after seeking should -// compensate for the encoding delay and allow sample accurate -// seeking. -const MP4SampleId kNumberOfPrefetchSampleBlocks = 3; - -// Searches for the first audio track in the MP4 file that -// suits our needs. -MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { - const MP4TrackId maxTrackId = MP4GetNumberOfTracks(hFile, NULL, 0); - for (MP4TrackId trackId = 1; trackId <= maxTrackId; ++trackId) { - const char* trackType = MP4GetTrackType(hFile, trackId); - if ((NULL == trackType) || !MP4_IS_AUDIO_TRACK_TYPE(trackType)) { - continue; - } - const char* mediaDataName = MP4GetTrackMediaDataName(hFile, trackId); - if (0 != strcasecmp(mediaDataName, "mp4a")) { - continue; - } - const u_int8_t audioType = MP4GetTrackEsdsObjectTypeId(hFile, trackId); - if (MP4_INVALID_AUDIO_TYPE == audioType) { - continue; - } - if (MP4_MPEG4_AUDIO_TYPE == audioType) { - const u_int8_t audioMpeg4Type = MP4GetTrackAudioMpeg4Type(hFile, - trackId); - if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(audioMpeg4Type)) { - return trackId; - } - } else if (MP4_IS_AAC_AUDIO_TYPE(audioType)) { - return trackId; - } - } - return MP4_INVALID_TRACK_ID; -} - -} - -AudioSourceM4A::AudioSourceM4A(QUrl url) - : AudioSource(url), - m_hFile(MP4_INVALID_FILE_HANDLE), - m_trackId(MP4_INVALID_TRACK_ID), - m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), - m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), - m_inputBufferOffset(0), - m_inputBufferLength(0), - m_hDecoder(NULL), - m_decodeSampleBufferReadOffset(0), - m_decodeSampleBufferWriteOffset(0), - m_curFrameIndex(kFrameIndexMin) { -} - -AudioSourceM4A::~AudioSourceM4A() { - preDestroy(); -} - -AudioSourcePointer AudioSourceM4A::create(QUrl url) { - return onCreate(new AudioSourceM4A(url)); -} - -Result AudioSourceM4A::postConstruct() { - /* open MP4 file, check for >= ver 1.9.1 */ -#if MP4V2_PROJECT_version_hex <= 0x00010901 - m_hFile = MP4Read(getLocalFileNameBytes().constData(), 0); -#else - m_hFile = MP4Read(getLocalFileNameBytes().constData()); -#endif - if (MP4_INVALID_FILE_HANDLE == m_hFile) { - qWarning() << "Failed to open file for reading:" << getUrl(); - return ERR; - } - - m_trackId = findFirstAudioTrackId(m_hFile); - if (MP4_INVALID_TRACK_ID == m_trackId) { - qWarning() << "No AAC track found:" << getUrl(); - return ERR; - } - - m_maxSampleBlockId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); - if (MP4_INVALID_SAMPLE_ID == m_maxSampleBlockId) { - qWarning() << "Failed to read file structure:" << getUrl(); - return ERR; - } - - // Determine the maximum input size (in bytes) of a - // sample block for the selected track. - const u_int32_t maxSampleBlockInputSize = MP4GetTrackMaxSampleSize(m_hFile, - m_trackId); - m_inputBuffer.resize(maxSampleBlockInputSize, 0); - - DEBUG_ASSERT(NULL == m_hDecoder); // not already opened - m_hDecoder = NeAACDecOpen(); - if (!m_hDecoder) { - qWarning() << "Failed to open the AAC decoder!"; - return ERR; - } - NeAACDecConfigurationPtr pDecoderConfig = NeAACDecGetCurrentConfiguration( - m_hDecoder); - pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ - pDecoderConfig->downMatrix = 1; /* 5.1 -> stereo */ - pDecoderConfig->defObjectType = LC; - if (!NeAACDecSetConfiguration(m_hDecoder, pDecoderConfig)) { - qWarning() << "Failed to configure AAC decoder!"; - return ERR; - } - - u_int8_t* configBuffer = NULL; - u_int32_t configBufferSize = 0; - if (!MP4GetTrackESConfiguration(m_hFile, m_trackId, &configBuffer, - &configBufferSize)) { - /* failed to get mpeg-4 audio config... this is ok. - * NeAACDecInit2() will simply use default values instead. - */ - qWarning() << "Failed to read the MP4 audio configuration." - << "Continuing with default values."; - } - - SAMPLERATE_TYPE sampleRate; - unsigned char channelCount; - if (0 > NeAACDecInit2(m_hDecoder, configBuffer, configBufferSize, - &sampleRate, &channelCount)) { - free(configBuffer); - qWarning() << "Failed to initialize the AAC decoder!"; - return ERR; - } else { - free(configBuffer); - } - - setChannelCount(channelCount); - setFrameRate(sampleRate); - setFrameCount(getFrameCountForSampleBlockId(m_maxSampleBlockId)); - - // Resize temporary buffer for decoded sample data - const SINT decodeSampleBufferSize = - frames2samples(kFramesPerSampleBlock); - SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); - - // Invalidate current position to enforce the following - // seek operation - m_curFrameIndex = getFrameIndexMax(); - - // (Re-)Start decoding at the beginning of the file - seekSampleFrame(kFrameIndexMin); - - return OK; -} - -void AudioSourceM4A::preDestroy() { - if (m_hDecoder) { - NeAACDecClose(m_hDecoder); - m_hDecoder = NULL; - } - if (MP4_INVALID_FILE_HANDLE != m_hFile) { - MP4Close(m_hFile); - m_hFile = MP4_INVALID_FILE_HANDLE; - } -} - -bool AudioSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { - return (sampleBlockId >= kSampleBlockIdMin) - && (sampleBlockId <= m_maxSampleBlockId); -} - -void AudioSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { - DEBUG_ASSERT(MP4_INVALID_SAMPLE_ID != sampleBlockId); - - NeAACDecPostSeekReset(m_hDecoder, sampleBlockId); - m_curSampleBlockId = sampleBlockId; - m_curFrameIndex = getFrameIndexForSampleBlockId(m_curSampleBlockId); - // discard input buffer - m_inputBufferOffset = 0; - m_inputBufferLength = 0; - // discard previously decoded sample data - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; - -} - -SINT AudioSourceM4A::seekSampleFrame(SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - - if (m_curFrameIndex != frameIndex) { - MP4SampleId sampleBlockId = kSampleBlockIdMin - + (frameIndex / kFramesPerSampleBlock); - DEBUG_ASSERT(isValidSampleBlockId(sampleBlockId)); - if ((frameIndex < m_curFrameIndex) || // seeking backwards? - !isValidSampleBlockId(m_curSampleBlockId) || // invalid seek position? - (sampleBlockId - > (m_curSampleBlockId + kNumberOfPrefetchSampleBlocks))) { // jumping forward? - // Restart decoding one or more blocks of samples backwards - // from the calculated starting block to avoid audible glitches. - // Implementation note: The type MP4SampleId is unsigned so we - // need to be careful when subtracting! - if ((kSampleBlockIdMin + kNumberOfPrefetchSampleBlocks) - < sampleBlockId) { - sampleBlockId -= kNumberOfPrefetchSampleBlocks; - } else { - sampleBlockId = kSampleBlockIdMin; - } - restartDecoding(sampleBlockId); - DEBUG_ASSERT(m_curSampleBlockId == sampleBlockId); - } - // decoding starts before the actual target position - DEBUG_ASSERT(m_curFrameIndex <= frameIndex); - // prefetch (decode and discard) all samples up to the target position - const SINT prefetchFrameCount = frameIndex - m_curFrameIndex; - const SINT skipFrameCount = - skipSampleFrames(prefetchFrameCount); - DEBUG_ASSERT(skipFrameCount <= prefetchFrameCount); - if (skipFrameCount != prefetchFrameCount) { - qWarning() << "Failed to skip over prefetched sample frames after seeking @" << m_curFrameIndex; - // Abort - return m_curFrameIndex; - } - } - DEBUG_ASSERT(m_curFrameIndex == frameIndex); - return m_curFrameIndex; -} - -SINT AudioSourceM4A::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - - const SINT numberOfFramesTotal = math_min(numberOfFrames, - SINT(getFrameIndexMax() - m_curFrameIndex)); - - CSAMPLE* pSampleBuffer = sampleBuffer; - SINT numberOfFramesRemaining = numberOfFramesTotal; - while (0 < numberOfFramesRemaining) { - DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= - m_decodeSampleBufferWriteOffset); - if (m_decodeSampleBufferReadOffset < m_decodeSampleBufferWriteOffset) { - // Consume previously decoded sample data - const SINT numberOfSamplesDecoded = - m_decodeSampleBufferWriteOffset - - m_decodeSampleBufferReadOffset; - const SINT numberOfFramesDecoded = - samples2frames(numberOfSamplesDecoded); - const SINT numberOfFramesRead = - math_min(numberOfFramesRemaining, numberOfFramesDecoded); - const SINT numberOfSamplesRead = - frames2samples(numberOfFramesRead); - if (pSampleBuffer) { - const CSAMPLE* const pDecodeBuffer = - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset]; - SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); - pSampleBuffer += numberOfSamplesRead; - m_decodeSampleBufferReadOffset += numberOfSamplesRead; - } - m_curFrameIndex += numberOfFramesRead; - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - numberOfFramesRemaining -= numberOfFramesRead; - if (0 == numberOfFramesRemaining) { - break; // exit loop - } - // All previously decoded sample data has been consumed - DEBUG_ASSERT(m_decodeSampleBufferReadOffset == m_decodeSampleBufferWriteOffset); - } - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; - - DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); - if (m_inputBufferOffset >= m_inputBufferLength) { - // reset input buffer - m_inputBufferOffset = 0; - m_inputBufferLength = 0; - if (isValidSampleBlockId(m_curSampleBlockId)) { - // fill input buffer with next block of samples - u_int8_t* pInputBuffer = &m_inputBuffer[0]; - u_int32_t inputBufferLength = m_inputBuffer.size(); // in/out parameter - if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleBlockId, - &pInputBuffer, &inputBufferLength, - NULL, NULL, NULL, NULL)) { - qWarning() - << "Failed to read MP4 input data for sample block" - << m_curSampleBlockId << "(" << "min =" - << kSampleBlockIdMin << "," << "max =" - << m_maxSampleBlockId << ")"; - break; // abort - } - ++m_curSampleBlockId; - m_inputBufferLength = inputBufferLength; - } - } - DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); - - // NOTE(uklotzde): The sample buffer for NeAACDecDecode2 has to - // be big enough for a whole block of decoded samples, which - // contains up to kFramesPerSampleBlock frames. Otherwise - // we need to use a temporary buffer. - CSAMPLE* pDecodeBuffer; // in/out parameter - SINT decodeBufferCapacityInBytes; - if (pSampleBuffer && (numberOfFramesRemaining >= kFramesPerSampleBlock)) { - // decode samples directly into sampleBuffer - pDecodeBuffer = pSampleBuffer; - decodeBufferCapacityInBytes = frames2samples( - numberOfFramesRemaining) * sizeof(*pSampleBuffer); - } else { - // decode next block of samples into temporary buffer - pDecodeBuffer = &m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset]; - const int decodeSampleBufferCapacity = - m_decodeSampleBuffer.size() - - m_decodeSampleBufferWriteOffset; - DEBUG_ASSERT(decodeSampleBufferCapacity >= - int(frames2samples(kFramesPerSampleBlock))); - decodeBufferCapacityInBytes = - decodeSampleBufferCapacity * - sizeof(*pDecodeBuffer); - } - - NeAACDecFrameInfo decFrameInfo; - void* pDecodeResult = NeAACDecDecode2(m_hDecoder, &decFrameInfo, - &m_inputBuffer[m_inputBufferOffset], - m_inputBufferLength - m_inputBufferOffset, - reinterpret_cast(&pDecodeBuffer), - decodeBufferCapacityInBytes); - // verify the decoding result - if (0 != decFrameInfo.error) { - qWarning() << "AAC decoding error:" - << decFrameInfo.error - << NeAACDecGetErrorMessage(decFrameInfo.error) - << getUrl(); - break; // abort - } - DEBUG_ASSERT(pDecodeResult == pDecodeBuffer); // verify the in/out parameter - - // verify the decoded sample data for consistency - if (getChannelCount() != decFrameInfo.channels) { - qWarning() << "Corrupt or unsupported AAC file:" - << "Unexpected number of channels" << decFrameInfo.channels - << "<>" << getChannelCount(); - break; // abort - } - if (getFrameRate() != SINT(decFrameInfo.samplerate)) { - qWarning() << "Corrupt or unsupported AAC file:" - << "Unexpected sample rate" << decFrameInfo.samplerate - << "<>" << getFrameRate(); - break; // abort - } - - // consume input data - m_inputBufferOffset += decFrameInfo.bytesconsumed; - - // consume decoded output data - const SINT numberOfSamplesDecoded = - decFrameInfo.samples; - const SINT numberOfFramesDecoded = - samples2frames(numberOfSamplesDecoded); - const SINT numberOfFramesRead = - math_min(numberOfFramesRemaining, numberOfFramesDecoded); - const SINT numberOfSamplesRead = - frames2samples(numberOfFramesRead); - if (pDecodeBuffer == pSampleBuffer) { - pSampleBuffer += numberOfSamplesRead; - } else { - m_decodeSampleBufferWriteOffset += numberOfSamplesDecoded; - if (pSampleBuffer) { - SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); - pSampleBuffer += numberOfSamplesRead; - } - m_decodeSampleBufferReadOffset += numberOfSamplesRead; - } - - m_curFrameIndex += numberOfFramesRead; - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - numberOfFramesRemaining -= numberOfFramesRead; - } - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); - return numberOfFramesTotal - numberOfFramesRemaining; -} - -} // namespace Mixxx diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h deleted file mode 100644 index 8473b8299a3..00000000000 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef AUDIOSOURCEM4A_H -#define AUDIOSOURCEM4A_H - -#include "sources/audiosource.h" -#include "samplebuffer.h" - -#ifdef __MP4V2__ -#include -#else -#include -#endif - -#include - -#include - -//As per QLibrary docs: http://doc.trolltech.com/4.6/qlibrary.html#resolve -#ifdef Q_OS_WIN -#define MY_EXPORT __declspec(dllexport) -#else -#define MY_EXPORT -#endif - -namespace Mixxx { - -class AudioSourceM4A: public AudioSource { -public: - static AudioSourcePointer create(QUrl url); - - ~AudioSourceM4A(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) /*override*/; - -private: - explicit AudioSourceM4A(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - bool isValidSampleBlockId(MP4SampleId sampleBlockId) const; - - void restartDecoding(MP4SampleId sampleBlockId); - - MP4FileHandle m_hFile; - MP4TrackId m_trackId; - MP4SampleId m_maxSampleBlockId; - MP4SampleId m_curSampleBlockId; - - typedef std::vector InputBuffer; - InputBuffer m_inputBuffer; - SINT m_inputBufferOffset; - SINT m_inputBufferLength; - - NeAACDecHandle m_hDecoder; - - SampleBuffer m_decodeSampleBuffer; - int m_decodeSampleBufferReadOffset; - int m_decodeSampleBufferWriteOffset; - - SINT m_curFrameIndex; -}; - -} - -#endif diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index db5e5f98b6a..00e045980b0 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -1,26 +1,96 @@ -/*************************************************************************** - soundsourcem4a.cpp - mp4/m4a decoder - ------------------- - copyright : (C) 2008 by Garth Dahlstrom - email : ironstorm@users.sf.net - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #include "soundsourcem4a.h" -#include "audiosourcem4a.h" -#include "metadata/trackmetadatataglib.h" +#include "sampleutil.h" + +#ifdef __WINDOWS__ +#include +#include +#endif + +#ifdef _MSC_VER +#define S_ISDIR(mode) (mode & _S_IFDIR) +#define strcasecmp stricmp +#endif + +// TODO(XXX): Do we still need this "hack" on the supported platforms? +#ifdef __M4AHACK__ +typedef uint32_t SAMPLERATE_TYPE; +#else +typedef unsigned long SAMPLERATE_TYPE; +#endif namespace Mixxx { +namespace { + +// According to the specification each AAC sample block decodes +// to 1024 sample frames. The rarely used AAC-960 profile with +// a block size of 960 frames is not supported. +const SINT kFramesPerSampleBlock = 1024; + +// MP4SampleId is 1-based +const MP4SampleId kSampleBlockIdMin = 1; + +inline SINT getFrameIndexForSampleBlockId( + MP4SampleId sampleBlockId) { + return AudioSource::kFrameIndexMin + + (sampleBlockId - kSampleBlockIdMin) * kFramesPerSampleBlock; +} + +inline SINT getFrameCountForSampleBlockId( + MP4SampleId sampleBlockId) { + return ((sampleBlockId - kSampleBlockIdMin) + 1) * kFramesPerSampleBlock; +} + +// Decoding will be restarted one or more blocks of samples +// before the actual position after seeking randomly in the +// audio stream to avoid audible glitches. +// +// "AAC Audio - Encoder Delay and Synchronization: The 2112 Sample Assumption" +// https://developer.apple.com/library/ios/technotes/tn2258/_index.html +// "It must also be assumed that without an explicit value, the playback +// system will trim 2112 samples from the AAC decoder output when starting +// playback from any point in the bistream." +// +// 2112 frames = 2 * 1024 + 64 < 3 * 1024 = 3 sample blocks +// +// Decoding 3 blocks of samples in advance after seeking should +// compensate for the encoding delay and allow sample accurate +// seeking. +const MP4SampleId kNumberOfPrefetchSampleBlocks = 3; + +// Searches for the first audio track in the MP4 file that +// suits our needs. +MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { + const MP4TrackId maxTrackId = MP4GetNumberOfTracks(hFile, NULL, 0); + for (MP4TrackId trackId = 1; trackId <= maxTrackId; ++trackId) { + const char* trackType = MP4GetTrackType(hFile, trackId); + if ((NULL == trackType) || !MP4_IS_AUDIO_TRACK_TYPE(trackType)) { + continue; + } + const char* mediaDataName = MP4GetTrackMediaDataName(hFile, trackId); + if (0 != strcasecmp(mediaDataName, "mp4a")) { + continue; + } + const u_int8_t audioType = MP4GetTrackEsdsObjectTypeId(hFile, trackId); + if (MP4_INVALID_AUDIO_TYPE == audioType) { + continue; + } + if (MP4_MPEG4_AUDIO_TYPE == audioType) { + const u_int8_t audioMpeg4Type = MP4GetTrackAudioMpeg4Type(hFile, + trackId); + if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(audioMpeg4Type)) { + return trackId; + } + } else if (MP4_IS_AAC_AUDIO_TYPE(audioType)) { + return trackId; + } + } + return MP4_INVALID_TRACK_ID; +} + +} // anonymous namespace + QList SoundSourceM4A::supportedFileExtensions() { QList list; list.push_back("m4a"); @@ -29,11 +99,340 @@ QList SoundSourceM4A::supportedFileExtensions() { } SoundSourceM4A::SoundSourceM4A(QUrl url) - : SoundSourcePlugin(url, "m4a") { + : SoundSourcePlugin(url, "m4a"), + m_hFile(MP4_INVALID_FILE_HANDLE), + m_trackId(MP4_INVALID_TRACK_ID), + m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_inputBufferOffset(0), + m_inputBufferLength(0), + m_hDecoder(NULL), + m_sampleBufferReadOffset(0), + m_sampleBufferWriteOffset(0), + m_curFrameIndex(kFrameIndexMin) { +} + +SoundSourceM4A::~SoundSourceM4A() { + close(); +} + +Result SoundSourceM4A::open() { + if (MP4_INVALID_FILE_HANDLE != m_hFile) { + qWarning() << "Cannot reopen M4A file:" << getUrl(); + return ERR; + } + + /* open MP4 file, check for >= ver 1.9.1 */ +#if MP4V2_PROJECT_version_hex <= 0x00010901 + m_hFile = MP4Read(getLocalFileNameBytes().constData(), 0); +#else + m_hFile = MP4Read(getLocalFileNameBytes().constData()); +#endif + if (MP4_INVALID_FILE_HANDLE == m_hFile) { + qWarning() << "Failed to open file for reading:" << getUrl(); + return ERR; + } + + m_trackId = findFirstAudioTrackId(m_hFile); + if (MP4_INVALID_TRACK_ID == m_trackId) { + qWarning() << "No AAC track found:" << getUrl(); + return ERR; + } + + m_maxSampleBlockId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); + if (MP4_INVALID_SAMPLE_ID == m_maxSampleBlockId) { + qWarning() << "Failed to read file structure:" << getUrl(); + return ERR; + } + + // Determine the maximum input size (in bytes) of a + // sample block for the selected track. + const u_int32_t maxSampleBlockInputSize = MP4GetTrackMaxSampleSize(m_hFile, + m_trackId); + m_inputBuffer.resize(maxSampleBlockInputSize, 0); + + DEBUG_ASSERT(NULL == m_hDecoder); // not already opened + m_hDecoder = NeAACDecOpen(); + if (!m_hDecoder) { + qWarning() << "Failed to open the AAC decoder!"; + return ERR; + } + NeAACDecConfigurationPtr pDecoderConfig = NeAACDecGetCurrentConfiguration( + m_hDecoder); + pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ + pDecoderConfig->downMatrix = 1; /* 5.1 -> stereo */ + pDecoderConfig->defObjectType = LC; + if (!NeAACDecSetConfiguration(m_hDecoder, pDecoderConfig)) { + qWarning() << "Failed to configure AAC decoder!"; + return ERR; + } + + u_int8_t* configBuffer = NULL; + u_int32_t configBufferSize = 0; + if (!MP4GetTrackESConfiguration(m_hFile, m_trackId, &configBuffer, + &configBufferSize)) { + /* failed to get mpeg-4 audio config... this is ok. + * NeAACDecInit2() will simply use default values instead. + */ + qWarning() << "Failed to read the MP4 audio configuration." + << "Continuing with default values."; + } + + SAMPLERATE_TYPE sampleRate; + unsigned char channelCount; + if (0 > NeAACDecInit2(m_hDecoder, configBuffer, configBufferSize, + &sampleRate, &channelCount)) { + free(configBuffer); + qWarning() << "Failed to initialize the AAC decoder!"; + return ERR; + } else { + free(configBuffer); + } + + setChannelCount(channelCount); + setFrameRate(sampleRate); + setFrameCount(getFrameCountForSampleBlockId(m_maxSampleBlockId)); + + // Resize temporary buffer for decoded sample data + const SINT sampleBufferSize = + frames2samples(kFramesPerSampleBlock); + SampleBuffer(sampleBufferSize).swap(m_sampleBuffer); + + // Invalidate current position to enforce the following + // seek operation + m_curFrameIndex = getFrameIndexMax(); + + // (Re-)Start decoding at the beginning of the file + seekSampleFrame(kFrameIndexMin); + + return OK; +} + +void SoundSourceM4A::close() { + if (m_hDecoder) { + NeAACDecClose(m_hDecoder); + m_hDecoder = NULL; + } + if (MP4_INVALID_FILE_HANDLE != m_hFile) { + MP4Close(m_hFile); + m_hFile = MP4_INVALID_FILE_HANDLE; + } + m_inputBuffer.clear(); +} + +bool SoundSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { + return (sampleBlockId >= kSampleBlockIdMin) + && (sampleBlockId <= m_maxSampleBlockId); +} + +void SoundSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { + DEBUG_ASSERT(MP4_INVALID_SAMPLE_ID != sampleBlockId); + + NeAACDecPostSeekReset(m_hDecoder, sampleBlockId); + m_curSampleBlockId = sampleBlockId; + m_curFrameIndex = getFrameIndexForSampleBlockId(m_curSampleBlockId); + // discard input buffer + m_inputBufferOffset = 0; + m_inputBufferLength = 0; + // discard previously decoded sample data + m_sampleBufferReadOffset = 0; + m_sampleBufferWriteOffset = 0; + } -Mixxx::AudioSourcePointer SoundSourceM4A::open() const { - return Mixxx::AudioSourceM4A::create(getUrl()); +SINT SoundSourceM4A::seekSampleFrame(SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + + if (m_curFrameIndex != frameIndex) { + MP4SampleId sampleBlockId = kSampleBlockIdMin + + (frameIndex / kFramesPerSampleBlock); + DEBUG_ASSERT(isValidSampleBlockId(sampleBlockId)); + if ((frameIndex < m_curFrameIndex) || // seeking backwards? + !isValidSampleBlockId(m_curSampleBlockId) || // invalid seek position? + (sampleBlockId + > (m_curSampleBlockId + kNumberOfPrefetchSampleBlocks))) { // jumping forward? + // Restart decoding one or more blocks of samples backwards + // from the calculated starting block to avoid audible glitches. + // Implementation note: The type MP4SampleId is unsigned so we + // need to be careful when subtracting! + if ((kSampleBlockIdMin + kNumberOfPrefetchSampleBlocks) + < sampleBlockId) { + sampleBlockId -= kNumberOfPrefetchSampleBlocks; + } else { + sampleBlockId = kSampleBlockIdMin; + } + restartDecoding(sampleBlockId); + DEBUG_ASSERT(m_curSampleBlockId == sampleBlockId); + } + // decoding starts before the actual target position + DEBUG_ASSERT(m_curFrameIndex <= frameIndex); + // prefetch (decode and discard) all samples up to the target position + const SINT prefetchFrameCount = frameIndex - m_curFrameIndex; + const SINT skipFrameCount = + skipSampleFrames(prefetchFrameCount); + DEBUG_ASSERT(skipFrameCount <= prefetchFrameCount); + if (skipFrameCount != prefetchFrameCount) { + qWarning() << "Failed to skip over prefetched sample frames after seeking @" << m_curFrameIndex; + // Abort + return m_curFrameIndex; + } + } + DEBUG_ASSERT(m_curFrameIndex == frameIndex); + return m_curFrameIndex; +} + +SINT SoundSourceM4A::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); + + CSAMPLE* pSampleBuffer = sampleBuffer; + SINT numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { + DEBUG_ASSERT(m_sampleBufferReadOffset <= + m_sampleBufferWriteOffset); + if (m_sampleBufferReadOffset < m_sampleBufferWriteOffset) { + // Consume previously decoded sample data + const SINT numberOfSamplesDecoded = + m_sampleBufferWriteOffset - + m_sampleBufferReadOffset; + const SINT numberOfFramesDecoded = + samples2frames(numberOfSamplesDecoded); + const SINT numberOfFramesRead = + math_min(numberOfFramesRemaining, numberOfFramesDecoded); + const SINT numberOfSamplesRead = + frames2samples(numberOfFramesRead); + if (pSampleBuffer) { + const CSAMPLE* const pDecodeBuffer = + &m_sampleBuffer[m_sampleBufferReadOffset]; + SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); + pSampleBuffer += numberOfSamplesRead; + m_sampleBufferReadOffset += numberOfSamplesRead; + } + m_curFrameIndex += numberOfFramesRead; + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + numberOfFramesRemaining -= numberOfFramesRead; + if (0 == numberOfFramesRemaining) { + break; // exit loop + } + // All previously decoded sample data has been consumed + DEBUG_ASSERT(m_sampleBufferReadOffset == m_sampleBufferWriteOffset); + } + m_sampleBufferReadOffset = 0; + m_sampleBufferWriteOffset = 0; + + DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); + if (m_inputBufferOffset >= m_inputBufferLength) { + // reset input buffer + m_inputBufferOffset = 0; + m_inputBufferLength = 0; + if (isValidSampleBlockId(m_curSampleBlockId)) { + // fill input buffer with next block of samples + u_int8_t* pInputBuffer = &m_inputBuffer[0]; + u_int32_t inputBufferLength = m_inputBuffer.size(); // in/out parameter + if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleBlockId, + &pInputBuffer, &inputBufferLength, + NULL, NULL, NULL, NULL)) { + qWarning() + << "Failed to read MP4 input data for sample block" + << m_curSampleBlockId << "(" << "min =" + << kSampleBlockIdMin << "," << "max =" + << m_maxSampleBlockId << ")"; + break; // abort + } + ++m_curSampleBlockId; + m_inputBufferLength = inputBufferLength; + } + } + DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); + + // NOTE(uklotzde): The sample buffer for NeAACDecDecode2 has to + // be big enough for a whole block of decoded samples, which + // contains up to kFramesPerSampleBlock frames. Otherwise + // we need to use a temporary buffer. + CSAMPLE* pDecodeBuffer; // in/out parameter + SINT decodeBufferCapacityInBytes; + if (pSampleBuffer && (numberOfFramesRemaining >= kFramesPerSampleBlock)) { + // decode samples directly into sampleBuffer + pDecodeBuffer = pSampleBuffer; + decodeBufferCapacityInBytes = frames2samples( + numberOfFramesRemaining) * sizeof(*pSampleBuffer); + } else { + // decode next block of samples into temporary buffer + pDecodeBuffer = &m_sampleBuffer[m_sampleBufferWriteOffset]; + const int sampleBufferCapacity = + m_sampleBuffer.size() - + m_sampleBufferWriteOffset; + DEBUG_ASSERT(sampleBufferCapacity >= + int(frames2samples(kFramesPerSampleBlock))); + decodeBufferCapacityInBytes = + sampleBufferCapacity * + sizeof(*pDecodeBuffer); + } + + NeAACDecFrameInfo decFrameInfo; + void* pDecodeResult = NeAACDecDecode2(m_hDecoder, &decFrameInfo, + &m_inputBuffer[m_inputBufferOffset], + m_inputBufferLength - m_inputBufferOffset, + reinterpret_cast(&pDecodeBuffer), + decodeBufferCapacityInBytes); + // verify the decoding result + if (0 != decFrameInfo.error) { + qWarning() << "AAC decoding error:" + << decFrameInfo.error + << NeAACDecGetErrorMessage(decFrameInfo.error) + << getUrl(); + break; // abort + } + DEBUG_ASSERT(pDecodeResult == pDecodeBuffer); // verify the in/out parameter + + // verify the decoded sample data for consistency + if (getChannelCount() != decFrameInfo.channels) { + qWarning() << "Corrupt or unsupported AAC file:" + << "Unexpected number of channels" << decFrameInfo.channels + << "<>" << getChannelCount(); + break; // abort + } + if (getFrameRate() != SINT(decFrameInfo.samplerate)) { + qWarning() << "Corrupt or unsupported AAC file:" + << "Unexpected sample rate" << decFrameInfo.samplerate + << "<>" << getFrameRate(); + break; // abort + } + + // consume input data + m_inputBufferOffset += decFrameInfo.bytesconsumed; + + // consume decoded output data + const SINT numberOfSamplesDecoded = + decFrameInfo.samples; + const SINT numberOfFramesDecoded = + samples2frames(numberOfSamplesDecoded); + const SINT numberOfFramesRead = + math_min(numberOfFramesRemaining, numberOfFramesDecoded); + const SINT numberOfSamplesRead = + frames2samples(numberOfFramesRead); + if (pDecodeBuffer == pSampleBuffer) { + pSampleBuffer += numberOfSamplesRead; + } else { + m_sampleBufferWriteOffset += numberOfSamplesDecoded; + if (pSampleBuffer) { + SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); + pSampleBuffer += numberOfSamplesRead; + } + m_sampleBufferReadOffset += numberOfSamplesRead; + } + + m_curFrameIndex += numberOfFramesRead; + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + numberOfFramesRemaining -= numberOfFramesRead; + } + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; } } // namespace Mixxx diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index e934788708a..8a4337ed75b 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -1,24 +1,18 @@ -/*************************************************************************** - soundsourcem4a.h - mp4/m4a decoder - ------------------- - copyright : (C) 2008 by Garth Dahlstrom - email : ironstorm@users.sf.net - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -#ifndef SOUNDSOURCEM4A_H -#define SOUNDSOURCEM4A_H +#ifndef MIXXX_SOUNDSOURCEM4A_H +#define MIXXX_SOUNDSOURCEM4A_H #include "sources/soundsourceplugin.h" -#include "defs_version.h" +#include "samplebuffer.h" + +#ifdef __MP4V2__ +#include +#else +#include +#endif + +#include + +#include //As per QLibrary docs: http://doc.trolltech.com/4.6/qlibrary.html#resolve #ifdef Q_OS_WIN @@ -34,8 +28,38 @@ class SoundSourceM4A: public SoundSourcePlugin { static QList supportedFileExtensions(); explicit SoundSourceM4A(QUrl url); + ~SoundSourceM4A(); + + Result open() /*override*/; + void close() /*override*/; + + SINT seekSampleFrame(SINT frameIndex) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; +private: + bool isValidSampleBlockId(MP4SampleId sampleBlockId) const; + + void restartDecoding(MP4SampleId sampleBlockId); + + MP4FileHandle m_hFile; + MP4TrackId m_trackId; + MP4SampleId m_maxSampleBlockId; + MP4SampleId m_curSampleBlockId; + + typedef std::vector InputBuffer; + InputBuffer m_inputBuffer; + SINT m_inputBufferOffset; + SINT m_inputBufferLength; + + NeAACDecHandle m_hDecoder; + + SampleBuffer m_sampleBuffer; + int m_sampleBufferReadOffset; + int m_sampleBufferWriteOffset; + + SINT m_curFrameIndex; }; } // namespace Mixxx @@ -46,4 +70,4 @@ extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName); extern "C" MY_EXPORT char** supportedFileExtensions(); extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions); -#endif +#endif // MIXXX_SOUNDSOURCEM4A_H diff --git a/plugins/soundsourcemediafoundation/SConscript b/plugins/soundsourcemediafoundation/SConscript index e58743b7393..d4d3bc41da4 100644 --- a/plugins/soundsourcemediafoundation/SConscript +++ b/plugins/soundsourcemediafoundation/SConscript @@ -18,7 +18,7 @@ if int(build.flags['mediafoundation']): else: env["LINKFLAGS"].remove("/subsystem:windows,5.01") ssmediafoundation_bin = env.SharedLibrary('soundsourcemediafoundation', - ['soundsourcemediafoundation.cpp', 'audiosourcemediafoundation.cpp', 'sources/soundsource.cpp', 'sources/audiosource.cpp', 'metadata/trackmetadata.cpp', 'metadata/trackmetadatataglib.cpp'], + ['soundsourcemediafoundation.cpp', 'sources/soundsource.cpp', 'sources/audiosource.cpp', 'metadata/trackmetadata.cpp', 'metadata/trackmetadatataglib.cpp'], LINKCOM = [env['LINKCOM'], 'mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;1']) Return("ssmediafoundation_bin") diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp deleted file mode 100644 index ef400bc0356..00000000000 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ /dev/null @@ -1,567 +0,0 @@ -// TODO(XXX): This implementation has been copied from the -// original file soundsourcemediafoundation.cpp and needs -// a major refactoring! Please refer to the helpful comments -// of daschuer in the following pull requests: -// https://github.com/mixxxdj/mixxx/pull/411 - -#include "audiosourcemediafoundation.h" - -#include -#include -#include -#include -#include - -#include - -namespace Mixxx { - -namespace -{ - -const bool sDebug = false; - -const int kNumChannels = 2; -const int kSampleRate = 44100; -const int kLeftoverSize = 4096; // in CSAMPLE's, this seems to be the size MF AAC -const int kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give - -/** - * Convert a 100ns Media Foundation value to a number of seconds. - */ -inline qreal secondsFromMF(qint64 mf) { - return static_cast(mf) / 1e7; -} - -/** - * Convert a number of seconds to a 100ns Media Foundation value. - */ -inline qint64 mfFromSeconds(qreal sec) { - return sec * 1e7; -} - -/** - * Convert a 100ns Media Foundation value to a frame offset. - */ -inline qint64 frameFromMF(qint64 mf) { - return static_cast(mf) * kSampleRate / 1e7; -} - -/** - * Convert a frame offset to a 100ns Media Foundation value. - */ -inline qint64 mfFromFrame(qint64 frame) { - return static_cast(frame) / kSampleRate * 1e7; -} - -/** Microsoft examples use this snippet often. */ -template static void safeRelease(T **ppT) { - if (*ppT) { - (*ppT)->Release(); - *ppT = NULL; - } -} - -} - -AudioSourceMediaFoundation::AudioSourceMediaFoundation(QUrl url) - : AudioSource(url), - m_hrCoInitialize(E_FAIL), - m_hrMFStartup(E_FAIL), - m_pReader(NULL), - m_pAudioType(NULL), - m_wcFilename(NULL), - m_nextFrame(0), - m_leftoverBuffer(NULL), - m_leftoverBufferSize(0), - m_leftoverBufferLength(0), - m_leftoverBufferPosition(0), - m_mfDuration(0), - m_iCurrentPosition(0), - m_dead(false), - m_seeking(false) { - - // these are always the same, might as well just stick them here - // -bkgood - // AudioSource properties - setChannelCount(kNumChannels); - setFrameRate(kSampleRate); - - // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for - // presentation descriptors, one of which MFSourceReader is not. - // Therefore, we calculate it ourselves, assuming 16 bits per sample - setBitrate(frames2samples(kBitsPerSampleForBitrate * kSampleRate) / 1000); -} - -AudioSourceMediaFoundation::~AudioSourceMediaFoundation() { - preDestroy(); -} - -AudioSourcePointer AudioSourceMediaFoundation::create(QUrl url) { - return onCreate(new AudioSourceMediaFoundation(url)); -} - -Result AudioSourceMediaFoundation::postConstruct() { - const QString fileName(getLocalFileName()); - - if (sDebug) { - qDebug() << "open()" << fileName; - } - - // Initialize the COM library. - m_hrCoInitialize = CoInitializeEx(NULL, - COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); - if (FAILED(m_hrCoInitialize)) { - qWarning() << "SSMF: failed to initialize COM"; - return ERR; - } - // Initialize the Media Foundation platform. - m_hrMFStartup = MFStartup(MF_VERSION); - if (FAILED(m_hrCoInitialize)) { - qWarning() << "SSMF: failed to initialize Media Foundation"; - return ERR; - } - - QString qurlStr(fileName); - // http://social.msdn.microsoft.com/Forums/en/netfxbcl/thread/35c6a451-3507-40c8-9d1c-8d4edde7c0cc - // gives maximum path + file length as 248 + 260, using that -bkgood - m_wcFilename = new wchar_t[248 + 260]; - int wcFilenameLength(fileName.toWCharArray(m_wcFilename)); - // toWCharArray does not append a null terminator to the string! - m_wcFilename[wcFilenameLength] = '\0'; - - // Create the source reader to read the input file. - HRESULT hr = MFCreateSourceReaderFromURL(m_wcFilename, NULL, &m_pReader); - if (FAILED(hr)) { - qWarning() << "SSMF: Error opening input file:" << fileName; - return ERR; - } - - if (!configureAudioStream()) { - qWarning() << "SSMF: Error configuring audio stream."; - return ERR; - } - - //Seek to position 0, which forces us to skip over all the header frames. - //This makes sure we're ready to just let the Analyser rip and it'll - //get the number of samples it expects (ie. no header frames). - seekSampleFrame(0); - - return OK; -} - -void AudioSourceMediaFoundation::preDestroy() { - delete[] m_wcFilename; - m_wcFilename = NULL; - delete[] m_leftoverBuffer; - m_leftoverBuffer = NULL; - - safeRelease(&m_pReader); - safeRelease(&m_pAudioType); - - if (SUCCEEDED(m_hrMFStartup)) { - MFShutdown(); - m_hrMFStartup = E_FAIL; - } - if (SUCCEEDED(m_hrCoInitialize)) { - CoUninitialize(); - m_hrCoInitialize = E_FAIL; - } -} - -SINT AudioSourceMediaFoundation::seekSampleFrame( - SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - if (sDebug) { - qDebug() << "seekSampleFrame()" << frameIndex; - } - qint64 mfSeekTarget(mfFromFrame(frameIndex) - 1); - // minus 1 here seems to make our seeking work properly, otherwise we will - // (more often than not, maybe always) seek a bit too far (although not - // enough for our calculatedFrameFromMF <= nextFrame assertion in ::read). - // Has something to do with 100ns MF units being much smaller than most - // frame offsets (in seconds) -bkgood - long result = m_iCurrentPosition; - if (m_dead) { - return result; - } - - PROPVARIANT prop; - HRESULT hr; - - // this doesn't fail, see MS's implementation - hr = InitPropVariantFromInt64(mfSeekTarget < 0 ? 0 : mfSeekTarget, &prop); - - hr = m_pReader->Flush(MF_SOURCE_READER_FIRST_AUDIO_STREAM); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to flush before seek"; - } - - // http://msdn.microsoft.com/en-us/library/dd374668(v=VS.85).aspx - hr = m_pReader->SetCurrentPosition(GUID_NULL, prop); - if (FAILED(hr)) { - // nothing we can do here as we can't fail (no facility to other than - // crashing mixxx) - qWarning() << "SSMF: failed to seek" - << (hr == MF_E_INVALIDREQUEST ? - "Sample requests still pending" : ""); - } else { - result = frameIndex; - } - PropVariantClear(&prop); - - // record the next frame so that we can make sure we're there the next - // time we get a buffer from MFSourceReader - m_nextFrame = frameIndex; - m_seeking = true; - m_iCurrentPosition = result; - return result; -} - -SINT AudioSourceMediaFoundation::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - if (sDebug) { - qDebug() << "read()" << numberOfFrames; - } - SINT framesNeeded(numberOfFrames); - - // first, copy frames from leftover buffer IF the leftover buffer is at - // the correct frame - if (m_leftoverBufferLength > 0 && m_leftoverBufferPosition == m_nextFrame) { - copyFrames(sampleBuffer, &framesNeeded, m_leftoverBuffer, - m_leftoverBufferLength); - if (m_leftoverBufferLength > 0) { - if (framesNeeded != 0) { - qWarning() << __FILE__ << __LINE__ - << "WARNING: Expected frames needed to be 0. Abandoning this file."; - m_dead = true; - } - m_leftoverBufferPosition += numberOfFrames; - } - } else { - // leftoverBuffer already empty or in the wrong position, clear it - m_leftoverBufferLength = 0; - } - - while (!m_dead && framesNeeded > 0) { - HRESULT hr(S_OK); - DWORD dwFlags(0); - qint64 timestamp(0); - IMFSample *pSample(NULL); - bool error(false); // set to true to break after releasing - - hr = m_pReader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, // [in] DWORD dwStreamIndex, - 0, // [in] DWORD dwControlFlags, - NULL, // [out] DWORD *pdwActualStreamIndex, - &dwFlags, // [out] DWORD *pdwStreamFlags, - ×tamp, // [out] LONGLONG *pllTimestamp, - &pSample); // [out] IMFSample **ppSample - if (FAILED(hr)) { - qWarning() << "ReadSample failed!"; - break; // abort - } - - if (sDebug) { - qDebug() << "ReadSample timestamp:" << timestamp << "frame:" - << frameFromMF(timestamp) << "dwflags:" << dwFlags; - } - - if (dwFlags & MF_SOURCE_READERF_ERROR) { - // our source reader is now dead, according to the docs - qWarning() - << "SSMF: ReadSample set ERROR, SourceReader is now dead"; - m_dead = true; - break; - } else if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { - qDebug() << "SSMF: End of input file."; - break; - } else if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) { - qWarning() << "SSMF: Type change"; - break; - } else if (pSample == NULL) { - // generally this will happen when dwFlags contains ENDOFSTREAM, - // so it'll be caught before now -bkgood - qWarning() << "SSMF: No sample"; - continue; - } // we now own a ref to the instance at pSample - - IMFMediaBuffer *pMBuffer(NULL); - // I know this does at least a memcopy and maybe a malloc, if we have - // xrun issues with this we might want to look into using - // IMFSample::GetBufferByIndex (although MS doesn't recommend this) - if (FAILED(hr = pSample->ConvertToContiguousBuffer(&pMBuffer))) { - error = true; - goto releaseSample; - } - CSAMPLE *buffer(NULL); - DWORD bufferLengthInBytes(0); - hr = pMBuffer->Lock(reinterpret_cast(&buffer), NULL, &bufferLengthInBytes); - if (FAILED(hr)) { - error = true; - goto releaseMBuffer; - } - SINT bufferLength = samples2frames(bufferLengthInBytes / sizeof(buffer[0])); - - if (m_seeking) { - qint64 bufferPosition(frameFromMF(timestamp)); - if (sDebug) { - qDebug() << "While seeking to " << m_nextFrame - << "WMF put us at" << bufferPosition; - - } - if (m_nextFrame < bufferPosition) { - // Uh oh. We are farther forward than our seek target. Emit - // silence? We can't seek backwards here. - CSAMPLE* pBufferCurpos = sampleBuffer - + frames2samples(numberOfFrames - framesNeeded); - qint64 offshootFrames = bufferPosition - m_nextFrame; - - // If we can correct this immediately, write zeros and adjust - // m_nextFrame to pretend it never happened. - - if (offshootFrames <= framesNeeded) { - qWarning() << __FILE__ << __LINE__ - << "Working around inaccurate seeking. Writing silence for" - << offshootFrames << "frames"; - // Set offshootFrames * kNumChannels samples to zero. - memset(pBufferCurpos, 0, - frames2samples(sizeof(*pBufferCurpos) * offshootFrames)); - // Now m_nextFrame == bufferPosition - m_nextFrame += offshootFrames; - framesNeeded -= offshootFrames; - } else { - // It's more complicated. The buffer we have just decoded is - // more than framesNeeded frames away from us. It's too hard - // for us to handle this correctly currently, so let's just - // try to get on with our lives. - m_seeking = false; - m_nextFrame = bufferPosition; - qWarning() << __FILE__ << __LINE__ - << "Seek offshoot is too drastic. Cutting losses and pretending the current decoded audio buffer is the right seek point."; - } - } - - if (m_nextFrame >= bufferPosition - && m_nextFrame < bufferPosition + bufferLength) { - // m_nextFrame is in this buffer. - buffer += frames2samples(m_nextFrame - bufferPosition); - bufferLength -= m_nextFrame - bufferPosition; - m_seeking = false; - } else { - // we need to keep going forward - goto releaseRawBuffer; - } - } - - // If the bufferLength is larger than the leftover buffer, re-allocate - // it with 2x the space. - if (frames2samples(bufferLength) > m_leftoverBufferSize) { - int newSize = m_leftoverBufferSize; - - while (newSize < frames2samples(bufferLength)) { - newSize *= 2; - } - CSAMPLE* newBuffer = new CSAMPLE[newSize]; - memcpy(newBuffer, m_leftoverBuffer, - sizeof(m_leftoverBuffer[0]) * m_leftoverBufferSize); - delete[] m_leftoverBuffer; - m_leftoverBuffer = newBuffer; - m_leftoverBufferSize = newSize; - } - copyFrames( - sampleBuffer + frames2samples(numberOfFrames - framesNeeded), - &framesNeeded, - buffer, bufferLength); - - releaseRawBuffer: hr = pMBuffer->Unlock(); - // I'm ignoring this, MSDN for IMFMediaBuffer::Unlock stipulates - // nothing about the state of the instance if this fails so might as - // well just let it be released. - //if (FAILED(hr)) break; - releaseMBuffer: safeRelease(&pMBuffer); - releaseSample: safeRelease(&pSample); - if (error) - break; - } - - SINT framesRead = numberOfFrames - framesNeeded; - m_iCurrentPosition += framesRead; - m_nextFrame += framesRead; - if (m_leftoverBufferLength > 0) { - if (framesNeeded != 0) { - qWarning() << __FILE__ << __LINE__ - << "WARNING: Expected frames needed to be 0. Abandoning this file."; - m_dead = true; - } - m_leftoverBufferPosition = m_nextFrame; - } - if (sDebug) { - qDebug() << "read()" << numberOfFrames << "returning" << framesRead; - } - return framesRead; -} - -//------------------------------------------------------------------- -// configureAudioStream -// -// Selects an audio stream from the source file, and configures the -// stream to deliver decoded PCM audio. -//------------------------------------------------------------------- - -/** Cobbled together from: - http://msdn.microsoft.com/en-us/library/dd757929(v=vs.85).aspx - and http://msdn.microsoft.com/en-us/library/dd317928(VS.85).aspx - -- Albert - If anything in here fails, just bail. I'm not going to decode HRESULTS. - -- Bill - */ -bool AudioSourceMediaFoundation::configureAudioStream() { - HRESULT hr(S_OK); - - // deselect all streams, we only want the first - hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, false); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to deselect all streams"; - return false; - } - - hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, - true); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to select first audio stream"; - return false; - } - - hr = MFCreateMediaType(&m_pAudioType); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to create media type"; - return false; - } - - hr = m_pAudioType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set major type"; - return false; - } - - hr = m_pAudioType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_Float); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set subtype"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, true); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set samples independent"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_FIXED_SIZE_SAMPLES, true); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set fixed size samples"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_SAMPLE_SIZE, kLeftoverSize); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set sample size"; - return false; - } - - // MSDN for this attribute says that if bps is 8, samples are unsigned. - // Otherwise, they're signed (so they're signed for us as 16 bps). Why - // chose to hide this rather useful tidbit here is beyond me -bkgood - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, sizeof(m_leftoverBuffer[0]) * 8); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set bits per sample"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, - frames2samples(sizeof(m_leftoverBuffer[0]))); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set block alignment"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, kNumChannels); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set number of channels"; - return false; - } - - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, kSampleRate); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set sample rate"; - return false; - } - - // Set this type on the source reader. The source reader will - // load the necessary decoder. - hr = m_pReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, - NULL, m_pAudioType); - - // the reader has the media type now, free our reference so we can use our - // pointer for other purposes. Do this before checking for failure so we - // don't dangle. - safeRelease(&m_pAudioType); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set media type"; - return false; - } - - // Get the complete uncompressed format. - hr = m_pReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, - &m_pAudioType); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to retrieve completed media type"; - return false; - } - - // Ensure the stream is selected. - hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, - true); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to select first audio stream (again)"; - return false; - } - - UINT32 leftoverBufferSize = 0; - hr = m_pAudioType->GetUINT32(MF_MT_SAMPLE_SIZE, &leftoverBufferSize); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to get buffer size"; - return false; - } - m_leftoverBufferSize = leftoverBufferSize; - m_leftoverBufferSize /= sizeof(CSAMPLE); // convert size in bytes to sizeof(CSAMPLE) - m_leftoverBuffer = new CSAMPLE[m_leftoverBufferSize]; - - return true; -} - -/** - * Copies min(destFrames, srcFrames) frames to dest from src. Anything leftover - * is moved to the beginning of m_leftoverBuffer, so empty it first (possibly - * with this method). If src and dest overlap, I'll hurt you. - */ -void AudioSourceMediaFoundation::copyFrames(CSAMPLE *dest, size_t *destFrames, - const CSAMPLE *src, size_t srcFrames) { - if (srcFrames > *destFrames) { - int samplesToCopy(*destFrames * kNumChannels); - memcpy(dest, src, samplesToCopy * sizeof(*src)); - srcFrames -= *destFrames; - memmove(m_leftoverBuffer, src + samplesToCopy, - srcFrames * kNumChannels * sizeof(*src)); - *destFrames = 0; - m_leftoverBufferLength = srcFrames; - } else { - int samplesToCopy(srcFrames * kNumChannels); - memcpy(dest, src, samplesToCopy * sizeof(*src)); - *destFrames -= srcFrames; - if (src == m_leftoverBuffer) { - m_leftoverBufferLength = 0; - } - } -} - -} diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h deleted file mode 100644 index 3733bd89793..00000000000 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ /dev/null @@ -1,66 +0,0 @@ -// TODO(XXX): This implementation has been copied from the -// original file soundsourcemediafoundation.h and needs -// a major refactoring! Please refer to the helpful comments -// of daschuer in the following pull requests: -// https://github.com/mixxxdj/mixxx/pull/411 - -#ifndef AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H -#define AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H - -#include "sources/audiosource.h" - -#include - -#ifdef Q_OS_WIN -#define MY_EXPORT __declspec(dllexport) -#else -#define MY_EXPORT -#endif - -class IMFSourceReader; -class IMFMediaType; -class IMFMediaSource; - -namespace Mixxx { - -class AudioSourceMediaFoundation : public AudioSource { -public: - static AudioSourcePointer create(QUrl url); - - ~AudioSourceMediaFoundation(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; - -private: - explicit AudioSourceMediaFoundation(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - bool configureAudioStream(); - - void copyFrames(CSAMPLE *dest, size_t *destFrames, const CSAMPLE *src, - size_t srcFrames); - - HRESULT m_hrCoInitialize; - HRESULT m_hrMFStartup; - IMFSourceReader *m_pReader; - IMFMediaType *m_pAudioType; - wchar_t *m_wcFilename; - int m_nextFrame; - CSAMPLE *m_leftoverBuffer; - size_t m_leftoverBufferSize; - size_t m_leftoverBufferLength; - int m_leftoverBufferPosition; - qint64 m_mfDuration; - long m_iCurrentPosition; - bool m_dead; - bool m_seeking; -}; - -} - -#endif // ifndef AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 06774b4a719..5f682655cde 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -21,11 +21,60 @@ ***************************************************************************/ #include "soundsourcemediafoundation.h" -#include "audiosourcemediafoundation.h" -#include "metadata/trackmetadatataglib.h" +#include +#include +#include +#include +#include -#include +namespace +{ + +const bool sDebug = false; + +const int kNumChannels = 2; +const int kSampleRate = 44100; +const int kLeftoverSize = 4096; // in CSAMPLE's, this seems to be the size MF AAC +const int kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give + +/** + * Convert a 100ns Media Foundation value to a number of seconds. + */ +inline qreal secondsFromMF(qint64 mf) { + return static_cast(mf) / 1e7; +} + +/** + * Convert a number of seconds to a 100ns Media Foundation value. + */ +inline qint64 mfFromSeconds(qreal sec) { + return sec * 1e7; +} + +/** + * Convert a 100ns Media Foundation value to a frame offset. + */ +inline qint64 frameFromMF(qint64 mf) { + return static_cast(mf) * kSampleRate / 1e7; +} + +/** + * Convert a frame offset to a 100ns Media Foundation value. + */ +inline qint64 mfFromFrame(qint64 frame) { + return static_cast(frame) / kSampleRate * 1e7; +} + +/** Microsoft examples use this snippet often. */ +template static void safeRelease(T **ppT) { + if (*ppT) { + (*ppT)->Release(); + *ppT = NULL; + } +} + +} // anonymous namespace // static QList SoundSourceMediaFoundation::supportedFileExtensions() { @@ -36,11 +85,504 @@ QList SoundSourceMediaFoundation::supportedFileExtensions() { } SoundSourceMediaFoundation::SoundSourceMediaFoundation(QUrl url) - : SoundSourcePlugin(url, "m4a") { + : SoundSourcePlugin(url, "m4a"), + m_hrCoInitialize(E_FAIL), + m_hrMFStartup(E_FAIL), + m_pReader(NULL), + m_pAudioType(NULL), + m_wcFilename(NULL), + m_nextFrame(0), + m_leftoverBuffer(NULL), + m_leftoverBufferSize(0), + m_leftoverBufferLength(0), + m_leftoverBufferPosition(0), + m_mfDuration(0), + m_iCurrentPosition(0), + m_dead(false), + m_seeking(false) { + + // these are always the same, might as well just stick them here + // -bkgood + // AudioSource properties + setChannelCount(kNumChannels); + setFrameRate(kSampleRate); + + // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for + // presentation descriptors, one of which MFSourceReader is not. + // Therefore, we calculate it ourselves, assuming 16 bits per sample + setBitrate(frames2samples(kBitsPerSampleForBitrate * kSampleRate) / 1000); +} + +SoundSourceMediaFoundation::~SoundSourceMediaFoundation() { + close(); +} + +Result SoundSourceMediaFoundation::open() { + if (SUCCEEDED(m_hrCoInitialize)) { + qWarning() << "Cannot reopen MediaFoundation file" << getUrl(); + return ERR; + } + + const QString fileName(getLocalFileName()); + + if (sDebug) { + qDebug() << "open()" << fileName; + } + + // Initialize the COM library. + m_hrCoInitialize = CoInitializeEx(NULL, + COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if (FAILED(m_hrCoInitialize)) { + qWarning() << "SSMF: failed to initialize COM"; + return ERR; + } + // Initialize the Media Foundation platform. + m_hrMFStartup = MFStartup(MF_VERSION); + if (FAILED(m_hrCoInitialize)) { + qWarning() << "SSMF: failed to initialize Media Foundation"; + return ERR; + } + + QString qurlStr(fileName); + // http://social.msdn.microsoft.com/Forums/en/netfxbcl/thread/35c6a451-3507-40c8-9d1c-8d4edde7c0cc + // gives maximum path + file length as 248 + 260, using that -bkgood + m_wcFilename = new wchar_t[248 + 260]; + int wcFilenameLength(fileName.toWCharArray(m_wcFilename)); + // toWCharArray does not append a null terminator to the string! + m_wcFilename[wcFilenameLength] = '\0'; + + // Create the source reader to read the input file. + HRESULT hr = MFCreateSourceReaderFromURL(m_wcFilename, NULL, &m_pReader); + if (FAILED(hr)) { + qWarning() << "SSMF: Error opening input file:" << fileName; + return ERR; + } + + if (!configureAudioStream()) { + qWarning() << "SSMF: Error configuring audio stream."; + return ERR; + } + + //Seek to position 0, which forces us to skip over all the header frames. + //This makes sure we're ready to just let the Analyser rip and it'll + //get the number of samples it expects (ie. no header frames). + seekSampleFrame(0); + + return OK; +} + +void SoundSourceMediaFoundation::close() { + delete[] m_wcFilename; + m_wcFilename = NULL; + delete[] m_leftoverBuffer; + m_leftoverBuffer = NULL; + + safeRelease(&m_pReader); + safeRelease(&m_pAudioType); + + if (SUCCEEDED(m_hrMFStartup)) { + MFShutdown(); + m_hrMFStartup = E_FAIL; + } + if (SUCCEEDED(m_hrCoInitialize)) { + CoUninitialize(); + m_hrCoInitialize = E_FAIL; + } } -Mixxx::AudioSourcePointer SoundSourceMediaFoundation::open() const { - return Mixxx::AudioSourceMediaFoundation::create(getUrl()); +SINT SoundSourceMediaFoundation::seekSampleFrame( + SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + if (sDebug) { + qDebug() << "seekSampleFrame()" << frameIndex; + } + qint64 mfSeekTarget(mfFromFrame(frameIndex) - 1); + // minus 1 here seems to make our seeking work properly, otherwise we will + // (more often than not, maybe always) seek a bit too far (although not + // enough for our calculatedFrameFromMF <= nextFrame assertion in ::read). + // Has something to do with 100ns MF units being much smaller than most + // frame offsets (in seconds) -bkgood + long result = m_iCurrentPosition; + if (m_dead) { + return result; + } + + PROPVARIANT prop; + HRESULT hr; + + // this doesn't fail, see MS's implementation + hr = InitPropVariantFromInt64(mfSeekTarget < 0 ? 0 : mfSeekTarget, &prop); + + hr = m_pReader->Flush(MF_SOURCE_READER_FIRST_AUDIO_STREAM); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to flush before seek"; + } + + // http://msdn.microsoft.com/en-us/library/dd374668(v=VS.85).aspx + hr = m_pReader->SetCurrentPosition(GUID_NULL, prop); + if (FAILED(hr)) { + // nothing we can do here as we can't fail (no facility to other than + // crashing mixxx) + qWarning() << "SSMF: failed to seek" + << (hr == MF_E_INVALIDREQUEST ? + "Sample requests still pending" : ""); + } else { + result = frameIndex; + } + PropVariantClear(&prop); + + // record the next frame so that we can make sure we're there the next + // time we get a buffer from MFSourceReader + m_nextFrame = frameIndex; + m_seeking = true; + m_iCurrentPosition = result; + return result; +} + +SINT SoundSourceMediaFoundation::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + if (sDebug) { + qDebug() << "read()" << numberOfFrames; + } + SINT framesNeeded(numberOfFrames); + + // first, copy frames from leftover buffer IF the leftover buffer is at + // the correct frame + if (m_leftoverBufferLength > 0 && m_leftoverBufferPosition == m_nextFrame) { + copyFrames(sampleBuffer, &framesNeeded, m_leftoverBuffer, + m_leftoverBufferLength); + if (m_leftoverBufferLength > 0) { + if (framesNeeded != 0) { + qWarning() << __FILE__ << __LINE__ + << "WARNING: Expected frames needed to be 0. Abandoning this file."; + m_dead = true; + } + m_leftoverBufferPosition += numberOfFrames; + } + } else { + // leftoverBuffer already empty or in the wrong position, clear it + m_leftoverBufferLength = 0; + } + + while (!m_dead && framesNeeded > 0) { + HRESULT hr(S_OK); + DWORD dwFlags(0); + qint64 timestamp(0); + IMFSample *pSample(NULL); + bool error(false); // set to true to break after releasing + + hr = m_pReader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, // [in] DWORD dwStreamIndex, + 0, // [in] DWORD dwControlFlags, + NULL, // [out] DWORD *pdwActualStreamIndex, + &dwFlags, // [out] DWORD *pdwStreamFlags, + ×tamp, // [out] LONGLONG *pllTimestamp, + &pSample); // [out] IMFSample **ppSample + if (FAILED(hr)) { + qWarning() << "ReadSample failed!"; + break; // abort + } + + if (sDebug) { + qDebug() << "ReadSample timestamp:" << timestamp << "frame:" + << frameFromMF(timestamp) << "dwflags:" << dwFlags; + } + + if (dwFlags & MF_SOURCE_READERF_ERROR) { + // our source reader is now dead, according to the docs + qWarning() + << "SSMF: ReadSample set ERROR, SourceReader is now dead"; + m_dead = true; + break; + } else if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) { + qDebug() << "SSMF: End of input file."; + break; + } else if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) { + qWarning() << "SSMF: Type change"; + break; + } else if (pSample == NULL) { + // generally this will happen when dwFlags contains ENDOFSTREAM, + // so it'll be caught before now -bkgood + qWarning() << "SSMF: No sample"; + continue; + } // we now own a ref to the instance at pSample + + IMFMediaBuffer *pMBuffer(NULL); + // I know this does at least a memcopy and maybe a malloc, if we have + // xrun issues with this we might want to look into using + // IMFSample::GetBufferByIndex (although MS doesn't recommend this) + if (FAILED(hr = pSample->ConvertToContiguousBuffer(&pMBuffer))) { + error = true; + goto releaseSample; + } + CSAMPLE *buffer(NULL); + DWORD bufferLengthInBytes(0); + hr = pMBuffer->Lock(reinterpret_cast(&buffer), NULL, &bufferLengthInBytes); + if (FAILED(hr)) { + error = true; + goto releaseMBuffer; + } + SINT bufferLength = samples2frames(bufferLengthInBytes / sizeof(buffer[0])); + + if (m_seeking) { + qint64 bufferPosition(frameFromMF(timestamp)); + if (sDebug) { + qDebug() << "While seeking to " << m_nextFrame + << "WMF put us at" << bufferPosition; + + } + if (m_nextFrame < bufferPosition) { + // Uh oh. We are farther forward than our seek target. Emit + // silence? We can't seek backwards here. + CSAMPLE* pBufferCurpos = sampleBuffer + + frames2samples(numberOfFrames - framesNeeded); + qint64 offshootFrames = bufferPosition - m_nextFrame; + + // If we can correct this immediately, write zeros and adjust + // m_nextFrame to pretend it never happened. + + if (offshootFrames <= framesNeeded) { + qWarning() << __FILE__ << __LINE__ + << "Working around inaccurate seeking. Writing silence for" + << offshootFrames << "frames"; + // Set offshootFrames * kNumChannels samples to zero. + memset(pBufferCurpos, 0, + frames2samples(sizeof(*pBufferCurpos) * offshootFrames)); + // Now m_nextFrame == bufferPosition + m_nextFrame += offshootFrames; + framesNeeded -= offshootFrames; + } else { + // It's more complicated. The buffer we have just decoded is + // more than framesNeeded frames away from us. It's too hard + // for us to handle this correctly currently, so let's just + // try to get on with our lives. + m_seeking = false; + m_nextFrame = bufferPosition; + qWarning() << __FILE__ << __LINE__ + << "Seek offshoot is too drastic. Cutting losses and pretending the current decoded audio buffer is the right seek point."; + } + } + + if (m_nextFrame >= bufferPosition + && m_nextFrame < bufferPosition + bufferLength) { + // m_nextFrame is in this buffer. + buffer += frames2samples(m_nextFrame - bufferPosition); + bufferLength -= m_nextFrame - bufferPosition; + m_seeking = false; + } else { + // we need to keep going forward + goto releaseRawBuffer; + } + } + + // If the bufferLength is larger than the leftover buffer, re-allocate + // it with 2x the space. + if (frames2samples(bufferLength) > m_leftoverBufferSize) { + int newSize = m_leftoverBufferSize; + + while (newSize < frames2samples(bufferLength)) { + newSize *= 2; + } + CSAMPLE* newBuffer = new CSAMPLE[newSize]; + memcpy(newBuffer, m_leftoverBuffer, + sizeof(m_leftoverBuffer[0]) * m_leftoverBufferSize); + delete[] m_leftoverBuffer; + m_leftoverBuffer = newBuffer; + m_leftoverBufferSize = newSize; + } + copyFrames( + sampleBuffer + frames2samples(numberOfFrames - framesNeeded), + &framesNeeded, + buffer, bufferLength); + + releaseRawBuffer: hr = pMBuffer->Unlock(); + // I'm ignoring this, MSDN for IMFMediaBuffer::Unlock stipulates + // nothing about the state of the instance if this fails so might as + // well just let it be released. + //if (FAILED(hr)) break; + releaseMBuffer: safeRelease(&pMBuffer); + releaseSample: safeRelease(&pSample); + if (error) + break; + } + + SINT framesRead = numberOfFrames - framesNeeded; + m_iCurrentPosition += framesRead; + m_nextFrame += framesRead; + if (m_leftoverBufferLength > 0) { + if (framesNeeded != 0) { + qWarning() << __FILE__ << __LINE__ + << "WARNING: Expected frames needed to be 0. Abandoning this file."; + m_dead = true; + } + m_leftoverBufferPosition = m_nextFrame; + } + if (sDebug) { + qDebug() << "read()" << numberOfFrames << "returning" << framesRead; + } + return framesRead; +} + +//------------------------------------------------------------------- +// configureAudioStream +// +// Selects an audio stream from the source file, and configures the +// stream to deliver decoded PCM audio. +//------------------------------------------------------------------- + +/** Cobbled together from: + http://msdn.microsoft.com/en-us/library/dd757929(v=vs.85).aspx + and http://msdn.microsoft.com/en-us/library/dd317928(VS.85).aspx + -- Albert + If anything in here fails, just bail. I'm not going to decode HRESULTS. + -- Bill + */ +bool SoundSourceMediaFoundation::configureAudioStream() { + HRESULT hr(S_OK); + + // deselect all streams, we only want the first + hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, false); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to deselect all streams"; + return false; + } + + hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + true); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to select first audio stream"; + return false; + } + + hr = MFCreateMediaType(&m_pAudioType); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to create media type"; + return false; + } + + hr = m_pAudioType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set major type"; + return false; + } + + hr = m_pAudioType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_Float); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set subtype"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, true); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set samples independent"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_FIXED_SIZE_SAMPLES, true); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set fixed size samples"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_SAMPLE_SIZE, kLeftoverSize); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set sample size"; + return false; + } + + // MSDN for this attribute says that if bps is 8, samples are unsigned. + // Otherwise, they're signed (so they're signed for us as 16 bps). Why + // chose to hide this rather useful tidbit here is beyond me -bkgood + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, sizeof(m_leftoverBuffer[0]) * 8); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set bits per sample"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, + frames2samples(sizeof(m_leftoverBuffer[0]))); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set block alignment"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, kNumChannels); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set number of channels"; + return false; + } + + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, kSampleRate); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set sample rate"; + return false; + } + + // Set this type on the source reader. The source reader will + // load the necessary decoder. + hr = m_pReader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + NULL, m_pAudioType); + + // the reader has the media type now, free our reference so we can use our + // pointer for other purposes. Do this before checking for failure so we + // don't dangle. + safeRelease(&m_pAudioType); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set media type"; + return false; + } + + // Get the complete uncompressed format. + hr = m_pReader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + &m_pAudioType); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to retrieve completed media type"; + return false; + } + + // Ensure the stream is selected. + hr = m_pReader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, + true); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to select first audio stream (again)"; + return false; + } + + UINT32 leftoverBufferSize = 0; + hr = m_pAudioType->GetUINT32(MF_MT_SAMPLE_SIZE, &leftoverBufferSize); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to get buffer size"; + return false; + } + m_leftoverBufferSize = leftoverBufferSize; + m_leftoverBufferSize /= sizeof(CSAMPLE); // convert size in bytes to sizeof(CSAMPLE) + m_leftoverBuffer = new CSAMPLE[m_leftoverBufferSize]; + + return true; +} + +/** + * Copies min(destFrames, srcFrames) frames to dest from src. Anything leftover + * is moved to the beginning of m_leftoverBuffer, so empty it first (possibly + * with this method). If src and dest overlap, I'll hurt you. + */ +void SoundSourceMediaFoundation::copyFrames(CSAMPLE *dest, size_t *destFrames, + const CSAMPLE *src, size_t srcFrames) { + if (srcFrames > *destFrames) { + int samplesToCopy(*destFrames * kNumChannels); + memcpy(dest, src, samplesToCopy * sizeof(*src)); + srcFrames -= *destFrames; + memmove(m_leftoverBuffer, src + samplesToCopy, + srcFrames * kNumChannels * sizeof(*src)); + *destFrames = 0; + m_leftoverBufferLength = srcFrames; + } else { + int samplesToCopy(srcFrames * kNumChannels); + memcpy(dest, src, samplesToCopy * sizeof(*src)); + *destFrames -= srcFrames; + if (src == m_leftoverBuffer) { + m_leftoverBufferLength = 0; + } + } } extern "C" MY_EXPORT const char* getMixxxVersion() { diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 760b538602b..36bdf8682fc 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -21,7 +21,8 @@ #define SOUNDSOURCEMEDIAFOUNDATION_H #include "sources/soundsourceplugin.h" -#include "defs_version.h" + +#include #ifdef Q_OS_WIN #define MY_EXPORT __declspec(dllexport) @@ -29,13 +30,44 @@ #define MY_EXPORT #endif +class IMFSourceReader; +class IMFMediaType; +class IMFMediaSource; + class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { public: static QList supportedFileExtensions(); explicit SoundSourceMediaFoundation(QUrl url); + ~SoundSourceMediaFoundation(); + + Result open() /*override*/; + void close() /*override*/; + + SINT seekSampleFrame(SINT frameIndex) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; + +private: + bool configureAudioStream(); + + void copyFrames(CSAMPLE *dest, size_t *destFrames, const CSAMPLE *src, + size_t srcFrames); - Mixxx::AudioSourcePointer open() const /*override*/; + HRESULT m_hrCoInitialize; + HRESULT m_hrMFStartup; + IMFSourceReader *m_pReader; + IMFMediaType *m_pAudioType; + wchar_t *m_wcFilename; + int m_nextFrame; + CSAMPLE *m_leftoverBuffer; + size_t m_leftoverBufferSize; + size_t m_leftoverBufferLength; + int m_leftoverBufferPosition; + qint64 m_mfDuration; + long m_iCurrentPosition; + bool m_dead; + bool m_seeking; }; extern "C" MY_EXPORT const char* getMixxxVersion(); @@ -44,4 +76,4 @@ extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName); extern "C" MY_EXPORT char** supportedFileExtensions(); extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions); -#endif // ifndef SOUNDSOURCEMEDIAFOUNDATION_H +#endif // SOUNDSOURCEMEDIAFOUNDATION_H diff --git a/plugins/soundsourcewv/SConscript b/plugins/soundsourcewv/SConscript index 322fdaf9f75..d25358ad0a7 100644 --- a/plugins/soundsourcewv/SConscript +++ b/plugins/soundsourcewv/SConscript @@ -12,7 +12,6 @@ Import('build') wv_sources = [ "soundsourcewv.cpp", - "audiosourcewv.cpp", "sources/soundsource.cpp", "sources/soundsourceplugin.cpp", "sources/audiosource.cpp", diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp deleted file mode 100644 index dba3f830cad..00000000000 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "audiosourcewv.h" - -namespace Mixxx { - -AudioSourceWV::AudioSourceWV(QUrl url) - : AudioSource(url), - m_wpc(NULL), - m_sampleScale(kSampleValueZero) { -} - -AudioSourceWV::~AudioSourceWV() { - preDestroy(); -} - -AudioSourcePointer AudioSourceWV::create(QUrl url) { - return onCreate(new AudioSourceWV(url)); -} - -Result AudioSourceWV::postConstruct() { - char msg[80]; // hold possible error message - m_wpc = WavpackOpenFileInput( - getLocalFileNameBytes().constData(), msg, - OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); - if (!m_wpc) { - qDebug() << "SSWV::open: failed to open file : " << msg; - return ERR; - } - - setChannelCount(WavpackGetReducedChannels(m_wpc)); - setFrameRate(WavpackGetSampleRate(m_wpc)); - setFrameCount(WavpackGetNumSamples(m_wpc)); - - if (WavpackGetMode(m_wpc) & MODE_FLOAT) { - m_sampleScale = kSampleValuePeak; - } else { - const int bitsPerSample = WavpackGetBitsPerSample(m_wpc); - const uint32_t wavpackPeakSampleValue = uint32_t(1) - << (bitsPerSample - 1); - m_sampleScale = kSampleValuePeak / CSAMPLE(wavpackPeakSampleValue); - } - - return OK; -} - -void AudioSourceWV::preDestroy() { - if (m_wpc) { - WavpackCloseFile(m_wpc); - m_wpc = NULL; - } -} - -SINT AudioSourceWV::seekSampleFrame(SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - if (WavpackSeekSample(m_wpc, frameIndex) == TRUE) { - return frameIndex; - } else { - qDebug() << "SSWV::seek : could not seek to frame #" << frameIndex; - return WavpackGetSampleIndex(m_wpc); - } -} - -SINT AudioSourceWV::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - // static assert: sizeof(CSAMPLE) == sizeof(int32_t) - SINT unpackCount = WavpackUnpackSamples(m_wpc, - reinterpret_cast(sampleBuffer), numberOfFrames); - if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { - // signed integer -> float - const SINT sampleCount = frames2samples(unpackCount); - for (SINT i = 0; i < sampleCount; ++i) { - const int32_t sampleValue = - reinterpret_cast(sampleBuffer)[i]; - sampleBuffer[i] = CSAMPLE(sampleValue) * m_sampleScale; - } - } - return unpackCount; -} - -} // namespace Mixxx diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h deleted file mode 100644 index 74c59f9a44c..00000000000 --- a/plugins/soundsourcewv/audiosourcewv.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef AUDIOSOURCEWV_H -#define AUDIOSOURCEWV_H - -#include "sources/audiosource.h" - -#include "wavpack/wavpack.h" - -#ifdef Q_OS_WIN -#define MY_EXPORT __declspec(dllexport) -#else -#define MY_EXPORT -#endif - -namespace Mixxx { - -class AudioSourceWV: public AudioSource { -public: - static AudioSourcePointer create(QUrl url); - - ~AudioSourceWV(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) /*override*/; - -private: - explicit AudioSourceWV(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - WavpackContext* m_wpc; - - CSAMPLE m_sampleScale; -}; - -} // namespace Mixxx - -#endif diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 60eb7d2a04e..64477e010b4 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -1,7 +1,4 @@ #include "soundsourcewv.h" -#include "audiosourcewv.h" - -#include "metadata/trackmetadatataglib.h" namespace Mixxx { @@ -12,11 +9,78 @@ QList SoundSourceWV::supportedFileExtensions() { } SoundSourceWV::SoundSourceWV(QUrl url) - : SoundSourcePlugin(url, "wv") { + : SoundSourcePlugin(url, "wv"), + m_wpc(NULL), + m_sampleScaleFactor(kSampleValueZero) { +} + +SoundSourceWV::~SoundSourceWV() { + close(); +} + +Result SoundSourceWV::open() { + if (m_wpc) { + qWarning() << "Cannot reopen WavPack file:" << getUrl(); + return ERR; + } + + char msg[80]; // hold possible error message + m_wpc = WavpackOpenFileInput( + getLocalFileNameBytes().constData(), msg, + OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); + if (!m_wpc) { + qDebug() << "SSWV::open: failed to open file : " << msg; + return ERR; + } + + setChannelCount(WavpackGetReducedChannels(m_wpc)); + setFrameRate(WavpackGetSampleRate(m_wpc)); + setFrameCount(WavpackGetNumSamples(m_wpc)); + + if (WavpackGetMode(m_wpc) & MODE_FLOAT) { + m_sampleScaleFactor = kSampleValuePeak; + } else { + const int bitsPerSample = WavpackGetBitsPerSample(m_wpc); + const uint32_t wavpackPeakSampleValue = uint32_t(1) + << (bitsPerSample - 1); + m_sampleScaleFactor = kSampleValuePeak / CSAMPLE(wavpackPeakSampleValue); + } + + return OK; +} + +void SoundSourceWV::close() { + if (m_wpc) { + WavpackCloseFile(m_wpc); + m_wpc = NULL; + } +} + +SINT SoundSourceWV::seekSampleFrame(SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + if (WavpackSeekSample(m_wpc, frameIndex) == TRUE) { + return frameIndex; + } else { + qDebug() << "SSWV::seek : could not seek to frame #" << frameIndex; + return WavpackGetSampleIndex(m_wpc); + } } -Mixxx::AudioSourcePointer SoundSourceWV::open() const { - return Mixxx::AudioSourceWV::create(getUrl()); +SINT SoundSourceWV::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + // static assert: sizeof(CSAMPLE) == sizeof(int32_t) + SINT unpackCount = WavpackUnpackSamples(m_wpc, + reinterpret_cast(sampleBuffer), numberOfFrames); + if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { + // signed integer -> float + const SINT sampleCount = frames2samples(unpackCount); + for (SINT i = 0; i < sampleCount; ++i) { + const int32_t sampleValue = + reinterpret_cast(sampleBuffer)[i]; + sampleBuffer[i] = CSAMPLE(sampleValue) * m_sampleScaleFactor; + } + } + return unpackCount; } } // namespace Mixxx diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 5217671404b..8a18f4baba1 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -1,8 +1,9 @@ -#ifndef SOUNDSOURCEWV_H -#define SOUNDSOURCEWV_H +#ifndef MIXXX_SOUNDSOURCEWV_H +#define MIXXX_SOUNDSOURCEWV_H #include "sources/soundsourceplugin.h" -#include "defs_version.h" + +#include "wavpack/wavpack.h" #ifdef Q_OS_WIN #define MY_EXPORT __declspec(dllexport) @@ -17,8 +18,20 @@ class SoundSourceWV: public SoundSourcePlugin { static QList supportedFileExtensions(); explicit SoundSourceWV(QUrl url); + ~SoundSourceWV(); + + Result open() /*override*/; + void close() /*override*/; + + SINT seekSampleFrame(SINT frameIndex) /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) /*override*/; + +private: + WavpackContext* m_wpc; + + CSAMPLE m_sampleScaleFactor; }; } // namespace Mixxx @@ -29,4 +42,4 @@ extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName); extern "C" MY_EXPORT char** supportedFileExtensions(); extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions); -#endif +#endif // MIXXX_SOUNDSOURCEWV_H diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 13cd3b3bc02..3dfe2881752 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -309,7 +309,7 @@ void AnalyserQueue::run() { // Get the audio SoundSourceProxy soundSourceProxy(nextTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); if (!pAudioSource) { qWarning() << "Failed to open file for analyzing:" << nextTrack->getLocation(); continue; diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 8cb4979b140..42b701c1f1d 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -136,7 +136,7 @@ namespace { Mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); if (pAudioSource.isNull()) { qWarning() << "Failed to open file:" << pTrack->getLocation(); return Mixxx::AudioSourcePointer(); diff --git a/src/dlgprefmodplug.cpp b/src/dlgprefmodplug.cpp index a4fa985f395..7dd73097b62 100644 --- a/src/dlgprefmodplug.cpp +++ b/src/dlgprefmodplug.cpp @@ -5,7 +5,7 @@ #include "ui_dlgprefmodplugdlg.h" #include "configobject.h" -#include "sources/audiosourcemodplug.h" +#include "sources/soundsourcemodplug.h" #define kConfigKey "[Modplug]" @@ -180,5 +180,5 @@ void DlgPrefModplug::applySettings() { settings.mLoopCount = 0; // apply modplug settings - Mixxx::AudioSourceModPlug::configure(bufferSizeLimit, settings); + Mixxx::SoundSourceModPlug::configure(bufferSizeLimit, settings); } diff --git a/src/library/coverartutils.h b/src/library/coverartutils.h index 706778c9053..3191a0315d1 100644 --- a/src/library/coverartutils.h +++ b/src/library/coverartutils.h @@ -40,11 +40,7 @@ class CoverArtUtils { return QImage(); } SoundSourceProxy proxy(trackLocation, pToken); - Mixxx::SoundSourcePointer pSoundSource(proxy.getSoundSource()); - if (pSoundSource.isNull()) { - return QImage(); - } - return pSoundSource->parseCoverArt(); + return proxy.parseCoverArt(); } static QImage loadCover(const CoverInfo& info) { diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index ab5cff76a93..aa85d64118c 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -1848,12 +1848,7 @@ namespace QImage parseCoverArt(const QFileInfo& fileInfo) { SecurityTokenPointer pToken = Sandbox::openSecurityToken(fileInfo, true); SoundSourceProxy proxy(fileInfo.filePath(), pToken); - Mixxx::SoundSourcePointer pSoundSource(proxy.getSoundSource()); - if (pSoundSource) { - return pSoundSource->parseCoverArt(); - } else { - return QImage(); - } + return proxy.parseCoverArt(); } } diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 35678917c64..19721a4a958 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -99,7 +99,7 @@ ChromaPrinter::ChromaPrinter(QObject* parent) QString ChromaPrinter::getFingerprint(TrackPointer pTrack) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); if (pAudioSource.isNull()) { qDebug() << "Skipping invalid file:" << pTrack->getLocation(); return QString(); diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index be3c8addbf2..af7c2addba4 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -1,20 +1,20 @@ /* -*- mode:C++; indent-tabs-mode:t; tab-width:8; c-basic-offset:4; -*- */ /*************************************************************************** - soundsourceproxy.cpp - description - ------------------- - begin : Wed Oct 13 2004 - copyright : (C) 2004 Tue Haste Andersen - email : -***************************************************************************/ + soundsourceproxy.cpp - description + ------------------- + begin : Wed Oct 13 2004 + copyright : (C) 2004 Tue Haste Andersen + email : + ***************************************************************************/ /*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -***************************************************************************/ + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ #include #include @@ -58,28 +58,32 @@ QMutex SoundSourceProxy::m_extensionsMutex; namespace { - SecurityTokenPointer openSecurityToken(QString qFilename, SecurityTokenPointer pToken) { - if (pToken.isNull()) { - // Open a security token for the file if we are in a sandbox. - QFileInfo info(qFilename); - return Sandbox::openSecurityToken(info, true); - } else { - return pToken; - } +SecurityTokenPointer openSecurityToken(QString qFilename, + SecurityTokenPointer pToken) { + if (pToken.isNull()) { + // Open a security token for the file if we are in a sandbox. + QFileInfo info(qFilename); + return Sandbox::openSecurityToken(info, true); + } else { + return pToken; } } +} //Constructor -SoundSourceProxy::SoundSourceProxy(QString qFilename, SecurityTokenPointer pToken) - : m_pSecurityToken(openSecurityToken(qFilename, pToken)) - , m_pSoundSource(initialize(qFilename)) { +SoundSourceProxy::SoundSourceProxy(QString qFilename, + SecurityTokenPointer pToken) + : m_pSecurityToken(openSecurityToken(qFilename, pToken)) + , m_pSoundSource(initialize(qFilename)) { } //Other constructor SoundSourceProxy::SoundSourceProxy(TrackPointer pTrack) - : m_pTrack(pTrack) - , m_pSecurityToken(openSecurityToken(pTrack->getLocation(), pTrack->getSecurityToken())) - , m_pSoundSource(initialize(pTrack->getLocation())) { + : m_pTrack(pTrack) + , m_pSecurityToken( + openSecurityToken(pTrack->getLocation(), + pTrack->getSecurityToken())) + , m_pSoundSource(initialize(pTrack->getLocation())) { } // static @@ -154,16 +158,17 @@ void SoundSourceProxy::loadPlugins() { nameFilters << "libsoundsource*"; #endif - foreach(QDir dir, pluginDirs) { - QStringList files = dir.entryList(nameFilters, QDir::Files | QDir::NoDotAndDotDot); - foreach (const QString& file, files) { - getPlugin(dir.filePath(file)); - } + foreach(QDir dir, pluginDirs){ + QStringList files = dir.entryList(nameFilters, QDir::Files | QDir::NoDotAndDotDot); + foreach (const QString& file, files) { + getPlugin(dir.filePath(file)); } } +} // static -Mixxx::SoundSourcePointer SoundSourceProxy::initialize(const QString& qFilename) { +Mixxx::SoundSourcePointer SoundSourceProxy::initialize( + const QString& qFilename) { const QUrl url(QUrl::fromLocalFile(qFilename)); const QString type(Mixxx::SoundSource::getTypeFromUrl(url)); @@ -173,27 +178,27 @@ Mixxx::SoundSourcePointer SoundSourceProxy::initialize(const QString& qFilename) } #ifdef __FFMPEGFILE__ - return Mixxx::SoundSourcePointer(new SoundSourceFFmpeg(url)); + return Mixxx::SoundSourcePointer(new Mixxx::SoundSourceFFmpeg(url)); #endif - if (SoundSourceOggVorbis::supportedFileExtensions().contains(type)) { - return Mixxx::SoundSourcePointer(new SoundSourceOggVorbis(url)); + if (Mixxx::SoundSourceOggVorbis::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new Mixxx::SoundSourceOggVorbis(url)); #ifdef __OPUS__ - } else if (SoundSourceOpus::supportedFileExtensions().contains(type)) { - return Mixxx::SoundSourcePointer(new SoundSourceOpus(url)); + } else if (Mixxx::SoundSourceOpus::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new Mixxx::SoundSourceOpus(url)); #endif #ifdef __MAD__ - } else if (SoundSourceMp3::supportedFileExtensions().contains(type)) { - return Mixxx::SoundSourcePointer(new SoundSourceMp3(url)); + } else if (Mixxx::SoundSourceMp3::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new Mixxx::SoundSourceMp3(url)); #endif - } else if (SoundSourceFLAC::supportedFileExtensions().contains(type)) { - return Mixxx::SoundSourcePointer(new SoundSourceFLAC(url)); + } else if (Mixxx::SoundSourceFLAC::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new Mixxx::SoundSourceFLAC(url)); #ifdef __COREAUDIO__ } else if (SoundSourceCoreAudio::supportedFileExtensions().contains(type)) { return Mixxx::SoundSourcePointer(new SoundSourceCoreAudio(url)); #endif #ifdef __MODPLUG__ - } else if (SoundSourceModPlug::supportedFileExtensions().contains(type)) { - return Mixxx::SoundSourcePointer(new SoundSourceModPlug(url)); + } else if (Mixxx::SoundSourceModPlug::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new Mixxx::SoundSourceModPlug(url)); #endif } else if (m_extensionsSupportedByPlugins.contains(type)) { getSoundSourceFunc getter = m_extensionsSupportedByPlugins.value(type); @@ -208,8 +213,8 @@ Mixxx::SoundSourcePointer SoundSourceProxy::initialize(const QString& qFilename) return Mixxx::SoundSourcePointer(); //Failed to load plugin } #ifdef __SNDFILE__ - } else if (SoundSourceSndFile::supportedFileExtensions().contains(type)) { - return Mixxx::SoundSourcePointer(new SoundSourceSndFile(url)); + } else if (Mixxx::SoundSourceSndFile::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new Mixxx::SoundSourceSndFile(url)); #endif } else { //Unsupported filetype return Mixxx::SoundSourcePointer(); @@ -218,7 +223,7 @@ Mixxx::SoundSourcePointer SoundSourceProxy::initialize(const QString& qFilename) // static QLibrary* SoundSourceProxy::getPlugin(QString lib_filename) -{ + { static QMutex mutex; QMutexLocker locker(&mutex); @@ -227,68 +232,72 @@ QLibrary* SoundSourceProxy::getPlugin(QString lib_filename) } QScopedPointer plugin(new QLibrary(lib_filename)); if (!plugin->load()) { - qDebug() << "Failed to dynamically load" << lib_filename << plugin->errorString(); - return NULL; + qDebug() << "Failed to dynamically load" << lib_filename + << plugin->errorString(); + return NULL; } qDebug() << "Dynamically loaded" << lib_filename; bool incompatible = false; //Plugin API version check getSoundSourceAPIVersionFunc getver = (getSoundSourceAPIVersionFunc) - plugin->resolve("getSoundSourceAPIVersion"); + plugin->resolve("getSoundSourceAPIVersion"); if (getver) { - int pluginAPIVersion = getver(); - if (pluginAPIVersion != MIXXX_SOUNDSOURCE_API_VERSION) { - //SoundSource API version mismatch - incompatible = true; - } + int pluginAPIVersion = getver(); + if (pluginAPIVersion != MIXXX_SOUNDSOURCE_API_VERSION) { + //SoundSource API version mismatch + incompatible = true; + } } else { - //Missing getSoundSourceAPIVersion symbol - incompatible = true; + //Missing getSoundSourceAPIVersion symbol + incompatible = true; } if (incompatible) { - //Plugin is using an older/incompatible version of the - //plugin API! - qDebug() << "Plugin" << lib_filename << "is incompatible with your version of Mixxx!"; - return NULL; + //Plugin is using an older/incompatible version of the + //plugin API! + qDebug() << "Plugin" << lib_filename + << "is incompatible with your version of Mixxx!"; + return NULL; } //Map the file extensions this plugin supports onto a function //pointer to the "getter" function that gets a SoundSourceBlah. getSoundSourceFunc getter = (getSoundSourceFunc) - plugin->resolve("getSoundSource"); + plugin->resolve("getSoundSource"); // Getter function not found. if (getter == NULL) { - qDebug() << "ERROR: Couldn't resolve getter function. Plugin" - << lib_filename << "corrupt."; - return NULL; + qDebug() << "ERROR: Couldn't resolve getter function. Plugin" + << lib_filename << "corrupt."; + return NULL; } // Did you export it properly in your plugin? - getSupportedFileExtensionsFunc getFileExts = (getSupportedFileExtensionsFunc) - plugin->resolve("supportedFileExtensions"); + getSupportedFileExtensionsFunc getFileExts = + (getSupportedFileExtensionsFunc) + plugin->resolve("supportedFileExtensions"); if (getFileExts == NULL) { - qDebug() << "ERROR: Couldn't resolve getFileExts function. Plugin" - << lib_filename << "corrupt."; - return NULL; + qDebug() << "ERROR: Couldn't resolve getFileExts function. Plugin" + << lib_filename << "corrupt."; + return NULL; } freeFileExtensionsFunc freeFileExts = - reinterpret_cast( - plugin->resolve("freeFileExtensions")); + reinterpret_cast( + plugin->resolve("freeFileExtensions")); if (freeFileExts == NULL) { - qDebug() << "ERROR: Couldn't resolve freeFileExts function. Plugin" - << lib_filename << "corrupt."; - return NULL; + qDebug() << "ERROR: Couldn't resolve freeFileExts function. Plugin" + << lib_filename << "corrupt."; + return NULL; } char** supportedFileExtensions = getFileExts(); int i = 0; while (supportedFileExtensions[i] != NULL) { - qDebug() << "Plugin supports:" << supportedFileExtensions[i]; - m_extensionsSupportedByPlugins.insert(QString(supportedFileExtensions[i]), getter); - i++; + qDebug() << "Plugin supports:" << supportedFileExtensions[i]; + m_extensionsSupportedByPlugins.insert( + QString(supportedFileExtensions[i]), getter); + i++; } freeFileExts(supportedFileExtensions); @@ -303,42 +312,40 @@ QLibrary* SoundSourceProxy::getPlugin(QString lib_filename) return pPlugin; } - -Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource() const { +Mixxx::AudioSourcePointer SoundSourceProxy::open() const { if (!m_pSoundSource) { return Mixxx::AudioSourcePointer(); } - Mixxx::AudioSourcePointer pAudioSource(m_pSoundSource->open()); - if (!pAudioSource) { + if (OK != m_pSoundSource->open()) { qWarning() << "Failed to open SoundSource"; return Mixxx::AudioSourcePointer(); } - if (!pAudioSource->isValid()) { + if (!m_pSoundSource->isValid()) { qWarning() << "Invalid file:" << m_pSoundSource->getUrl() - << "channels" << pAudioSource->getChannelCount() - << "frame rate" << pAudioSource->getChannelCount(); + << "channels" << m_pSoundSource->getChannelCount() + << "frame rate" << m_pSoundSource->getChannelCount(); return Mixxx::AudioSourcePointer(); } - if (pAudioSource->isEmpty()) { + if (m_pSoundSource->isEmpty()) { qWarning() << "Empty file:" << m_pSoundSource->getUrl(); return Mixxx::AudioSourcePointer(); } // Overwrite metadata with actual audio properties if (m_pTrack) { - m_pTrack->setChannels(pAudioSource->getChannelCount()); - m_pTrack->setSampleRate(pAudioSource->getFrameRate()); - if (pAudioSource->hasDuration()) { - m_pTrack->setDuration(pAudioSource->getDuration()); + m_pTrack->setChannels(m_pSoundSource->getChannelCount()); + m_pTrack->setSampleRate(m_pSoundSource->getFrameRate()); + if (m_pSoundSource->hasDuration()) { + m_pTrack->setDuration(m_pSoundSource->getDuration()); } - if (pAudioSource->hasBitrate()) { - m_pTrack->setBitrate(pAudioSource->getBitrate()); + if (m_pSoundSource->hasBitrate()) { + m_pTrack->setBitrate(m_pSoundSource->getBitrate()); } } - return pAudioSource; + return m_pSoundSource; } // static @@ -347,23 +354,27 @@ QStringList SoundSourceProxy::supportedFileExtensions() QMutexLocker locker(&m_extensionsMutex); QList supportedFileExtensions; #ifdef __FFMPEGFILE__ - supportedFileExtensions.append(SoundSourceFFmpeg::supportedFileExtensions()); + supportedFileExtensions.append(Mixxx::SoundSourceFFmpeg::supportedFileExtensions()); #endif #ifdef __MAD__ - supportedFileExtensions.append(SoundSourceMp3::supportedFileExtensions()); + supportedFileExtensions.append( + Mixxx::SoundSourceMp3::supportedFileExtensions()); #endif - supportedFileExtensions.append(SoundSourceOggVorbis::supportedFileExtensions()); + supportedFileExtensions.append( + Mixxx::SoundSourceOggVorbis::supportedFileExtensions()); #ifdef __OPUS__ - supportedFileExtensions.append(SoundSourceOpus::supportedFileExtensions()); + supportedFileExtensions.append(Mixxx::SoundSourceOpus::supportedFileExtensions()); #endif #ifdef __SNDFILE__ - supportedFileExtensions.append(SoundSourceSndFile::supportedFileExtensions()); + supportedFileExtensions.append( + Mixxx::SoundSourceSndFile::supportedFileExtensions()); #endif #ifdef __COREAUDIO__ supportedFileExtensions.append(SoundSourceCoreAudio::supportedFileExtensions()); #endif #ifdef __MODPLUG__ - supportedFileExtensions.append(SoundSourceModPlug::supportedFileExtensions()); + supportedFileExtensions.append( + Mixxx::SoundSourceModPlug::supportedFileExtensions()); #endif supportedFileExtensions.append(m_extensionsSupportedByPlugins.keys()); @@ -380,17 +391,19 @@ QStringList SoundSourceProxy::supportedFileExtensionsByPlugins() { // static QString SoundSourceProxy::supportedFileExtensionsString() { - QStringList supportedFileExtList = SoundSourceProxy::supportedFileExtensions(); + QStringList supportedFileExtList = + SoundSourceProxy::supportedFileExtensions(); // Turn the list into a "*.mp3 *.wav *.etc" style string for (int i = 0; i < supportedFileExtList.size(); ++i) { - supportedFileExtList[i] = QString("*.%1").arg(supportedFileExtList[i]); + supportedFileExtList[i] = QString("*.%1").arg(supportedFileExtList[i]); } return supportedFileExtList.join(" "); } // static QString SoundSourceProxy::supportedFileExtensionsRegex() { - QStringList supportedFileExtList = SoundSourceProxy::supportedFileExtensions(); + QStringList supportedFileExtList = + SoundSourceProxy::supportedFileExtensions(); return RegexUtils::fileExtensionsRegex(supportedFileExtList); } diff --git a/src/soundsourceproxy.h b/src/soundsourceproxy.h index 3da0d95f976..867aab405c2 100644 --- a/src/soundsourceproxy.h +++ b/src/soundsourceproxy.h @@ -1,20 +1,3 @@ -/*************************************************************************** - soundsourceproxy.h - description - ------------------- - begin : Wed Oct 13 2004 - copyright : (C) 2004 by Tue Haste Andersen - email : - ***************************************************************************/ - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - #ifndef SOUNDSOURCEPROXY_H #define SOUNDSOURCEPROXY_H @@ -25,7 +8,7 @@ #include #include "trackinfoobject.h" -#include "sources/soundsource.h" +#include "sources/soundsourceplugin.h" #include "util/sandbox.h" /** @@ -36,8 +19,7 @@ /** * Creates sound sources for filenames or tracks. */ -class SoundSourceProxy -{ +class SoundSourceProxy: public Mixxx::MetadataSource { public: static void loadPlugins(); @@ -47,17 +29,43 @@ class SoundSourceProxy static QString supportedFileExtensionsRegex(); static bool isFilenameSupported(QString fileName); - SoundSourceProxy(QString qFilename, SecurityTokenPointer pToken); + explicit SoundSourceProxy(QString qFilename, SecurityTokenPointer pToken = SecurityTokenPointer()); explicit SoundSourceProxy(TrackPointer pTrack); - const Mixxx::SoundSourcePointer& getSoundSource() const { - return m_pSoundSource; + QString getType() const { + if (m_pSoundSource) { + return m_pSoundSource->getType(); + } else { + return QString(); + } + } + + Result parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/ { + if (m_pSoundSource) { + return m_pSoundSource->parseTrackMetadata(pMetadata); + } else { + return ERR; + } + } + + QImage parseCoverArt() const /*override*/ { + if (m_pSoundSource) { + return m_pSoundSource->parseCoverArt(); + } else { + return QImage(); + } } // Opening the audio data through the proxy will // update the some metadata of the track object. // Returns a null pointer on failure. - Mixxx::AudioSourcePointer openAudioSource() const; + Mixxx::AudioSourcePointer open() const; + + void close() const { + if (m_pSoundSource) { + m_pSoundSource->close(); + } + } private: static QRegExp m_supportedFileRegex; diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index f477f2051e8..90d358cf493 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -9,17 +9,6 @@ namespace Mixxx { /*static*/const CSAMPLE AudioSource::kSampleValuePeak = CSAMPLE_PEAK; -AudioSourcePointer AudioSource::onCreate(AudioSource* pNewAudioSource) { - AudioSourcePointer pAudioSource(pNewAudioSource); // take ownership - if (OK == pAudioSource->postConstruct()) { - // success - return pAudioSource; - } else { - // failure - return AudioSourcePointer(); - } -} - AudioSource::AudioSource(QUrl url) : UrlResource(url), m_channelCount(kChannelCountDefault), diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 4e3d3c0490d..5e6a5606ad4 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -239,10 +239,6 @@ class AudioSource: public UrlResource { protected: explicit AudioSource(QUrl url); - static AudioSourcePointer onCreate(AudioSource* pNewAudioSource); - - virtual Result postConstruct() /*override*/ = 0; - void setChannelCount(SINT channelCount); void setFrameRate(SINT frameRate); void setFrameCount(SINT frameCount); diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp deleted file mode 100644 index 9d0ffbc13ee..00000000000 --- a/src/sources/audiosourcecoreaudio.cpp +++ /dev/null @@ -1,170 +0,0 @@ -#include "sources/audiosourcecoreaudio.h" - -#include "util/math.h" - -#include - -namespace Mixxx { - -namespace { -SINT kChannelCount = 2; -} - -AudioSourceCoreAudio::AudioSourceCoreAudio(QUrl url) - : AudioSource(url), - m_headerFrames(0) { -} - -AudioSourceCoreAudio::~AudioSourceCoreAudio() { - close(); -} - -AudioSourcePointer AudioSourceCoreAudio::create(QUrl url) { - return onCreate(new AudioSourceCoreAudio(url)); -} - -// soundsource overrides -Result AudioSourceCoreAudio::postConstruct() { - const QString fileName(getLocalFileName()); - - //Open the audio file. - OSStatus err; - - /** This code blocks works with OS X 10.5+ only. DO NOT DELETE IT for now. */ - CFStringRef urlStr = CFStringCreateWithCharacters(0, - reinterpret_cast(fileName.unicode()), - fileName.size()); - CFURLRef urlRef = CFURLCreateWithFileSystemPath(NULL, urlStr, - kCFURLPOSIXPathStyle, false); - err = ExtAudioFileOpenURL(urlRef, &m_audioFile); - CFRelease(urlStr); - CFRelease(urlRef); - - /** TODO: Use FSRef for compatibility with 10.4 Tiger. - Note that ExtAudioFileOpen() is deprecated above Tiger, so we must maintain - both code paths if someone finishes this part of the code. - FSRef fsRef; - CFURLGetFSRef(reinterpret_cast(url.get()), &fsRef); - err = ExtAudioFileOpen(&fsRef, &m_audioFile); - */ - - if (err != noErr) { - qDebug() << "SSCA: Error opening file " << fileName; - return ERR; - } - - // get the input file format - UInt32 inputFormatSize = sizeof(m_inputFormat); - err = ExtAudioFileGetProperty(m_audioFile, - kExtAudioFileProperty_FileDataFormat, &inputFormatSize, - &m_inputFormat); - if (err != noErr) { - qDebug() << "SSCA: Error getting file format (" << fileName << ")"; - return ERR; - } - - // create the output format - m_outputFormat = CAStreamBasicDescription(m_inputFormat.mSampleRate, - kChannelCount, CAStreamBasicDescription::kPCMFormatFloat32, true); - - // set the client format - err = ExtAudioFileSetProperty(m_audioFile, - kExtAudioFileProperty_ClientDataFormat, sizeof(m_outputFormat), - &m_outputFormat); - if (err != noErr) { - qDebug() << "SSCA: Error setting file property"; - return ERR; - } - - //get the total length in frames of the audio file - copypasta: http://discussions.apple.com/thread.jspa?threadID=2364583&tstart=47 - SInt64 totalFrameCount; - UInt32 totalFrameCountSize = sizeof(totalFrameCount); - err = ExtAudioFileGetProperty(m_audioFile, - kExtAudioFileProperty_FileLengthFrames, &totalFrameCountSize, - &totalFrameCount); - if (err != noErr) { - qDebug() << "SSCA: Error getting number of frames"; - return ERR; - } - - // - // WORKAROUND for bug in ExtFileAudio - // - - AudioConverterRef acRef; - UInt32 acrsize = sizeof(AudioConverterRef); - err = ExtAudioFileGetProperty(m_audioFile, - kExtAudioFileProperty_AudioConverter, &acrsize, &acRef); - //_ThrowExceptionIfErr(@"kExtAudioFileProperty_AudioConverter", err); - - AudioConverterPrimeInfo primeInfo; - UInt32 piSize = sizeof(AudioConverterPrimeInfo); - memset(&primeInfo, 0, piSize); - err = AudioConverterGetProperty(acRef, kAudioConverterPrimeInfo, &piSize, - &primeInfo); - if (err != kAudioConverterErr_PropertyNotSupported) { // Only if decompressing - //_ThrowExceptionIfErr(@"kAudioConverterPrimeInfo", err); - m_headerFrames = primeInfo.leadingFrames; - } - - setChannelCount(m_outputFormat.NumberChannels()); - setFrameRate(m_inputFormat.mSampleRate); - // NOTE(uklotzde): This is what I found when migrating - // the code from SoundSource (sample-oriented) to the new - // AudioSource (frame-oriented) API. It is not documented - // when m_headerFrames > 0 and what the consequences are. - setFrameCount(totalFrameCount/* - m_headerFrames*/); - - //Seek to position 0, which forces us to skip over all the header frames. - //This makes sure we're ready to just let the Analyser rip and it'll - //get the number of samples it expects (ie. no header frames). - seekSampleFrame(0); - - return OK; -} - -void AudioSourceCoreAudio::preDestroy() { - ExtAudioFileDispose(m_audioFile); -} - -SINT AudioSourceCoreAudio::seekSampleFrame( - SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); - //_ThrowExceptionIfErr(@"ExtAudioFileSeek", err); - //qDebug() << "SSCA: Seeking to" << frameIndex; - if (err != noErr) { - qDebug() << "SSCA: Error seeking to" << frameIndex; // << GetMacOSStatusErrorString(err) << GetMacOSStatusCommentString(err); - } - return frameIndex; -} - -SINT AudioSourceCoreAudio::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - //if (!m_decoder) return 0; - SINT numFramesRead = 0; - - while (numFramesRead < numberOfFrames) { - SINT numFramesToRead = numberOfFrames - numFramesRead; - - AudioBufferList fillBufList; - fillBufList.mNumberBuffers = 1; - fillBufList.mBuffers[0].mNumberChannels = getChannelCount(); - fillBufList.mBuffers[0].mDataByteSize = frames2samples(numFramesToRead) - * sizeof(sampleBuffer[0]); - fillBufList.mBuffers[0].mData = sampleBuffer - + frames2samples(numFramesRead); - - UInt32 numFramesToReadInOut = numFramesToRead; // input/output parameter - OSStatus err = ExtAudioFileRead(m_audioFile, &numFramesToReadInOut, - &fillBufList); - if (0 == numFramesToReadInOut) { - // EOF - break;// done - } - numFramesRead += numFramesToReadInOut; - } - return numFramesRead; -} - -} // namespace Mixxx diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h deleted file mode 100644 index 03df23fc9a7..00000000000 --- a/src/sources/audiosourcecoreaudio.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef AUDIOSOURCECOREAUDIO_H -#define AUDIOSOURCECOREAUDIO_H - -#include "sources/audiosource.h" - -#include -//In our tree at lib/apple/ -#include "CAStreamBasicDescription.h" - -#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) -#include -#include -#include -#include -#else -#include "CoreAudioTypes.h" -#include "AudioFile.h" -#include "AudioFormat.h" -#endif - -namespace Mixxx { - -class AudioSourceCoreAudio: public AudioSource { -public: - static AudioSourcePointer create(QUrl url); - - ~AudioSourceCoreAudio(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) /*override*/; - -private: - explicit AudioSourceCoreAudio(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - ExtAudioFileRef m_audioFile; - CAStreamBasicDescription m_inputFormat; - CAStreamBasicDescription m_outputFormat; - SInt64 m_headerFrames; -}; - -} - -#endif // ifndef AUDIOSOURCECOREAUDIO_H diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp deleted file mode 100644 index fdd0cb53b59..00000000000 --- a/src/sources/audiosourceffmpeg.cpp +++ /dev/null @@ -1,502 +0,0 @@ -#include "sources/audiosourceffmpeg.h" - -#include - -#include - -#define AUDIOSOURCEFFMPEG_CACHESIZE 1000 -#define AUDIOSOURCEFFMPEG_POSDISTANCE ((1024 * 1000) / 8) - -namespace Mixxx { - -AudioSourceFFmpeg::AudioSourceFFmpeg(QUrl url) - : AudioSource(url), - m_pFormatCtx(NULL), - m_iAudioStream(-1), - m_pCodecCtx(NULL), - m_pCodec(NULL), - m_pResample(NULL), - m_iCurrentMixxTs(0), - m_bIsSeeked(false), - m_lCacheBytePos(0), - m_lCacheStartByte(0), - m_lCacheEndByte(0), - m_lCacheLastPos(0), - m_lLastStoredPos(0), - m_lStoredSeekPoint(-1) { -} - -AudioSourceFFmpeg::~AudioSourceFFmpeg() { - preDestroy(); -} - -AudioSourcePointer AudioSourceFFmpeg::create(QUrl url) { - return onCreate(new AudioSourceFFmpeg(url)); -} - -Result AudioSourceFFmpeg::postConstruct() { - unsigned int i; - AVDictionary *l_iFormatOpts = NULL; - - const QByteArray qBAFilename(getLocalFileNameBytes()); - - qDebug() << "New AudioSourceFFmpeg :" << qBAFilename; - - m_pFormatCtx = avformat_alloc_context(); - -#if LIBAVCODEC_VERSION_INT < 3622144 - m_pFormatCtx->max_analyze_duration = 999999999; -#endif - - // Open file and make m_pFormatCtx - if (avformat_open_input(&m_pFormatCtx, qBAFilename.constData(), NULL, - &l_iFormatOpts)!=0) { - qDebug() << "av_open_input_file: cannot open" << qBAFilename; - return ERR; - } - -#if LIBAVCODEC_VERSION_INT > 3544932 - av_dict_free(&l_iFormatOpts); -#endif - - // Retrieve stream information - if (avformat_find_stream_info(m_pFormatCtx, NULL)<0) { - qDebug() << "av_find_stream_info: cannot open" << qBAFilename; - return ERR; - } - - //debug only (Enable if needed) - //av_dump_format(m_pFormatCtx, 0, qBAFilename.constData(), false); - - // Find the first audio stream - m_iAudioStream=-1; - - for (i=0; inb_streams; i++) - if (m_pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) { - m_iAudioStream=i; - break; - } - if (m_iAudioStream==-1) { - qDebug() << "ffmpeg: cannot find an audio stream: cannot open" - << qBAFilename; - return ERR; - } - - // Get a pointer to the codec context for the audio stream - m_pCodecCtx=m_pFormatCtx->streams[m_iAudioStream]->codec; - - // Find the decoder for the audio stream - if (!(m_pCodec=avcodec_find_decoder(m_pCodecCtx->codec_id))) { - qDebug() << "ffmpeg: cannot find a decoder for" << qBAFilename; - return ERR; - } - - if (avcodec_open2(m_pCodecCtx, m_pCodec, NULL)<0) { - qDebug() << "ffmpeg: cannot open" << qBAFilename; - return ERR; - } - - m_pResample = new EncoderFfmpegResample(m_pCodecCtx); - // TODO(XXX): Use AV_SAMPLE_FMT_FLT instead of AV_SAMPLE_FMT_S16 - m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_S16); - - setChannelCount(m_pCodecCtx->channels); - setFrameRate(m_pCodecCtx->sample_rate); - setFrameCount((m_pFormatCtx->duration * m_pCodecCtx->sample_rate) / AV_TIME_BASE); - - qDebug() << "ffmpeg: Samplerate: " << getFrameRate() << ", Channels: " << - getChannelCount() << "\n"; - if (getChannelCount() > 2) { - qDebug() << "ffmpeg: No support for more than 2 channels!"; - return ERR; - } - - return OK; -} - -void AudioSourceFFmpeg::preDestroy() { - clearCache(); - - if (m_pCodecCtx != NULL) { - qDebug() << "~AudioSourceFFmpeg(): Clear FFMPEG stuff"; - avcodec_close(m_pCodecCtx); - avformat_close_input(&m_pFormatCtx); - av_free(m_pFormatCtx); - m_pFormatCtx = NULL; - } - - if (m_pResample != NULL) { - qDebug() << "~AudioSourceFFmpeg(): Delete FFMPEG Resampler"; - delete m_pResample; - m_pResample = NULL; - } - - while (m_SJumpPoints.size() > 0) { - ffmpegLocationObject* l_SRmJmp = m_SJumpPoints[0]; - m_SJumpPoints.remove(0); - free(l_SRmJmp); - } -} - -void AudioSourceFFmpeg::clearCache() { - while (m_SCache.size() > 0) { - struct ffmpegCacheObject* l_SRmObj = m_SCache[0]; - m_SCache.remove(0); - free(l_SRmObj->bytes); - free(l_SRmObj); - } -} - -bool AudioSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { - unsigned int l_iCount = 0; - qint32 l_iRet = 0; - AVPacket l_SPacket; - AVFrame *l_pFrame = NULL; - bool l_iStop = false; - int l_iFrameFinished = 0; - struct ffmpegCacheObject *l_SObj = NULL; - struct ffmpegCacheObject *l_SRmObj = NULL; - bool m_bUnique = false; - qint64 l_lLastPacketPos = -1; - int l_iError = 0; - int l_iFrameCount = 0; - - l_iCount = count; - - l_SPacket.data = NULL; - l_SPacket.size = 0; - - - while (l_iCount > 0) { - if (l_pFrame != NULL) { - l_iFrameCount --; -// FFMPEG 2.2 3561060 anb beyond -#if LIBAVCODEC_VERSION_INT >= 3561060 - av_frame_free(&l_pFrame); -// FFMPEG 0.11 and below -#elif LIBAVCODEC_VERSION_INT <= 3544932 - av_free(l_pFrame); -// FFMPEG 1.0 - 2.1 -#else - avcodec_free_frame(&l_pFrame); -#endif - l_pFrame = NULL; - - } - - if (l_iStop == true) { - break; - } - l_iFrameCount ++; - av_init_packet(&l_SPacket); -#if LIBAVCODEC_VERSION_INT < 3617792 - l_pFrame = avcodec_alloc_frame(); -#else - l_pFrame = av_frame_alloc(); -#endif - - if (av_read_frame(m_pFormatCtx, &l_SPacket) >= 0) { - if (l_SPacket.stream_index == m_iAudioStream) { - if (m_lStoredSeekPoint > 0) { - // Seek for correct jump point - if (m_lStoredSeekPoint > l_SPacket.pos && - m_lStoredSeekPoint >= AUDIOSOURCEFFMPEG_POSDISTANCE) { - av_free_packet(&l_SPacket); - l_SPacket.data = NULL; - l_SPacket.size = 0; - continue; - } - m_lStoredSeekPoint = -1; - } - - l_iRet = avcodec_decode_audio4(m_pCodecCtx,l_pFrame,&l_iFrameFinished, - &l_SPacket); - - if (l_iRet <= 0) { - // An error or EOF occured,index break out and return what - // we have so far. - qDebug() << "EOF!"; - l_iStop = true; - continue; - } else { - l_iRet = 0; - l_SObj = (struct ffmpegCacheObject *)malloc(sizeof(struct ffmpegCacheObject)); - if (l_SObj == NULL) { - qDebug() << "AudioSourceFFmpeg::readFramesToCache: Not enough memory!"; - l_iStop = true; - continue; - } - memset(l_SObj, 0x00, sizeof(struct ffmpegCacheObject)); - l_iRet = m_pResample->reSample(l_pFrame, &l_SObj->bytes); - - if (l_iRet > 0) { - // Remove from cache - if (m_SCache.size() >= (AUDIOSOURCEFFMPEG_CACHESIZE - 10)) { - l_SRmObj = m_SCache[0]; - m_SCache.remove(0); - free(l_SRmObj->bytes); - free(l_SRmObj); - } - - // Add to cache and store byte place to memory - m_SCache.append(l_SObj); - l_SObj->startByte = m_lCacheBytePos / 2; - l_SObj->length = l_iRet / 2; - m_lCacheBytePos += l_iRet; - - // Ogg/Opus have packages pos that have many - // audio frames so seek next unique pos.. - if (l_SPacket.pos != l_lLastPacketPos) { - m_bUnique = true; - l_lLastPacketPos = l_SPacket.pos; - } - - // If we are over last storepos and we have read more than jump point need is and pos is unique we store it to memory - if (m_lCacheBytePos > m_lLastStoredPos && - m_lCacheBytePos > (AUDIOSOURCEFFMPEG_POSDISTANCE + m_lLastStoredPos) && - m_bUnique == true) { - struct ffmpegLocationObject *l_SJmp = (struct ffmpegLocationObject *)malloc( - sizeof(struct ffmpegLocationObject)); - m_lLastStoredPos = m_lCacheBytePos; - l_SJmp->startByte = m_lCacheBytePos / 2; - l_SJmp->pos = l_SPacket.pos; - l_SJmp->pts = l_SPacket.pts; - m_SJumpPoints.append(l_SJmp); - m_bUnique = false; - } - - if (offset < 0 || (quint64) offset <= (m_lCacheBytePos / 2)) { - l_iCount --; - } - } else { - free(l_SObj); - l_SObj = NULL; - qDebug() << - "AudioSourceFFmpeg::readFramesToCache: General error in audio decode:" << - l_iRet; - } - } - - av_free_packet(&l_SPacket); - l_SPacket.data = NULL; - l_SPacket.size = 0; - - } else { - l_iError ++; - if (l_iError == 5) { - // Stream end and we couldn't read enough frames - l_iStop = true; - } - } - - - } else { - qDebug() << "AudioSourceFFmpeg::readFramesToCache: Packet too big or File end"; - l_iStop = true; - } - - } - - if (l_pFrame != NULL) { - l_iFrameCount --; -// FFMPEG 2.2 3561060 anb beyond -#if LIBAVCODEC_VERSION_INT >= 3561060 - av_frame_unref(l_pFrame); - av_frame_free(&l_pFrame); -// FFMPEG 0.11 and below -#elif LIBAVCODEC_VERSION_INT <= 3544932 - av_free(l_pFrame); -// FFMPEG 1.0 - 2.1 -#else - avcodec_free_frame(&l_pFrame); -#endif - l_pFrame = NULL; - - } - - if (l_iFrameCount > 0) { - qDebug() << "AudioSourceFFmpeg::readFramesToCache(): Frame balance is not 0 it is: " << l_iFrameCount; - } - - l_SObj = m_SCache.first(); - m_lCacheStartByte = l_SObj->startByte; - l_SObj = m_SCache.last(); - m_lCacheEndByte = (l_SObj->startByte + l_SObj->length); - - if (!l_iCount) { - return true; - } else { - return false; - } -} - -bool AudioSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, - quint64 size) { - struct ffmpegCacheObject *l_SObj = NULL; - quint32 l_lPos = 0; - quint32 l_lLeft = 0; - quint32 l_lOffset = 0; - quint32 l_lBytesToCopy = 0; - - if (offset >= m_lCacheStartByte) { - if (m_lCacheLastPos == 0) { - m_lCacheLastPos = m_SCache.size() - 1; - } - for (l_lPos = m_lCacheLastPos; l_lPos > 0; l_lPos --) { - l_SObj = m_SCache[l_lPos]; - if ((l_SObj->startByte + l_SObj->length) < offset) { - break; - } - } - - l_SObj = m_SCache[l_lPos]; - - l_lLeft = (size * 2); - memset(buffer, 0x00, l_lLeft); - while (l_lLeft > 0) { - - if (l_SObj == NULL || (l_lPos + 5) > (unsigned int)m_SCache.size()) { - offset = l_SObj->startByte; - if (readFramesToCache(50, -1) == false) { - return false; - } - for (l_lPos = (m_SCache.size() - 50); l_lPos > 0; l_lPos --) { - l_SObj = m_SCache[l_lPos]; - if ((l_SObj->startByte + l_SObj->length) < offset) { - break; - } - } - l_SObj = m_SCache[l_lPos]; - continue; - } - - if (l_SObj->startByte <= offset) { - l_lOffset = (offset - l_SObj->startByte) * 2; - } - - if (l_lOffset >= (l_SObj->length * 2)) { - l_SObj = m_SCache[++ l_lPos]; - continue; - } - - if (l_lLeft > (l_SObj->length * 2)) { - l_lBytesToCopy = ((l_SObj->length * 2) - l_lOffset); - memcpy(buffer, (l_SObj->bytes + l_lOffset), l_lBytesToCopy); - l_lOffset = 0; - buffer += l_lBytesToCopy; - l_lLeft -= l_lBytesToCopy; - } else { - memcpy(buffer, l_SObj->bytes, l_lLeft); - l_lLeft = 0; - } - - l_SObj = m_SCache[++ l_lPos]; - } - - m_lCacheLastPos = --l_lPos; - return true; - } - - return false; -} - -SINT AudioSourceFFmpeg::seekSampleFrame(SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - - const SINT filepos = frames2samples(frameIndex); - - int ret = 0; - qint64 i = 0; - - if (filepos < 0 || (unsigned long) filepos < m_lCacheStartByte) { - ret = avformat_seek_file(m_pFormatCtx, - m_iAudioStream, - 0, - 32767 * 2, - 32767 * 2, - AVSEEK_FLAG_BACKWARD); - - - if (ret < 0) { - qDebug() << "AudioSourceFFmpeg::seek: Can't seek to 0 byte!"; - return -1; - } - - clearCache(); - m_lCacheStartByte = 0; - m_lCacheEndByte = 0; - m_lCacheLastPos = 0; - m_lCacheBytePos = 0; - m_lStoredSeekPoint = -1; - - - // Try to find some jump point near to - // where we are located so we don't needed - // to try guess it - if (filepos >= AUDIOSOURCEFFMPEG_POSDISTANCE) { - for (i = 0; i < m_SJumpPoints.size(); i ++) { - if (m_SJumpPoints[i]->startByte >= (unsigned long) filepos && i > 2) { - m_lCacheBytePos = m_SJumpPoints[i - 2]->startByte * 2; - m_lStoredSeekPoint = m_SJumpPoints[i - 2]->pos; - break; - } - } - } - - if (filepos == 0) { - readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE - 50), -1); - } else { - readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE / 2), filepos); - } - } - - - if (m_lCacheEndByte <= (unsigned long) filepos) { - readFramesToCache(100, filepos); - } - - m_iCurrentMixxTs = filepos; - - m_bIsSeeked = TRUE; - - return frameIndex; -} - -unsigned int AudioSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { - - if (m_SCache.size() == 0) { - // Make sure we allways start at begining and cache have some - // material that we can consume. - seekSampleFrame(0); - m_bIsSeeked = FALSE; - } - - getBytesFromCache((char *)destination, m_iCurrentMixxTs, size); - - // As this is also Hack - // If we don't seek like we don't on analyzer.. keep - // place in mind.. - if (m_bIsSeeked == FALSE) { - m_iCurrentMixxTs += size; - } - - m_bIsSeeked = FALSE; - return size; -} - -SINT AudioSourceFFmpeg::readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) { - // This is just a hack that simply reuses existing - // functionality. Sample data should be resampled - // directly into AV_SAMPLE_FMT_FLT instead of - // AV_SAMPLE_FMT_S16! - typedef std::vector TempBuffer; - TempBuffer tempBuffer(frames2samples(numberOfFrames)); - const SINT readSamples = read(tempBuffer.size(), &tempBuffer[0]); - for (SINT i = 0; i < readSamples; ++i) { - sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / CSAMPLE(SAMPLE_MAX); - } - return samples2frames(readSamples); -} - -} // namespace Mixxx diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h deleted file mode 100644 index 437974abc6d..00000000000 --- a/src/sources/audiosourceffmpeg.h +++ /dev/null @@ -1,90 +0,0 @@ -#ifndef AUDIOSOURCEFFMPEG_H -#define AUDIOSOURCEFFMPEG_H - -#include "sources/audiosource.h" - -#include - -// Needed to ensure that macros in get defined. -#ifndef __STDC_CONSTANT_MACROS -#if __cplusplus < 201103L -#define __STDC_CONSTANT_MACROS -#endif -#endif - -#include -#include - -#ifndef __FFMPEGOLDAPI__ -#include -#include -#endif - -// Compability -#include -#include - -#include - -namespace Mixxx { - -struct ffmpegLocationObject { - quint64 pos; - qint64 pts; - quint64 startByte; -}; - -struct ffmpegCacheObject { - quint64 startByte; - quint32 length; - quint8 *bytes; -}; - -class AudioSourceFFmpeg : public AudioSource { -public: - static AudioSourcePointer create(QUrl url); - - ~AudioSourceFFmpeg(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; - -private: - explicit AudioSourceFFmpeg(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - bool readFramesToCache(unsigned int count, qint64 offset); - bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); - quint64 getSizeofCache(); - void clearCache(); - - unsigned int read(unsigned long size, SAMPLE*); - - AVFormatContext *m_pFormatCtx; - int m_iAudioStream; - AVCodecContext *m_pCodecCtx; - AVCodec *m_pCodec; - - EncoderFfmpegResample *m_pResample; - - qint64 m_iCurrentMixxTs; - - bool m_bIsSeeked; - - quint64 m_lCacheBytePos; - quint64 m_lCacheStartByte; - quint64 m_lCacheEndByte; - quint32 m_lCacheLastPos; - QVector m_SCache; - QVector m_SJumpPoints; - quint64 m_lLastStoredPos; - qint64 m_lStoredSeekPoint; -}; - -} - -#endif diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp deleted file mode 100644 index 9f48d5db89a..00000000000 --- a/src/sources/audiosourceflac.cpp +++ /dev/null @@ -1,436 +0,0 @@ -#include "sources/audiosourceflac.h" - -#include "sampleutil.h" -#include "util/math.h" - -#include - -namespace Mixxx { - -namespace { -// begin callbacks (have to be regular functions because normal libFLAC isn't C++-aware) - -FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder*, - FLAC__byte buffer[], size_t* bytes, void* client_data) { - return static_cast(client_data)->flacRead(buffer, bytes); -} - -FLAC__StreamDecoderSeekStatus FLAC_seek_cb(const FLAC__StreamDecoder*, - FLAC__uint64 absolute_byte_offset, void* client_data) { - return static_cast(client_data)->flacSeek( - absolute_byte_offset); -} - -FLAC__StreamDecoderTellStatus FLAC_tell_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *absolute_byte_offset, void* client_data) { - return static_cast(client_data)->flacTell( - absolute_byte_offset); -} - -FLAC__StreamDecoderLengthStatus FLAC_length_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *stream_length, void* client_data) { - return static_cast(client_data)->flacLength(stream_length); -} - -FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder*, void* client_data) { - return static_cast(client_data)->flacEOF(); -} - -FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder*, - const FLAC__Frame* frame, const FLAC__int32* const buffer[], - void* client_data) { - return static_cast(client_data)->flacWrite(frame, buffer); -} - -void FLAC_metadata_cb(const FLAC__StreamDecoder*, - const FLAC__StreamMetadata* metadata, void* client_data) { - static_cast(client_data)->flacMetadata(metadata); -} - -void FLAC_error_cb(const FLAC__StreamDecoder*, - FLAC__StreamDecoderErrorStatus status, void* client_data) { - static_cast(client_data)->flacError(status); -} - -// end callbacks - -const unsigned kBitsPerSampleDefault = 0; - -} - - -AudioSourceFLAC::AudioSourceFLAC(QUrl url) - : AudioSource(url), - m_file(getLocalFileName()), - m_decoder(NULL), - m_minBlocksize(0), - m_maxBlocksize(0), - m_minFramesize(0), - m_maxFramesize(0), - m_bitsPerSample(kBitsPerSampleDefault), - m_sampleScale(kSampleValueZero), - m_decodeSampleBufferReadOffset(0), - m_decodeSampleBufferWriteOffset(0), - m_curFrameIndex(kFrameIndexMin) { -} - -AudioSourceFLAC::~AudioSourceFLAC() { - preDestroy(); -} - -AudioSourcePointer AudioSourceFLAC::create(QUrl url) { - return onCreate(new AudioSourceFLAC(url)); -} - -Result AudioSourceFLAC::postConstruct() { - if (!m_file.open(QIODevice::ReadOnly)) { - qWarning() << "SSFLAC: Could not read file!"; - return ERR; - } - - m_decoder = FLAC__stream_decoder_new(); - if (m_decoder == NULL) { - qWarning() << "SSFLAC: decoder allocation failed!"; - return ERR; - } - FLAC__stream_decoder_set_md5_checking(m_decoder, FALSE); - const FLAC__StreamDecoderInitStatus initStatus( - FLAC__stream_decoder_init_stream(m_decoder, FLAC_read_cb, - FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, - FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); - if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { - qWarning() << "SSFLAC: decoder init failed:" << initStatus; - return ERR; - } - if (!FLAC__stream_decoder_process_until_end_of_metadata(m_decoder)) { - qWarning() << "SSFLAC: process to end of meta failed:" - << FLAC__stream_decoder_get_state(m_decoder); - return ERR; - } - - return OK; -} - -void AudioSourceFLAC::preDestroy() { - if (m_decoder) { - FLAC__stream_decoder_finish(m_decoder); - FLAC__stream_decoder_delete(m_decoder); // frees memory - m_decoder = NULL; - } - m_file.close(); -} - -SINT AudioSourceFLAC::seekSampleFrame( - SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - - // clear decode buffer before seeking - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; - if (!FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex)) { - qWarning() << "SSFLAC: Seeking error at file" << m_file.fileName(); - } - if ((FLAC__STREAM_DECODER_SEEK_ERROR == FLAC__stream_decoder_get_state(m_decoder)) && - !FLAC__stream_decoder_flush(m_decoder)) { - qWarning() << "SSFLAC: Failed to flush the decoder's input buffer after seeking" << m_file.fileName(); - } - m_curFrameIndex = frameIndex; - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - return m_curFrameIndex; -} - -SINT AudioSourceFLAC::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, - frames2samples(numberOfFrames), false); -} - -SINT AudioSourceFLAC::readSampleFramesStereo( - SINT numberOfFrames, CSAMPLE* sampleBuffer, - SINT sampleBufferSize) { - return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, - true); -} - -SINT AudioSourceFLAC::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer, - SINT sampleBufferSize, bool readStereoSamples) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); - - const SINT numberOfFramesTotal = numberOfFrames; - - CSAMPLE* outBuffer = sampleBuffer; - SINT numberOfFramesRemaining = numberOfFramesTotal; - while (0 < numberOfFramesRemaining) { - DEBUG_ASSERT( - m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - // if our buffer from libflac is empty (either because we explicitly cleared - // it or because we've simply used all the samples), ask for a new buffer - if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { - // Documentation of FLAC__stream_decoder_process_single(): - // "Depending on what was decoded, the metadata or write callback - // will be called with the decoded metadata block or audio frame." - // See also: https://xiph.org/flac/api/group__flac__stream__decoder.html#ga9d6df4a39892c05955122cf7f987f856 - if (FLAC__stream_decoder_process_single(m_decoder)) { - if (m_decodeSampleBufferReadOffset - >= m_decodeSampleBufferWriteOffset) { - // EOF - break; - } - } else { - qWarning() << "SSFLAC: decoder_process_single returned false (" - << m_file.fileName() << ")"; - break; - } - } - DEBUG_ASSERT( - m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - const SINT decodeBufferSamples = m_decodeSampleBufferWriteOffset - - m_decodeSampleBufferReadOffset; - const SINT decodeBufferFrames = samples2frames( - decodeBufferSamples); - const SINT framesToCopy = - math_min(decodeBufferFrames, numberOfFramesRemaining); - const SINT samplesToCopy = frames2samples(framesToCopy); - if (readStereoSamples && !isChannelCountStereo()) { - if (isChannelCountMono()) { - SampleUtil::copyMonoToDualMono(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy); - } else { - SampleUtil::copyMultiToStereo(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy, getChannelCount()); - } - outBuffer += framesToCopy * 2; // copied 2 samples per frame - } else { - SampleUtil::copy(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - samplesToCopy); - outBuffer += samplesToCopy; - } - m_decodeSampleBufferReadOffset += samplesToCopy; - m_curFrameIndex += framesToCopy; - numberOfFramesRemaining -= framesToCopy; - DEBUG_ASSERT( - m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - } - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); - return numberOfFramesTotal - numberOfFramesRemaining; -} - -// flac callback methods -FLAC__StreamDecoderReadStatus AudioSourceFLAC::flacRead(FLAC__byte buffer[], - size_t* bytes) { - *bytes = m_file.read((char*) buffer, *bytes); - if (*bytes > 0) { - return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; - } else if (*bytes == 0) { - return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; - } else { - return FLAC__STREAM_DECODER_READ_STATUS_ABORT; - } -} - -FLAC__StreamDecoderSeekStatus AudioSourceFLAC::flacSeek(FLAC__uint64 offset) { - if (m_file.seek(offset)) { - return FLAC__STREAM_DECODER_SEEK_STATUS_OK; - } else { - qWarning() << "SSFLAC: An unrecoverable error occurred (" - << m_file.fileName() << ")"; - return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR; - } -} - -FLAC__StreamDecoderTellStatus AudioSourceFLAC::flacTell(FLAC__uint64* offset) { - if (m_file.isSequential()) { - return FLAC__STREAM_DECODER_TELL_STATUS_UNSUPPORTED; - } - *offset = m_file.pos(); - return FLAC__STREAM_DECODER_TELL_STATUS_OK; -} - -FLAC__StreamDecoderLengthStatus AudioSourceFLAC::flacLength( - FLAC__uint64* length) { - if (m_file.isSequential()) { - return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; - } - *length = m_file.size(); - return FLAC__STREAM_DECODER_LENGTH_STATUS_OK; -} - -FLAC__bool AudioSourceFLAC::flacEOF() { - if (m_file.isSequential()) { - return false; - } - return m_file.atEnd(); -} - -FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( - const FLAC__Frame* frame, const FLAC__int32* const buffer[]) { - // decode buffer must be empty before decoding the next frame - DEBUG_ASSERT(m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset); - // reset decode buffer - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; - if (getChannelCount() != frame->header.channels) { - qWarning() << "Corrupt or unsupported FLAC file:" - << "Invalid number of channels in FLAC frame header" - << frame->header.channels << "<>" << getChannelCount(); - return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; - } - if (getFrameRate() != frame->header.sample_rate) { - qWarning() << "Corrupt or unsupported FLAC file:" - << "Invalid sample rate in FLAC frame header" - << frame->header.sample_rate << "<>" << getFrameRate(); - return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; - } - const unsigned maxBlocksize = samples2frames( - m_decodeSampleBuffer.size()); - if (maxBlocksize < frame->header.blocksize) { - qWarning() << "Corrupt or unsupported FLAC file:" - << "Block size in FLAC frame header exceeds the maximum block size" - << frame->header.blocksize << ">" << maxBlocksize; - } - switch (getChannelCount()) { - case 1: { - // optimized code for 1 channel (mono) - DEBUG_ASSERT(1 <= frame->header.channels); - for (unsigned i = 0; i < frame->header.blocksize; ++i) { - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[0][i] * m_sampleScale; - } - break; - } - case 2: { - // optimized code for 2 channels (stereo) - DEBUG_ASSERT(2 <= frame->header.channels); - for (unsigned i = 0; i < frame->header.blocksize; ++i) { - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[0][i] * m_sampleScale; - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[1][i] * m_sampleScale; - } - break; - } - default: { - // generic code for multiple channels - DEBUG_ASSERT(getChannelCount() == frame->header.channels); - for (unsigned i = 0; i < frame->header.blocksize; ++i) { - for (unsigned j = 0; j < frame->header.channels; ++j) { - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[j][i] * m_sampleScale; - } - } - } - } - DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; -} - -void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { - // https://xiph.org/flac/api/group__flac__stream__decoder.html#ga43e2329c15731c002ac4182a47990f85 - // "...one STREAMINFO block, followed by zero or more other metadata blocks." - // "...by default the decoder only calls the metadata callback for the STREAMINFO block..." - // "...always before the first audio frame (i.e. write callback)." - switch (metadata->type) { - case FLAC__METADATA_TYPE_STREAMINFO: - { - const SINT channelCount = metadata->data.stream_info.channels; - DEBUG_ASSERT(kChannelCountDefault != channelCount); - if (getChannelCount() == kChannelCountDefault) { - // not set before - setChannelCount(channelCount); - } else { - // already set before -> check for consistency - if (getChannelCount() != channelCount) { - qWarning() << "Unexpected channel count:" - << channelCount << " <> " << getChannelCount(); - } - } - const SINT frameRate = metadata->data.stream_info.sample_rate; - DEBUG_ASSERT(kFrameRateDefault != frameRate); - if (getFrameRate() == kFrameRateDefault) { - // not set before - setFrameRate(frameRate); - } else { - // already set before -> check for consistency - if (getFrameRate() != frameRate) { - qWarning() << "Unexpected frame/sample rate:" - << frameRate << " <> " << getFrameRate(); - } - } - const SINT frameCount = metadata->data.stream_info.total_samples; - DEBUG_ASSERT(kFrameCountDefault != frameCount); - if (getFrameCount() == kFrameCountDefault) { - // not set before - setFrameCount(frameCount); - } else { - // already set before -> check for consistency - if (getFrameCount() != frameCount) { - qWarning() << "Unexpected frame count:" - << frameCount << " <> " << getFrameCount(); - } - } - const unsigned bitsPerSample = metadata->data.stream_info.bits_per_sample; - DEBUG_ASSERT(kBitsPerSampleDefault != bitsPerSample); - if (kBitsPerSampleDefault == m_bitsPerSample) { - // not set before - m_bitsPerSample = bitsPerSample; - m_sampleScale = kSampleValuePeak - / CSAMPLE(FLAC__int32(1) << bitsPerSample); - } else { - // already set before -> check for consistency - if (bitsPerSample != m_bitsPerSample) { - qWarning() << "Unexpected bits per sample:" - << bitsPerSample << " <> " << m_bitsPerSample; - } - } - m_minBlocksize = metadata->data.stream_info.min_blocksize; - m_maxBlocksize = metadata->data.stream_info.max_blocksize; - m_minFramesize = metadata->data.stream_info.min_framesize; - m_maxFramesize = metadata->data.stream_info.max_framesize; - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; - const unsigned decodeSampleBufferSize = - m_maxBlocksize * getChannelCount(); - SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); - break; - } - default: - // Ignore all other metadata types - break; - } -} - -void AudioSourceFLAC::flacError(FLAC__StreamDecoderErrorStatus status) { - QString error; - // not much can be done at this point -- luckly the decoder seems to be - // pretty forgiving -- bkgood - switch (status) { - case FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC: - error = "STREAM_DECODER_ERROR_STATUS_LOST_SYNC"; - break; - case FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER: - error = "STREAM_DECODER_ERROR_STATUS_BAD_HEADER"; - break; - case FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH: - error = "STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH"; - break; - case FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM: - error = "STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM"; - break; - } - qWarning() << "SSFLAC got error" << error << "from libFLAC for file" - << m_file.fileName(); - // not much else to do here... whatever function that initiated whatever - // decoder method resulted in this error will return an error, and the caller - // will bail. libFLAC docs say to not close the decoder here -- bkgood -} - -} // namespace Mixxx diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h deleted file mode 100644 index 9c42104682b..00000000000 --- a/src/sources/audiosourceflac.h +++ /dev/null @@ -1,73 +0,0 @@ -#ifndef AUDIOSOURCEFLAC_H -#define AUDIOSOURCEFLAC_H - -#include "sources/audiosource.h" -#include "samplebuffer.h" - -#include - -#include - -namespace Mixxx { - -class AudioSourceFLAC: public AudioSource { -public: - static AudioSourcePointer create(QUrl url); - - ~AudioSourceFLAC(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) /*override*/; - SINT readSampleFramesStereo(SINT numberOfFrames, - CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; - - // callback methods - FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t* bytes); - FLAC__StreamDecoderSeekStatus flacSeek(FLAC__uint64 offset); - FLAC__StreamDecoderTellStatus flacTell(FLAC__uint64* offset); - FLAC__StreamDecoderLengthStatus flacLength(FLAC__uint64* length); - FLAC__bool flacEOF(); - FLAC__StreamDecoderWriteStatus flacWrite(const FLAC__Frame *frame, - const FLAC__int32* const buffer[]); - void flacMetadata(const FLAC__StreamMetadata* metadata); - void flacError(FLAC__StreamDecoderErrorStatus status); - -private: - explicit AudioSourceFLAC(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer, SINT sampleBufferSize, - bool readStereoSamples); - - QFile m_file; - - FLAC__StreamDecoder *m_decoder; - // misc bits about the flac format: - // flac encodes from and decodes to LPCM in blocks, each block is made up of - // subblocks (one for each chan) - // flac stores in 'frames', each of which has a header and a certain number - // of subframes (one for each channel) - unsigned m_minBlocksize; // in time samples (audio samples = time samples * chanCount) - unsigned m_maxBlocksize; - unsigned m_minFramesize; - unsigned m_maxFramesize; - unsigned m_bitsPerSample; - - CSAMPLE m_sampleScale; - - SampleBuffer m_decodeSampleBuffer; - int m_decodeSampleBufferReadOffset; - int m_decodeSampleBufferWriteOffset; - - SINT m_curFrameIndex; -}; - -} - -#endif // ifndef AUDIOSOURCEFLAC_H diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp deleted file mode 100644 index 191e84d8bf5..00000000000 --- a/src/sources/audiosourcemodplug.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "sources/audiosourcemodplug.h" - -#include "util/timer.h" - -#include -#include - -#include -#include - -/* read files in 512k chunks */ -#define CHUNKSIZE (1 << 18) - -namespace Mixxx { - -// reserve some static space for settings... -namespace { -// identification of modplug module type -enum ModuleTypes { - NONE = 0x00, - MOD = 0x01, - S3M = 0x02, - XM = 0x04, - MED = 0x08, - IT = 0x20, - STM = 0x100, - OKT = 0x8000 -}; -} - -unsigned int AudioSourceModPlug::s_bufferSizeLimit = 0; - -void AudioSourceModPlug::configure(unsigned int bufferSizeLimit, - const ModPlug::ModPlug_Settings &settings) { - s_bufferSizeLimit = bufferSizeLimit; - ModPlug::ModPlug_SetSettings(&settings); -} - -AudioSourceModPlug::AudioSourceModPlug(QUrl url) - : AudioSource(url), - m_pModFile(NULL), - m_fileLength(0), - m_seekPos(0) { -} - -AudioSourceModPlug::~AudioSourceModPlug() { - preDestroy(); -} - -AudioSourcePointer AudioSourceModPlug::create(QUrl url) { - return onCreate(new AudioSourceModPlug(url)); -} - -Result AudioSourceModPlug::postConstruct() { - ScopedTimer t("AudioSourceModPlug::open()"); - - // read module file to byte array - const QString fileName(getLocalFileName()); - QFile modFile(fileName); - qDebug() << "[ModPlug] Loading ModPlug module " << modFile.fileName(); - modFile.open(QIODevice::ReadOnly); - m_fileBuf = modFile.readAll(); - modFile.close(); - // get ModPlugFile descriptor for later access - m_pModFile = ModPlug::ModPlug_Load(m_fileBuf.constData(), - m_fileBuf.length()); - - if (m_pModFile == NULL) { - // an error occured - t.cancel(); - qDebug() << "[ModPlug] Could not load module file: " << fileName; - return ERR; - } - - // estimate size of sample buffer (for better performance) - // beware: module length estimation is unreliable due to loops - // song milliseconds * 2 (bytes per sample) - // * 2 (channels) - // * 44.1 (samples per millisecond) - // + some more to accomodate short loops etc. - // approximate and align with CHUNKSIZE yields: - // (((milliseconds << 1) >> 10 /* to seconds */) - // div 11 /* samples to chunksize ratio */) - // << 19 /* align to chunksize */ - unsigned int estimate = ((ModPlug::ModPlug_GetLength(m_pModFile) >> 8) / 11) - << 18; - estimate = math_min(estimate, s_bufferSizeLimit); - m_sampleBuf.reserve(estimate); - qDebug() << "[ModPlug] Reserved " << m_sampleBuf.capacity() << " #samples"; - - // decode samples to sample buffer - int samplesRead = -1; - int currentSize = 0; - while ((samplesRead != 0) && (m_sampleBuf.size() < s_bufferSizeLimit)) { - // reserve enough space in sample buffer - m_sampleBuf.resize(currentSize + CHUNKSIZE); - samplesRead = ModPlug::ModPlug_Read(m_pModFile, - m_sampleBuf.data() + currentSize, - CHUNKSIZE * 2) / 2; - // adapt to actual size - currentSize += samplesRead; - if (samplesRead != CHUNKSIZE) { - m_sampleBuf.resize(currentSize); - samplesRead = 0; // we reached the end of the file - } - } - qDebug() << "[ModPlug] Filled Sample buffer with " << m_sampleBuf.size() - << " samples."; - qDebug() << "[ModPlug] Sample buffer has " - << m_sampleBuf.capacity() - m_sampleBuf.size() - << " samples unused capacity."; - - setChannelCount(kChannelCount); - setFrameRate(kFrameRate); - setFrameCount(samples2frames(m_sampleBuf.size())); - m_seekPos = 0; - - return OK; -} - -void AudioSourceModPlug::preDestroy() { - if (m_pModFile) { - ModPlug::ModPlug_Unload(m_pModFile); - m_pModFile = NULL; - } -} - -SINT AudioSourceModPlug::seekSampleFrame( - SINT frameIndex) { - return m_seekPos = frameIndex; -} - -SINT AudioSourceModPlug::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - const SINT maxFrames = samples2frames(m_sampleBuf.size()); - const SINT readFrames = math_min(maxFrames - m_seekPos, numberOfFrames); - - const SINT readSamples = frames2samples(readFrames); - const SINT readOffset = frames2samples(m_seekPos); - for (SINT i = 0; i < readSamples; ++i) { - sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) - / CSAMPLE(SAMPLE_MAX); - } - - m_seekPos += readFrames; - return readFrames; -} - -} // namespace Mixxx diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h deleted file mode 100644 index 4546a3add28..00000000000 --- a/src/sources/audiosourcemodplug.h +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef AUDIOSOURCEMODPLUG_H -#define AUDIOSOURCEMODPLUG_H - -#include "sources/audiosource.h" - -namespace ModPlug { -#include -} - -#include - -namespace Mixxx { - -// Class for reading tracker files using libmodplug. -// The whole file is decoded at once and saved -// in RAM to allow seeking and smooth operation in Mixxx. -class AudioSourceModPlug: public AudioSource { -public: - static const SINT kChannelCount = 2; // always stereo - static const SINT kFrameRate = 44100; // always 44.1kHz - - // apply settings for decoding - static void configure(unsigned int bufferSizeLimit, - const ModPlug::ModPlug_Settings &settings); - - static AudioSourcePointer create(QUrl url); - - ~AudioSourceModPlug(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) /*override*/; - -private: - static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) - - explicit AudioSourceModPlug(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor - SINT m_fileLength; // length of file in samples - SINT m_seekPos; // current read position - QByteArray m_fileBuf; // original module file data - std::vector m_sampleBuf; // 16bit stereo samples, 44.1kHz -}; - -} - -#endif diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp deleted file mode 100644 index 2d879974bfb..00000000000 --- a/src/sources/audiosourcemp3.cpp +++ /dev/null @@ -1,539 +0,0 @@ -#include "sources/audiosourcemp3.h" - -#include "util/math.h" - -#include - -namespace Mixxx { - -namespace { - -// In the worst case up to 29 MP3 frames need to be prefetched -// for accurate seeking: -// http://www.mars.org/mailman/public/mad-dev/2002-May/000634.html -const SINT kSeekFramePrefetchCount = 29; - -const CSAMPLE kMadScale = AudioSource::kSampleValuePeak - / CSAMPLE(MAD_F_ONE); - -inline CSAMPLE madScale(mad_fixed_t sample) { - return sample * kMadScale; - -} // anonymous namespace - -// Optimization: Reserve initial capacity for seek frame list -const SINT kMinutesPerFile = 10; // enough for the majority of files (tunable) -const SINT kSecondsPerMinute = 60; // fixed -const SINT kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 -const SINT kSeekFrameListCapacity = kMinutesPerFile - * kSecondsPerMinute * kMaxMp3FramesPerSecond; - -int decodeFrameHeader( - mad_header* pMadHeader, - mad_stream* pMadStream, - bool skipId3Tag) { - const int result = mad_header_decode(pMadHeader, pMadStream); - if (0 != result) { - if (MAD_RECOVERABLE(pMadStream->error)) { - if ((MAD_ERROR_LOSTSYNC == pMadStream->error) && skipId3Tag) { - long tagsize = id3_tag_query(pMadStream->this_frame, - pMadStream->bufend - pMadStream->this_frame); - if (0 < tagsize) { - // Skip ID3 tag data - mad_stream_skip(pMadStream, tagsize); - // Suppress lost synchronization warnings - return result; - } - } - qWarning() << "Recoverable MP3 header decoding error:" - << mad_stream_errorstr(pMadStream); - } else { - if (MAD_ERROR_BUFLEN != pMadStream->error) { // EOF - qWarning() << "Unrecoverable MP3 header decoding error:" - << mad_stream_errorstr(pMadStream); - } - } - } - return result; -} - -} // anonymous namespace - -AudioSourceMp3::AudioSourceMp3(QUrl url) - : AudioSource(url), - m_file(getLocalFileName()), - m_fileSize(0), - m_pFileData(NULL), - m_avgSeekFrameCount(0), - m_curFrameIndex(kFrameIndexMin), - m_madSynthCount(0) { - mad_stream_init(&m_madStream); - mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - mad_frame_init(&m_madFrame); - mad_synth_init(&m_madSynth); - m_seekFrameList.reserve(kSeekFrameListCapacity); -} - -AudioSourceMp3::~AudioSourceMp3() { - preDestroy(); -} - -AudioSourcePointer AudioSourceMp3::create(QUrl url) { - return onCreate(new AudioSourceMp3(url)); -} - -Result AudioSourceMp3::postConstruct() { - if (!m_file.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file:" << m_file.fileName(); - return ERR; - } - - // Get a pointer to the file using memory mapped IO - m_fileSize = m_file.size(); - m_pFileData = m_file.map(0, m_fileSize); - // Transfer it to the mad stream-buffer: - mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); - DEBUG_ASSERT(m_pFileData == m_madStream.this_frame); - - // Decode all the headers and calculate audio properties - - mad_units madUnits = MAD_UNITS_44100_HZ; // default value - mad_timer_t madDuration = mad_timer_zero; - unsigned long sumBitrate = 0; - - mad_header madHeader; - mad_header_init(&madHeader); - - do { - if (0 != decodeFrameHeader(&madHeader, &m_madStream, true)) { - if (MAD_RECOVERABLE(m_madStream.error)) { - continue; - } else { - if (MAD_ERROR_BUFLEN == m_madStream.error) { - // EOF - break; - } else { - // Abort - mad_header_finish(&madHeader); - return ERR; - } - } - } - - // Grab data from madHeader - const SINT madChannelCount = MAD_NCHANNELS(&madHeader); - if (kChannelCountDefault == getChannelCount()) { - // initially set the number of channels - setChannelCount(madChannelCount); - } else { - // check for consistent number of channels - if ((0 < madChannelCount) - && (getChannelCount() != madChannelCount)) { - qWarning() << "Differing number of channels in some headers:" - << m_file.fileName() << getChannelCount() << "<>" - << madChannelCount; - // Abort - mad_header_finish(&madHeader); - return ERR; - } - } - const SINT madSampleRate = madHeader.samplerate; - if (kFrameRateDefault == getFrameRate()) { - // initially set the frame/sample rate - setFrameRate(madSampleRate); - switch (madSampleRate) { - case 8000: - madUnits = MAD_UNITS_8000_HZ; - break; - case 11025: - madUnits = MAD_UNITS_11025_HZ; - break; - case 12000: - madUnits = MAD_UNITS_12000_HZ; - break; - case 16000: - madUnits = MAD_UNITS_16000_HZ; - break; - case 22050: - madUnits = MAD_UNITS_22050_HZ; - break; - case 24000: - madUnits = MAD_UNITS_24000_HZ; - break; - case 32000: - madUnits = MAD_UNITS_32000_HZ; - break; - case 44100: - madUnits = MAD_UNITS_44100_HZ; - break; - case 48000: - madUnits = MAD_UNITS_48000_HZ; - break; - default: - qWarning() << "Invalid sample rate:" << m_file.fileName() - << madSampleRate; - // Abort - mad_header_finish(&madHeader); - return ERR; - } - } else { - // check for consistent frame/sample rate - if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { - qWarning() << "Differing sample rate in some headers:" - << m_file.fileName() - << getFrameRate() << "<>" << madSampleRate; - // Abort - mad_header_finish(&madHeader); - return ERR; - } - } - - addSeekFrame(m_curFrameIndex, m_madStream.this_frame); - - // Accumulate data from the header - sumBitrate += madHeader.bitrate; - mad_timer_add(&madDuration, madHeader.duration); - - // Update current stream position - m_curFrameIndex = kFrameIndexMin + - mad_timer_count(madDuration, madUnits); - - DEBUG_ASSERT(NULL != m_madStream.this_frame); - DEBUG_ASSERT(0 < (m_madStream.this_frame - m_pFileData)); - } while (quint64(m_madStream.this_frame - m_pFileData) < m_fileSize); - - mad_header_finish(&madHeader); - - if (MAD_ERROR_NONE != m_madStream.error) { - // Unreachable code for recoverable errors - DEBUG_ASSERT(!MAD_RECOVERABLE(m_madStream.error)); - if (MAD_ERROR_BUFLEN != m_madStream.error) { - qWarning() << "Unrecoverable MP3 header error:" - << mad_stream_errorstr(&m_madStream); - // Abort - return ERR; - } - } - - if (m_seekFrameList.empty()) { - // This is not a working MP3 file. - qWarning() << "SSMP3: This is not a working MP3 file:" - << m_file.fileName(); - // Abort - return ERR; - } - - // Initialize the audio stream length - setFrameCount(m_curFrameIndex); - - // Calculate average values - m_avgSeekFrameCount = getFrameCount() / m_seekFrameList.size(); - const unsigned long avgBitrate = sumBitrate / m_seekFrameList.size(); - setBitrate(avgBitrate / 1000); - - // Terminate m_seekFrameList - addSeekFrame(m_curFrameIndex, 0); - - // Restart decoding at the beginning of the audio stream - m_curFrameIndex = restartDecoding(m_seekFrameList.front()); - if (m_curFrameIndex != m_seekFrameList.front().frameIndex) { - qWarning() << "Failed to start decoding:" << m_file.fileName(); - // Abort - return ERR; - } - - return OK; -} - -void AudioSourceMp3::preDestroy() { - mad_synth_finish(&m_madSynth); - mad_frame_finish(&m_madFrame); - mad_stream_finish(&m_madStream); - - if (NULL != m_pFileData) { - m_file.unmap(m_pFileData); - m_pFileData = NULL; - } - - m_file.close(); -} - -SINT AudioSourceMp3::restartDecoding( - const SeekFrameType& seekFrame) { - qDebug() << "restartDecoding @" << seekFrame.frameIndex; - - if (kFrameIndexMin == seekFrame.frameIndex) { - mad_frame_finish(&m_madFrame); - mad_synth_finish(&m_madSynth); - } - mad_stream_finish(&m_madStream); - - mad_stream_init(&m_madStream); - mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - if (kFrameIndexMin == seekFrame.frameIndex) { - mad_synth_init(&m_madSynth); - mad_frame_init(&m_madFrame); - } - - // Fill input buffer - mad_stream_buffer(&m_madStream, seekFrame.pInputData, - m_fileSize - (seekFrame.pInputData - m_pFileData)); - - if (kFrameIndexMin < seekFrame.frameIndex) { - // Muting is done here to eliminate potential pops/clicks - // from skipping Rob Leslie explains why here: - // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html - mad_frame_mute(&m_madFrame); - mad_synth_mute(&m_madSynth); - } - - if (0 != decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) { - if (!MAD_RECOVERABLE(m_madStream.error)) { - // Failure -> Seek to EOF - return getFrameCount(); - } - } - - return seekFrame.frameIndex; -} - -void AudioSourceMp3::addSeekFrame( - SINT frameIndex, - const unsigned char* pInputData) { - DEBUG_ASSERT(m_seekFrameList.empty() || - (m_seekFrameList.back().frameIndex < frameIndex)); - DEBUG_ASSERT(m_seekFrameList.empty() || - (NULL == pInputData) || - (0 < (pInputData - m_seekFrameList.back().pInputData))); - SeekFrameType seekFrame; - seekFrame.pInputData = pInputData; - seekFrame.frameIndex = frameIndex; - m_seekFrameList.push_back(seekFrame); -} - -SINT AudioSourceMp3::findSeekFrameIndex( - SINT frameIndex) const { - // Check preconditions - DEBUG_ASSERT(0 < m_avgSeekFrameCount); - DEBUG_ASSERT(!m_seekFrameList.empty()); - DEBUG_ASSERT(kFrameIndexMin == m_seekFrameList.front().frameIndex); - DEBUG_ASSERT(SINT(kFrameIndexMin + getFrameIndexMax()) == m_seekFrameList.back().frameIndex); - - SINT lowerBound = - 0; - SINT upperBound = - m_seekFrameList.size(); - DEBUG_ASSERT(lowerBound < upperBound); - - // Initial guess based on average frame size - SINT seekFrameIndex = - frameIndex / m_avgSeekFrameCount; - if (seekFrameIndex >= upperBound) { - seekFrameIndex = upperBound - 1; - } - - while ((upperBound - lowerBound) > 1) { - DEBUG_ASSERT(seekFrameIndex >= lowerBound); - DEBUG_ASSERT(seekFrameIndex < upperBound); - DEBUG_ASSERT(m_seekFrameList[lowerBound].frameIndex <= frameIndex); - if (m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex) { - lowerBound = seekFrameIndex; - } else { - upperBound = seekFrameIndex; - } - // Next guess halfway between lower and upper bound - seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; - } - - // Check postconditions - DEBUG_ASSERT(seekFrameIndex == lowerBound); - DEBUG_ASSERT(SINT(m_seekFrameList.size()) > seekFrameIndex); - DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); - DEBUG_ASSERT(((seekFrameIndex + 1) >= SINT(m_seekFrameList.size())) || - (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); - - return seekFrameIndex; -} - -SINT AudioSourceMp3::seekSampleFrame(SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - - SINT seekFrameIndex = findSeekFrameIndex( - frameIndex); - DEBUG_ASSERT(SINT(m_seekFrameList.size()) > seekFrameIndex); - const SINT curSeekFrameIndex = findSeekFrameIndex( - m_curFrameIndex); - DEBUG_ASSERT(SINT(m_seekFrameList.size()) > curSeekFrameIndex); - // some consistency checks - DEBUG_ASSERT((curSeekFrameIndex >= seekFrameIndex) || (m_curFrameIndex < frameIndex)); - DEBUG_ASSERT((curSeekFrameIndex <= seekFrameIndex) || (m_curFrameIndex > frameIndex)); - if ((getFrameIndexMax() <= m_curFrameIndex) || // out of range - (frameIndex < m_curFrameIndex) || // seek backward - (seekFrameIndex > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jump forward - - // Discard decoded output - m_madSynthCount = 0; - - // Adjust the seek frame index for prefetching - // Implementation note: The type SINT is unsigned so - // need to be careful when subtracting! - if (kSeekFramePrefetchCount < seekFrameIndex) { - // Restart decoding kSeekFramePrefetchCount seek frames - // before the expected sync position - seekFrameIndex -= kSeekFramePrefetchCount; - } else { - // Restart decoding at the beginning of the audio stream - seekFrameIndex = 0; - } - - m_curFrameIndex = restartDecoding(m_seekFrameList[seekFrameIndex]); - if (getFrameIndexMax() <= m_curFrameIndex) { - // out of range -> abort - return m_curFrameIndex; - } - DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == seekFrameIndex); - } - - // Decoding starts before the actual target position - DEBUG_ASSERT(m_curFrameIndex <= frameIndex); - - // Skip (= decode and discard) prefetch data - const SINT skipFrameCount = frameIndex - m_curFrameIndex; - skipSampleFrames(skipFrameCount); - DEBUG_ASSERT(m_curFrameIndex == frameIndex); - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - return m_curFrameIndex; -} - -SINT AudioSourceMp3::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - return readSampleFrames(numberOfFrames, - sampleBuffer, frames2samples(numberOfFrames), - false); -} - -SINT AudioSourceMp3::readSampleFramesStereo( - SINT numberOfFrames, CSAMPLE* sampleBuffer, - SINT sampleBufferSize) { - return readSampleFrames(numberOfFrames, - sampleBuffer, sampleBufferSize, - true); -} - -SINT AudioSourceMp3::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer, - SINT sampleBufferSize, bool readStereoSamples) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); - - const SINT numberOfFramesTotal = math_min(numberOfFrames, - SINT(getFrameIndexMax() - m_curFrameIndex)); - - CSAMPLE* pSampleBuffer = sampleBuffer; - SINT numberOfFramesRemaining = numberOfFramesTotal; - while (0 < numberOfFramesRemaining) { - if (0 >= m_madSynthCount) { - // When all decoded output data has been consumed... - DEBUG_ASSERT(0 == m_madSynthCount); - // ...decode the next MP3 frame - DEBUG_ASSERT(NULL != m_madStream.buffer); - DEBUG_ASSERT(NULL != m_madStream.this_frame); - - // WARNING: Correctly evaluating and handling the result - // of mad_frame_decode() has proven to be extremely tricky. - // Don't change anything at the following lines of code - // unless you know what you are doing!!! - unsigned char const* pMadThisFrame = m_madStream.this_frame; - if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { - if (MAD_RECOVERABLE(m_madStream.error)) { - if (pMadThisFrame != m_madStream.this_frame) { - // Ignore all recoverable errors (and especially - // "lost synchronization" warnings) while skipping - // over prefetched frames after seeking. - if (NULL == pSampleBuffer) { - // Decoded samples will simply be discarded - qDebug() << "Recoverable MP3 frame decoding error while skipping:" - << mad_stream_errorstr(&m_madStream); - } else { - qWarning() << "Recoverable MP3 frame decoding error:" - << mad_stream_errorstr(&m_madStream); - } - } - // Acknowledge error... - m_madStream.error = MAD_ERROR_NONE; - // ...and continue - } else { - if (MAD_ERROR_BUFLEN != m_madStream.error) { - qWarning() << "Unrecoverable MP3 frame decoding error:" - << mad_stream_errorstr(&m_madStream); - } - // Abort - break; - } - } - if (pMadThisFrame == m_madStream.this_frame) { - qDebug() << "Retry decoding MP3 frame @" << m_curFrameIndex; - // Retry - continue; - } - - DEBUG_ASSERT(getChannelCount() == MAD_NCHANNELS(&m_madFrame.header)); - - // Once decoded the frame is synthesized to PCM samples - mad_synth_frame(&m_madSynth, &m_madFrame); - DEBUG_ASSERT(getFrameRate() == m_madSynth.pcm.samplerate); - m_madSynthCount = m_madSynth.pcm.length; - DEBUG_ASSERT(0 < m_madSynthCount); - } - - const SINT synthReadCount = math_min( - m_madSynthCount, numberOfFramesRemaining); - if (NULL != pSampleBuffer) { - DEBUG_ASSERT(m_madSynthCount <= m_madSynth.pcm.length); - const SINT madSynthOffset = - m_madSynth.pcm.length - m_madSynthCount; - DEBUG_ASSERT(madSynthOffset < m_madSynth.pcm.length); - if (isChannelCountMono()) { - for (SINT i = 0; i < synthReadCount; ++i) { - const CSAMPLE sampleValue = madScale( - m_madSynth.pcm.samples[0][madSynthOffset + i]); - *pSampleBuffer = sampleValue; - ++pSampleBuffer; - if (readStereoSamples) { - *pSampleBuffer = sampleValue; - ++pSampleBuffer; - } - } - } else if (isChannelCountStereo() || readStereoSamples) { - for (SINT i = 0; i < synthReadCount; ++i) { - *pSampleBuffer = madScale( - m_madSynth.pcm.samples[0][madSynthOffset + i]); - ++pSampleBuffer; - *pSampleBuffer = madScale( - m_madSynth.pcm.samples[1][madSynthOffset + i]); - ++pSampleBuffer; - } - } else { - for (SINT i = 0; i < synthReadCount; ++i) { - for (SINT j = 0; j < getChannelCount(); ++j) { - *pSampleBuffer = madScale( - m_madSynth.pcm.samples[j][madSynthOffset + i]); - ++pSampleBuffer; - } - } - } - } - // consume decoded output data - m_madSynthCount -= synthReadCount; - m_curFrameIndex += synthReadCount; - numberOfFramesRemaining -= synthReadCount; - } - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); - return numberOfFramesTotal - numberOfFramesRemaining; -} - -} // namespace Mixxx diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h deleted file mode 100644 index 590295673f2..00000000000 --- a/src/sources/audiosourcemp3.h +++ /dev/null @@ -1,82 +0,0 @@ -#ifndef AUDIOSOURCEMP3_H -#define AUDIOSOURCEMP3_H - -#include "sources/audiosource.h" - -#ifdef _MSC_VER -// So mad.h doesn't try to use inline assembly which MSVC doesn't support. -// Notably, FPM_64BIT does not require a 64-bit machine. It merely requires a -// compiler that supports 64-bit types. -#define FPM_64BIT -#endif -#include - -#include - -#include - -namespace Mixxx { - -class AudioSourceMp3: public AudioSource { -public: - static AudioSourcePointer create(QUrl url); - - ~AudioSourceMp3(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) /*override*/; - SINT readSampleFramesStereo(SINT numberOfFrames, - CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; - -private: - explicit AudioSourceMp3(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer, SINT sampleBufferSize, - bool readStereoSamples); - - QFile m_file; - quint64 m_fileSize; - unsigned char* m_pFileData; - - mad_stream m_madStream; - - /** Struct used to store mad frames for seeking */ - struct SeekFrameType { - SINT frameIndex; - const unsigned char* pInputData; - }; - - /** It is not possible to make a precise seek in an mp3 file without decoding the whole stream. - * To have precise seek within a limited range from the current decode position, we keep track - * of past decoded frame, and their exact position. If a seek occurs and it is within the - * range of frames we keep track of a precise seek occurs, otherwise an unprecise seek is performed - */ - typedef std::vector SeekFrameList; - SeekFrameList m_seekFrameList; // ordered-by frameIndex - SINT m_avgSeekFrameCount; // avg. sample frames per MP3 frame - - void addSeekFrame(SINT frameIndex, const unsigned char* pInputData); - - /** Returns the position in m_seekFrameList of the requested frame index. */ - SINT findSeekFrameIndex(SINT frameIndex) const; - - SINT m_curFrameIndex; - - SINT restartDecoding(const SeekFrameType& seekFrame); - - // current play position - mad_frame m_madFrame; - mad_synth m_madSynth; - SINT m_madSynthCount; // left overs from the previous read -}; - -} - -#endif diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp deleted file mode 100644 index e240965ea25..00000000000 --- a/src/sources/audiosourceoggvorbis.cpp +++ /dev/null @@ -1,170 +0,0 @@ -#include "sources/audiosourceoggvorbis.h" - -namespace Mixxx { - -namespace { - -// Parameter for ov_info() -// See also: https://xiph.org/vorbis/doc/vorbisfile/ov_info.html -const int kCurrentBitstreamLink = -1; // retrieve ... for the current bitstream - -// Parameter for ov_pcm_total() -// See also: https://xiph.org/vorbis/doc/vorbisfile/ov_pcm_total.html -const int kEntireBitstreamLink = -1; // retrieve ... for the entire physical bitstream - -} // anonymous namespace - -AudioSourceOggVorbis::AudioSourceOggVorbis(QUrl url) - : AudioSource(url), - m_curFrameIndex(0) { - memset(&m_vf, 0, sizeof(m_vf)); -} - -AudioSourceOggVorbis::~AudioSourceOggVorbis() { - preDestroy(); -} - -AudioSourcePointer AudioSourceOggVorbis::create(QUrl url) { - return onCreate(new AudioSourceOggVorbis(url)); -} - -Result AudioSourceOggVorbis::postConstruct() { - const QByteArray qbaFilename(getLocalFileNameBytes()); - if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { - qWarning() << "Failed to open OggVorbis file:" << getUrl(); - return ERR; - } - - if (!ov_seekable(&m_vf)) { - qWarning() << "OggVorbis file is not seekable:" << getUrl(); - return ERR; - } - - // lookup the ogg's channels and sample rate - const vorbis_info* vi = ov_info(&m_vf, kCurrentBitstreamLink); - if (!vi) { - qWarning() << "Failed to read OggVorbis file:" << getUrl(); - return ERR; - } - setChannelCount(vi->channels); - setFrameRate(vi->rate); - if (0 < vi->bitrate_nominal) { - setBitrate(vi->bitrate_nominal / 1000); - } else { - if ((0 < vi->bitrate_lower) && (vi->bitrate_lower == vi->bitrate_upper)) { - setBitrate(vi->bitrate_lower / 1000); - } - } - - ogg_int64_t pcmTotal = ov_pcm_total(&m_vf, kEntireBitstreamLink); - if (0 <= pcmTotal) { - setFrameCount(pcmTotal); - } else { - qWarning() << "Failed to read total length of OggVorbis file:" << getUrl(); - return ERR; - } - - return OK; -} - -void AudioSourceOggVorbis::preDestroy() { - const int clearResult = ov_clear(&m_vf); - if (0 != clearResult) { - qWarning() << "Failed to close OggVorbis file" << clearResult; - } -} - -SINT AudioSourceOggVorbis::seekSampleFrame( - SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - - const int seekResult = ov_pcm_seek(&m_vf, frameIndex); - if (0 == seekResult) { - m_curFrameIndex = frameIndex; - } else { - qWarning() << "Failed to seek OggVorbis file:" << seekResult; - const ogg_int64_t pcmOffset = ov_pcm_tell(&m_vf); - if (0 <= pcmOffset) { - m_curFrameIndex = pcmOffset; - } else { - // Reset to EOF - m_curFrameIndex = getFrameIndexMax(); - } - } - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - return m_curFrameIndex; -} - -SINT AudioSourceOggVorbis::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, - frames2samples(numberOfFrames), false); -} - -SINT AudioSourceOggVorbis::readSampleFramesStereo( - SINT numberOfFrames, CSAMPLE* sampleBuffer, - SINT sampleBufferSize) { - return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, - true); -} - -SINT AudioSourceOggVorbis::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer, - SINT sampleBufferSize, bool readStereoSamples) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); - - const SINT numberOfFramesTotal = math_min(numberOfFrames, - SINT(getFrameIndexMax() - m_curFrameIndex)); - - CSAMPLE* pSampleBuffer = sampleBuffer; - SINT numberOfFramesRemaining = numberOfFramesTotal; - while (0 < numberOfFramesRemaining) { - float** pcmChannels; - int currentSection; - // Use 'long' here, because ov_read_float() returns this type. - // This is an exception from the rule not to any types with - // differing sizes on different platforms. - // https://bugs.launchpad.net/mixxx/+bug/1094143 - const long readResult = ov_read_float(&m_vf, &pcmChannels, - numberOfFramesRemaining, ¤tSection); - if (0 < readResult) { - m_curFrameIndex += readResult; - if (isChannelCountMono()) { - if (readStereoSamples) { - for (long i = 0; i < readResult; ++i) { - *pSampleBuffer++ = pcmChannels[0][i]; - *pSampleBuffer++ = pcmChannels[0][i]; - } - } else { - for (long i = 0; i < readResult; ++i) { - *pSampleBuffer++ = pcmChannels[0][i]; - } - } - } else if (isChannelCountStereo() || readStereoSamples) { - for (long i = 0; i < readResult; ++i) { - *pSampleBuffer++ = pcmChannels[0][i]; - *pSampleBuffer++ = pcmChannels[1][i]; - } - } else { - for (long i = 0; i < readResult; ++i) { - for (SINT j = 0; j < getChannelCount(); ++j) { - *pSampleBuffer++ = pcmChannels[j][i]; - } - } - } - numberOfFramesRemaining -= readResult; - } else { - qWarning() << "Failed to read from OggVorbis file:" << readResult; - break; // abort - } - } - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); - return numberOfFramesTotal - numberOfFramesRemaining; -} - -} // namespace Mixxx diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h deleted file mode 100644 index c1db19bece1..00000000000 --- a/src/sources/audiosourceoggvorbis.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef AUDIOSOURCEOGGVORBIS_H -#define AUDIOSOURCEOGGVORBIS_H - -#include "sources/audiosource.h" - -#define OV_EXCLUDE_STATIC_CALLBACKS -#include - -namespace Mixxx { - -class AudioSourceOggVorbis: public AudioSource { -public: - static AudioSourcePointer create(QUrl url); - - ~AudioSourceOggVorbis(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) /*override*/; - SINT readSampleFramesStereo(SINT numberOfFrames, - CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; - -private: - explicit AudioSourceOggVorbis(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer, SINT sampleBufferSize, - bool readStereoSamples); - - OggVorbis_File m_vf; - - SINT m_curFrameIndex; -}; - -} - -#endif diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp deleted file mode 100644 index 448dc23ab7a..00000000000 --- a/src/sources/audiosourceopus.cpp +++ /dev/null @@ -1,167 +0,0 @@ -#include "sources/audiosourceopus.h" - -namespace Mixxx { - -namespace { - -// Parameter for op_channel_count() -// See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html -const int kCurrentStreamLink = -1; // get ... of the current (stream) link - -// Parameter for op_pcm_total() and op_bitrate() -// See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html -const int kEntireStreamLink = -1; // get ... of the whole/entire stream - -} // anonymous namespace - -// Decoded output of opusfile has a fixed sample rate of 48 kHz -const SINT AudioSourceOpus::kFrameRate = 48000; - -AudioSourceOpus::AudioSourceOpus(QUrl url) - : AudioSource(url), - m_pOggOpusFile(NULL), - m_curFrameIndex(0) { -} - -AudioSourceOpus::~AudioSourceOpus() { - preDestroy(); -} - -AudioSourcePointer AudioSourceOpus::create(QUrl url) { - return onCreate(new AudioSourceOpus(url)); -} - -Result AudioSourceOpus::postConstruct() { - const QByteArray qbaFilename(getLocalFileNameBytes()); - int errorCode = 0; - m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); - if (!m_pOggOpusFile) { - qDebug() << "Failed to open OggOpus file:" << getUrl() << "errorCode" - << errorCode; - return ERR; - } - - if (!op_seekable(m_pOggOpusFile)) { - qWarning() << "OggOpus file is not seekable:" << getUrl(); - return ERR; - } - - const int channelCount = op_channel_count(m_pOggOpusFile, kCurrentStreamLink); - if (0 < channelCount) { - setChannelCount(channelCount); - } else { - qWarning() << "Failed to read channel configuration of OggOpus file:" << getUrl(); - return ERR; - } - - const ogg_int64_t pcmTotal = op_pcm_total(m_pOggOpusFile, kEntireStreamLink); - if (0 <= pcmTotal) { - setFrameCount(pcmTotal); - } else { - qWarning() << "Failed to read total length of OggOpus file:" << getUrl(); - return ERR; - } - - const opus_int32 bitrate = op_bitrate(m_pOggOpusFile, kEntireStreamLink); - if (0 < bitrate) { - setBitrate(bitrate / 1000); - } else { - qWarning() << "Failed to determine bitrate of OggOpus file:" << getUrl(); - return ERR; - } - - setFrameRate(kFrameRate); - - return OK; -} - -void AudioSourceOpus::preDestroy() { - if (m_pOggOpusFile) { - op_free(m_pOggOpusFile); - m_pOggOpusFile = NULL; - } -} - -SINT AudioSourceOpus::seekSampleFrame(SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - - int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); - if (0 == seekResult) { - m_curFrameIndex = frameIndex; - } else { - qWarning() << "Failed to seek OggOpus file:" << seekResult; - const ogg_int64_t pcmOffset = op_pcm_tell(m_pOggOpusFile); - if (0 <= pcmOffset) { - m_curFrameIndex = pcmOffset; - } else { - // Reset to EOF - m_curFrameIndex = getFrameIndexMax(); - } - } - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - return m_curFrameIndex; -} - -SINT AudioSourceOpus::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - - const SINT numberOfFramesTotal = math_min(numberOfFrames, - SINT(getFrameIndexMax() - m_curFrameIndex)); - - CSAMPLE* pSampleBuffer = sampleBuffer; - SINT numberOfFramesRemaining = numberOfFramesTotal; - while (0 < numberOfFramesRemaining) { - int readResult = op_read_float(m_pOggOpusFile, - pSampleBuffer, - frames2samples(numberOfFramesRemaining), NULL); - if (0 < readResult) { - m_curFrameIndex += readResult; - pSampleBuffer += frames2samples(readResult); - numberOfFramesRemaining -= readResult; - } else { - qWarning() << "Failed to read sample data from OggOpus file:" - << readResult; - break; // abort - } - } - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); - return numberOfFramesTotal - numberOfFramesRemaining; -} - -SINT AudioSourceOpus::readSampleFramesStereo( - SINT numberOfFrames, CSAMPLE* sampleBuffer, - SINT sampleBufferSize) { - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) <= sampleBufferSize); - - const SINT numberOfFramesTotal = math_min(numberOfFrames, - SINT(getFrameIndexMax() - m_curFrameIndex)); - - CSAMPLE* pSampleBuffer = sampleBuffer; - SINT numberOfFramesRemaining = numberOfFramesTotal; - while (0 < numberOfFramesRemaining) { - int readResult = op_read_float_stereo(m_pOggOpusFile, - pSampleBuffer, - numberOfFramesRemaining * 2); // stereo - if (0 < readResult) { - m_curFrameIndex += readResult; - pSampleBuffer += readResult * 2; // stereo - numberOfFramesRemaining -= readResult; - } else { - qWarning() << "Failed to read sample data from OggOpus file:" - << readResult; - break; // abort - } - } - - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); - return numberOfFramesTotal - numberOfFramesRemaining; -} - -} // namespace Mixxx diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h deleted file mode 100644 index e35a2181010..00000000000 --- a/src/sources/audiosourceopus.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef AUDIOSOURCEOPUS_H -#define AUDIOSOURCEOPUS_H - -#include "sources/audiosource.h" - -#define OV_EXCLUDE_STATIC_CALLBACKS -#include - -namespace Mixxx { - -class AudioSourceOpus: public AudioSource { -public: - static const SINT kFrameRate; - - static AudioSourcePointer create(QUrl url); - - ~AudioSourceOpus(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) /*override*/; - SINT readSampleFramesStereo(SINT numberOfFrames, - CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; - -private: - explicit AudioSourceOpus(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - OggOpusFile *m_pOggOpusFile; - - SINT m_curFrameIndex; -}; - -} -; - -#endif diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp deleted file mode 100644 index b9cacd026d0..00000000000 --- a/src/sources/audiosourcesndfile.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include "sources/audiosourcesndfile.h" - -namespace Mixxx { - -AudioSourceSndFile::AudioSourceSndFile(QUrl url) - : AudioSource(url), - m_pSndFile(NULL) { - memset(&m_sfInfo, 0, sizeof(m_sfInfo)); -} - -AudioSourceSndFile::~AudioSourceSndFile() { - preDestroy(); -} - -AudioSourcePointer AudioSourceSndFile::create(QUrl url) { - return onCreate(new AudioSourceSndFile(url)); -} - -Result AudioSourceSndFile::postConstruct() { -#ifdef __WINDOWS__ - // Pointer valid until string changed - const QString fileName(getLocalFileName()); - LPCWSTR lpcwFilename = (LPCWSTR) fileName.utf16(); - m_pSndFile = sf_wchar_open(lpcwFilename, SFM_READ, &m_sfInfo); -#else - m_pSndFile = sf_open(getLocalFileNameBytes().constData(), SFM_READ, - &m_sfInfo); -#endif - - if (m_pSndFile == NULL) { // sf_format_check is only for writes - qWarning() << "Error opening libsndfile file:" << getUrl() - << sf_strerror(m_pSndFile); - return ERR; - } - - if (sf_error(m_pSndFile) > 0) { - qWarning() << "Error opening libsndfile file:" << getUrl() - << sf_strerror(m_pSndFile); - return ERR; - } - - setChannelCount(m_sfInfo.channels); - setFrameRate(m_sfInfo.samplerate); - setFrameCount(m_sfInfo.frames); - - return OK; -} - -void AudioSourceSndFile::preDestroy() { - if (m_pSndFile) { - const int closeResult = sf_close(m_pSndFile); - if (0 == closeResult) { - m_pSndFile = NULL; - } else { - qWarning() << "Failed to close libsnd file:" << closeResult - << sf_strerror(m_pSndFile); - } - } -} - -SINT AudioSourceSndFile::seekSampleFrame( - SINT frameIndex) { - DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - - const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); - if (0 <= seekResult) { - return seekResult; - } else { - qWarning() << "Failed to seek libsnd file:" << seekResult - << sf_strerror(m_pSndFile); - return sf_seek(m_pSndFile, 0, SEEK_CUR); - } -} - -SINT AudioSourceSndFile::readSampleFrames( - SINT numberOfFrames, CSAMPLE* sampleBuffer) { - const sf_count_t readCount = - sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); - if (0 <= readCount) { - return readCount; - } else { - qWarning() << "Failed to read from libsnd file:" << readCount - << sf_strerror(m_pSndFile); - return 0; - } -} - -} // namespace Mixxx diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h deleted file mode 100644 index 2a622f7556b..00000000000 --- a/src/sources/audiosourcesndfile.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef AUDIOSOURCESNDFILE_H -#define AUDIOSOURCESNDFILE_H - -#include "sources/audiosource.h" - -#ifdef Q_OS_WIN -//Enable unicode in libsndfile on Windows -//(sf_open uses UTF-8 otherwise) -#include -#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 -#endif -#include - -namespace Mixxx { - -class AudioSourceSndFile: public AudioSource { -public: - static AudioSourcePointer create(QUrl url); - - ~AudioSourceSndFile(); - - SINT seekSampleFrame(SINT frameIndex) /*override*/; - - SINT readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) /*override*/; - -private: - explicit AudioSourceSndFile(QUrl url); - - Result postConstruct() /*override*/; - - void preDestroy(); - - SNDFILE* m_pSndFile; - SF_INFO m_sfInfo; -}; - -} - -#endif diff --git a/src/sources/metadatasource.h b/src/sources/metadatasource.h new file mode 100644 index 00000000000..67f2488442f --- /dev/null +++ b/src/sources/metadatasource.h @@ -0,0 +1,28 @@ +#ifndef METADATASOURCE_H +#define METADATASOURCE_H + +#include "metadata/trackmetadata.h" + +#include + +namespace Mixxx { + +// Interface for metadata +class MetadataSource { +public: + // Only metadata that is quickly readable should be read. + // The implementation is free to set inaccurate estimated + // values here. + virtual Result parseTrackMetadata(TrackMetadata* pMetadata) const = 0; + + // Returns the first cover art image embedded within the + // file (if any). + virtual QImage parseCoverArt() const = 0; + +protected: + virtual ~MetadataSource() {} +}; + +} //namespace Mixxx + +#endif // METADATASOURCE_H diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index 8976cd1756c..2d463695979 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -9,18 +9,18 @@ namespace Mixxx { } SoundSource::SoundSource(QUrl url) - : UrlResource(url), + : AudioSource(url), m_type(getTypeFromUrl(url)) { DEBUG_ASSERT(getUrl().isValid()); } SoundSource::SoundSource(QUrl url, QString type) - : UrlResource(url), + : AudioSource(url), m_type(type) { DEBUG_ASSERT(getUrl().isValid()); } -Result SoundSource::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { +Result SoundSource::parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) const { return readTrackMetadataFromFile(pMetadata, getLocalFileName()); } diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index 1062de9d500..7d2e2235d1e 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -1,5 +1,5 @@ -#ifndef SOUNDSOURCE_H -#define SOUNDSOURCE_H +#ifndef MIXXX_SOUNDSOURCE_H +#define MIXXX_SOUNDSOURCE_H #define MIXXX_SOUNDSOURCE_API_VERSION 7 /** @note SoundSource API Version history: @@ -13,47 +13,30 @@ */ #include "sources/audiosource.h" +#include "sources/metadatasource.h" #include -/** Getter function to be declared by all SoundSource plugins */ namespace Mixxx { -class SoundSource; -class TrackMetadata; -} -typedef Mixxx::SoundSource* (*getSoundSourceFunc)(QString fileName); -typedef char** (*getSupportedFileExtensionsFunc)(); -typedef int (*getSoundSourceAPIVersionFunc)(); -/* New in version 3 */ -typedef void (*freeFileExtensionsFunc)(char** fileExts); - -namespace Mixxx { - -/* - Base class for sound sources. - */ -class SoundSource: public UrlResource { +// Base class for sound sources. +class SoundSource: public MetadataSource, public AudioSource { public: static QString getTypeFromUrl(QUrl url); - inline const QString& getType() const { + const QString& getType() const { return m_type; } - // Parses metadata before opening the SoundSource for reading. - // - // Only metadata that is quickly readable should be read. - // The implementation is free to set inaccurate estimated - // values here that are overwritten when the AudioSource is - // actually opened for reading. - virtual Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const; + Result parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + + QImage parseCoverArt() const /*override*/; - // Returns the first cover art image embedded within the file (if any). - virtual QImage parseCoverArt() const; + // Opens the AudioSource for reading audio data. + virtual Result open() = 0; - // Opens the SoundSource for reading audio data. - virtual AudioSourcePointer open() const = 0; + // Closes the AudioSource. + virtual void close() = 0; protected: explicit SoundSource(QUrl url); @@ -67,4 +50,4 @@ typedef QSharedPointer SoundSourcePointer; } //namespace Mixxx -#endif +#endif // MIXXX_SOUNDSOURCE_H diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index 809a02d87c3..1540eaa3675 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -1,8 +1,8 @@ #include "sources/soundsourcecoreaudio.h" -#include "sources/audiosourcecoreaudio.h" +#include "util/math.h" -#include +const SINT SoundSourceCoreAudio::kChannelCount = 2; // always stereo QList SoundSourceCoreAudio::supportedFileExtensions() { QList list; @@ -18,9 +18,158 @@ QList SoundSourceCoreAudio::supportedFileExtensions() { } SoundSourceCoreAudio::SoundSourceCoreAudio(QUrl url) - : SoundSource(url) { + : SoundSource(url), + m_headerFrames(0) { } -Mixxx::AudioSourcePointer SoundSourceCoreAudio::open() const { - return Mixxx::AudioSourceCoreAudio::create(getUrl()); +SoundSourceCoreAudio::~SoundSourceCoreAudio() { + close(); +} + +// soundsource overrides +Result AudioSourceCoreAudio::open() { + close(); // safety first + + const QString fileName(getLocalFileName()); + + //Open the audio file. + OSStatus err; + + /** This code blocks works with OS X 10.5+ only. DO NOT DELETE IT for now. */ + CFStringRef urlStr = CFStringCreateWithCharacters(0, + reinterpret_cast(fileName.unicode()), + fileName.size()); + CFURLRef urlRef = CFURLCreateWithFileSystemPath(NULL, urlStr, + kCFURLPOSIXPathStyle, false); + err = ExtAudioFileOpenURL(urlRef, &m_audioFile); + CFRelease(urlStr); + CFRelease(urlRef); + + /** TODO: Use FSRef for compatibility with 10.4 Tiger. + Note that ExtAudioFileOpen() is deprecated above Tiger, so we must maintain + both code paths if someone finishes this part of the code. + FSRef fsRef; + CFURLGetFSRef(reinterpret_cast(url.get()), &fsRef); + err = ExtAudioFileOpen(&fsRef, &m_audioFile); + */ + + if (err != noErr) { + qDebug() << "SSCA: Error opening file " << fileName; + return ERR; + } + + // get the input file format + UInt32 inputFormatSize = sizeof(m_inputFormat); + err = ExtAudioFileGetProperty(m_audioFile, + kExtAudioFileProperty_FileDataFormat, &inputFormatSize, + &m_inputFormat); + if (err != noErr) { + qDebug() << "SSCA: Error getting file format (" << fileName << ")"; + return ERR; + } + + // create the output format + m_outputFormat = CAStreamBasicDescription(m_inputFormat.mSampleRate, + kChannelCount, CAStreamBasicDescription::kPCMFormatFloat32, true); + + // set the client format + err = ExtAudioFileSetProperty(m_audioFile, + kExtAudioFileProperty_ClientDataFormat, sizeof(m_outputFormat), + &m_outputFormat); + if (err != noErr) { + qDebug() << "SSCA: Error setting file property"; + return ERR; + } + + //get the total length in frames of the audio file - copypasta: http://discussions.apple.com/thread.jspa?threadID=2364583&tstart=47 + SInt64 totalFrameCount; + UInt32 totalFrameCountSize = sizeof(totalFrameCount); + err = ExtAudioFileGetProperty(m_audioFile, + kExtAudioFileProperty_FileLengthFrames, &totalFrameCountSize, + &totalFrameCount); + if (err != noErr) { + qDebug() << "SSCA: Error getting number of frames"; + return ERR; + } + + // + // WORKAROUND for bug in ExtFileAudio + // + + AudioConverterRef acRef; + UInt32 acrsize = sizeof(AudioConverterRef); + err = ExtAudioFileGetProperty(m_audioFile, + kExtAudioFileProperty_AudioConverter, &acrsize, &acRef); + //_ThrowExceptionIfErr(@"kExtAudioFileProperty_AudioConverter", err); + + AudioConverterPrimeInfo primeInfo; + UInt32 piSize = sizeof(AudioConverterPrimeInfo); + memset(&primeInfo, 0, piSize); + err = AudioConverterGetProperty(acRef, kAudioConverterPrimeInfo, &piSize, + &primeInfo); + if (err != kAudioConverterErr_PropertyNotSupported) { // Only if decompressing + //_ThrowExceptionIfErr(@"kAudioConverterPrimeInfo", err); + m_headerFrames = primeInfo.leadingFrames; + } else { + m_headerFrames = 0; + } + + setChannelCount(m_outputFormat.NumberChannels()); + setFrameRate(m_inputFormat.mSampleRate); + // NOTE(uklotzde): This is what I found when migrating + // the code from SoundSource (sample-oriented) to the new + // AudioSource (frame-oriented) API. It is not documented + // when m_headerFrames > 0 and what the consequences are. + setFrameCount(totalFrameCount/* - m_headerFrames*/); + + //Seek to position 0, which forces us to skip over all the header frames. + //This makes sure we're ready to just let the Analyser rip and it'll + //get the number of samples it expects (ie. no header frames). + seekSampleFrame(0); + + return OK; +} + +void AudioSourceCoreAudio::close() { + ExtAudioFileDispose(m_audioFile); +} + +SINT AudioSourceCoreAudio::seekSampleFrame( + SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); + //_ThrowExceptionIfErr(@"ExtAudioFileSeek", err); + //qDebug() << "SSCA: Seeking to" << frameIndex; + if (err != noErr) { + qDebug() << "SSCA: Error seeking to" << frameIndex; // << GetMacOSStatusErrorString(err) << GetMacOSStatusCommentString(err); + } + return frameIndex; +} + +SINT AudioSourceCoreAudio::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + //if (!m_decoder) return 0; + SINT numFramesRead = 0; + + while (numFramesRead < numberOfFrames) { + SINT numFramesToRead = numberOfFrames - numFramesRead; + + AudioBufferList fillBufList; + fillBufList.mNumberBuffers = 1; + fillBufList.mBuffers[0].mNumberChannels = getChannelCount(); + fillBufList.mBuffers[0].mDataByteSize = frames2samples(numFramesToRead) + * sizeof(sampleBuffer[0]); + fillBufList.mBuffers[0].mData = sampleBuffer + + frames2samples(numFramesRead); + + UInt32 numFramesToReadInOut = numFramesToRead; // input/output parameter + OSStatus err = ExtAudioFileRead(m_audioFile, &numFramesToReadInOut, + &fillBufList); + if (0 == numFramesToReadInOut) { + // EOF + break;// done + } + numFramesRead += numFramesToReadInOut; + } + return numFramesRead; } diff --git a/src/sources/soundsourcecoreaudio.h b/src/sources/soundsourcecoreaudio.h index 786da92dcf1..7d36096756a 100644 --- a/src/sources/soundsourcecoreaudio.h +++ b/src/sources/soundsourcecoreaudio.h @@ -3,13 +3,43 @@ #include "sources/soundsource.h" +#include +//In our tree at lib/apple/ +#include "CAStreamBasicDescription.h" + +#if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) +#include +#include +#include +#include +#else +#include "CoreAudioTypes.h" +#include "AudioFile.h" +#include "AudioFormat.h" +#endif + class SoundSourceCoreAudio : public Mixxx::SoundSource { public: + static const kChannelCount; + static QList supportedFileExtensions(); explicit SoundSourceCoreAudio(QUrl url); + ~SoundSourceCoreAudio(); + + Result open() /*override*/; + void close() /*override*/; + + SINT seekSampleFrame(SINT frameIndex) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; +private: + ExtAudioFileRef m_audioFile; + CAStreamBasicDescription m_inputFormat; + CAStreamBasicDescription m_outputFormat; + SInt64 m_headerFrames; }; -#endif // ifndef SOUNDSOURCECOREAUDIO_H +#endif // SOUNDSOURCECOREAUDIO_H diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index c1efae0c299..e07998293ca 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -1,6 +1,11 @@ #include "sources/soundsourceffmpeg.h" -#include "sources/audiosourceffmpeg.h" +#include + +#define AUDIOSOURCEFFMPEG_CACHESIZE 1000 +#define AUDIOSOURCEFFMPEG_POSDISTANCE ((1024 * 1000) / 8) + +namespace Mixxx { QList SoundSourceFFmpeg::supportedFileExtensions() { QList list; @@ -35,9 +40,493 @@ QList SoundSourceFFmpeg::supportedFileExtensions() { } SoundSourceFFmpeg::SoundSourceFFmpeg(QUrl url) - : SoundSource(url) { + : SoundSource(url), + m_pFormatCtx(NULL), + m_iAudioStream(-1), + m_pCodecCtx(NULL), + m_pCodec(NULL), + m_pResample(NULL), + m_iCurrentMixxTs(0), + m_bIsSeeked(false), + m_lCacheBytePos(0), + m_lCacheStartByte(0), + m_lCacheEndByte(0), + m_lCacheLastPos(0), + m_lLastStoredPos(0), + m_lStoredSeekPoint(-1) { +} + +SoundSourceFFmpeg::~SoundSourceFFmpeg() { + close(); +} + +Result SoundSourceFFmpeg::open() { + if (m_pFormatCtx) { + qWarning() << "Cannot reopen FFmpeg file:" << getUrl(); + return ERR; + } + + unsigned int i; + AVDictionary *l_iFormatOpts = NULL; + + const QByteArray qBAFilename(getLocalFileNameBytes()); + qDebug() << "New SoundSourceFFmpeg :" << qBAFilename; + + m_pFormatCtx = avformat_alloc_context(); + +#if LIBAVCODEC_VERSION_INT < 3622144 + m_pFormatCtx->max_analyze_duration = 999999999; +#endif + + // Open file and make m_pFormatCtx + if (avformat_open_input(&m_pFormatCtx, qBAFilename.constData(), NULL, + &l_iFormatOpts)!=0) { + qDebug() << "av_open_input_file: cannot open" << qBAFilename; + return ERR; + } + +#if LIBAVCODEC_VERSION_INT > 3544932 + av_dict_free(&l_iFormatOpts); +#endif + + // Retrieve stream information + if (avformat_find_stream_info(m_pFormatCtx, NULL)<0) { + qDebug() << "av_find_stream_info: cannot open" << qBAFilename; + return ERR; + } + + //debug only (Enable if needed) + //av_dump_format(m_pFormatCtx, 0, qBAFilename.constData(), false); + + // Find the first audio stream + m_iAudioStream=-1; + + for (i=0; inb_streams; i++) + if (m_pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) { + m_iAudioStream=i; + break; + } + if (m_iAudioStream==-1) { + qDebug() << "ffmpeg: cannot find an audio stream: cannot open" + << qBAFilename; + return ERR; + } + + // Get a pointer to the codec context for the audio stream + m_pCodecCtx=m_pFormatCtx->streams[m_iAudioStream]->codec; + + // Find the decoder for the audio stream + if (!(m_pCodec=avcodec_find_decoder(m_pCodecCtx->codec_id))) { + qDebug() << "ffmpeg: cannot find a decoder for" << qBAFilename; + return ERR; + } + + if (avcodec_open2(m_pCodecCtx, m_pCodec, NULL)<0) { + qDebug() << "ffmpeg: cannot open" << qBAFilename; + return ERR; + } + + m_pResample = new EncoderFfmpegResample(m_pCodecCtx); + // TODO(XXX): Use AV_SAMPLE_FMT_FLT instead of AV_SAMPLE_FMT_S16 + m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_S16); + + setChannelCount(m_pCodecCtx->channels); + setFrameRate(m_pCodecCtx->sample_rate); + setFrameCount((m_pFormatCtx->duration * m_pCodecCtx->sample_rate) / AV_TIME_BASE); + + qDebug() << "ffmpeg: Samplerate: " << getFrameRate() << ", Channels: " << + getChannelCount() << "\n"; + if (getChannelCount() > 2) { + qDebug() << "ffmpeg: No support for more than 2 channels!"; + return ERR; + } + + return OK; +} + +void SoundSourceFFmpeg::close() { + clearCache(); + + if (m_pCodecCtx != NULL) { + qDebug() << "~SoundSourceFFmpeg(): Clear FFMPEG stuff"; + avcodec_close(m_pCodecCtx); + avformat_close_input(&m_pFormatCtx); + av_free(m_pFormatCtx); + m_pFormatCtx = NULL; + } + + if (m_pResample != NULL) { + qDebug() << "~SoundSourceFFmpeg(): Delete FFMPEG Resampler"; + delete m_pResample; + m_pResample = NULL; + } + + while (m_SJumpPoints.size() > 0) { + ffmpegLocationObject* l_SRmJmp = m_SJumpPoints[0]; + m_SJumpPoints.remove(0); + free(l_SRmJmp); + } +} + +void SoundSourceFFmpeg::clearCache() { + while (m_SCache.size() > 0) { + struct ffmpegCacheObject* l_SRmObj = m_SCache[0]; + m_SCache.remove(0); + free(l_SRmObj->bytes); + free(l_SRmObj); + } +} + +bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { + unsigned int l_iCount = 0; + qint32 l_iRet = 0; + AVPacket l_SPacket; + AVFrame *l_pFrame = NULL; + bool l_iStop = false; + int l_iFrameFinished = 0; + struct ffmpegCacheObject *l_SObj = NULL; + struct ffmpegCacheObject *l_SRmObj = NULL; + bool m_bUnique = false; + qint64 l_lLastPacketPos = -1; + int l_iError = 0; + int l_iFrameCount = 0; + + l_iCount = count; + + l_SPacket.data = NULL; + l_SPacket.size = 0; + + + while (l_iCount > 0) { + if (l_pFrame != NULL) { + l_iFrameCount --; +// FFMPEG 2.2 3561060 anb beyond +#if LIBAVCODEC_VERSION_INT >= 3561060 + av_frame_free(&l_pFrame); +// FFMPEG 0.11 and below +#elif LIBAVCODEC_VERSION_INT <= 3544932 + av_free(l_pFrame); +// FFMPEG 1.0 - 2.1 +#else + avcodec_free_frame(&l_pFrame); +#endif + l_pFrame = NULL; + + } + + if (l_iStop == true) { + break; + } + l_iFrameCount ++; + av_init_packet(&l_SPacket); +#if LIBAVCODEC_VERSION_INT < 3617792 + l_pFrame = avcodec_alloc_frame(); +#else + l_pFrame = av_frame_alloc(); +#endif + + if (av_read_frame(m_pFormatCtx, &l_SPacket) >= 0) { + if (l_SPacket.stream_index == m_iAudioStream) { + if (m_lStoredSeekPoint > 0) { + // Seek for correct jump point + if (m_lStoredSeekPoint > l_SPacket.pos && + m_lStoredSeekPoint >= AUDIOSOURCEFFMPEG_POSDISTANCE) { + av_free_packet(&l_SPacket); + l_SPacket.data = NULL; + l_SPacket.size = 0; + continue; + } + m_lStoredSeekPoint = -1; + } + + l_iRet = avcodec_decode_audio4(m_pCodecCtx,l_pFrame,&l_iFrameFinished, + &l_SPacket); + + if (l_iRet <= 0) { + // An error or EOF occured,index break out and return what + // we have so far. + qDebug() << "EOF!"; + l_iStop = true; + continue; + } else { + l_iRet = 0; + l_SObj = (struct ffmpegCacheObject *)malloc(sizeof(struct ffmpegCacheObject)); + if (l_SObj == NULL) { + qDebug() << "SoundSourceFFmpeg::readFramesToCache: Not enough memory!"; + l_iStop = true; + continue; + } + memset(l_SObj, 0x00, sizeof(struct ffmpegCacheObject)); + l_iRet = m_pResample->reSample(l_pFrame, &l_SObj->bytes); + + if (l_iRet > 0) { + // Remove from cache + if (m_SCache.size() >= (AUDIOSOURCEFFMPEG_CACHESIZE - 10)) { + l_SRmObj = m_SCache[0]; + m_SCache.remove(0); + free(l_SRmObj->bytes); + free(l_SRmObj); + } + + // Add to cache and store byte place to memory + m_SCache.append(l_SObj); + l_SObj->startByte = m_lCacheBytePos / 2; + l_SObj->length = l_iRet / 2; + m_lCacheBytePos += l_iRet; + + // Ogg/Opus have packages pos that have many + // audio frames so seek next unique pos.. + if (l_SPacket.pos != l_lLastPacketPos) { + m_bUnique = true; + l_lLastPacketPos = l_SPacket.pos; + } + + // If we are over last storepos and we have read more than jump point need is and pos is unique we store it to memory + if (m_lCacheBytePos > m_lLastStoredPos && + m_lCacheBytePos > (AUDIOSOURCEFFMPEG_POSDISTANCE + m_lLastStoredPos) && + m_bUnique == true) { + struct ffmpegLocationObject *l_SJmp = (struct ffmpegLocationObject *)malloc( + sizeof(struct ffmpegLocationObject)); + m_lLastStoredPos = m_lCacheBytePos; + l_SJmp->startByte = m_lCacheBytePos / 2; + l_SJmp->pos = l_SPacket.pos; + l_SJmp->pts = l_SPacket.pts; + m_SJumpPoints.append(l_SJmp); + m_bUnique = false; + } + + if (offset < 0 || (quint64) offset <= (m_lCacheBytePos / 2)) { + l_iCount --; + } + } else { + free(l_SObj); + l_SObj = NULL; + qDebug() << + "SoundSourceFFmpeg::readFramesToCache: General error in audio decode:" << + l_iRet; + } + } + + av_free_packet(&l_SPacket); + l_SPacket.data = NULL; + l_SPacket.size = 0; + + } else { + l_iError ++; + if (l_iError == 5) { + // Stream end and we couldn't read enough frames + l_iStop = true; + } + } + + + } else { + qDebug() << "SoundSourceFFmpeg::readFramesToCache: Packet too big or File end"; + l_iStop = true; + } + + } + + if (l_pFrame != NULL) { + l_iFrameCount --; +// FFMPEG 2.2 3561060 anb beyond +#if LIBAVCODEC_VERSION_INT >= 3561060 + av_frame_unref(l_pFrame); + av_frame_free(&l_pFrame); +// FFMPEG 0.11 and below +#elif LIBAVCODEC_VERSION_INT <= 3544932 + av_free(l_pFrame); +// FFMPEG 1.0 - 2.1 +#else + avcodec_free_frame(&l_pFrame); +#endif + l_pFrame = NULL; + + } + + if (l_iFrameCount > 0) { + qDebug() << "SoundSourceFFmpeg::readFramesToCache(): Frame balance is not 0 it is: " << l_iFrameCount; + } + + l_SObj = m_SCache.first(); + m_lCacheStartByte = l_SObj->startByte; + l_SObj = m_SCache.last(); + m_lCacheEndByte = (l_SObj->startByte + l_SObj->length); + + if (!l_iCount) { + return true; + } else { + return false; + } +} + +bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, + quint64 size) { + struct ffmpegCacheObject *l_SObj = NULL; + quint32 l_lPos = 0; + quint32 l_lLeft = 0; + quint32 l_lOffset = 0; + quint32 l_lBytesToCopy = 0; + + if (offset >= m_lCacheStartByte) { + if (m_lCacheLastPos == 0) { + m_lCacheLastPos = m_SCache.size() - 1; + } + for (l_lPos = m_lCacheLastPos; l_lPos > 0; l_lPos --) { + l_SObj = m_SCache[l_lPos]; + if ((l_SObj->startByte + l_SObj->length) < offset) { + break; + } + } + + l_SObj = m_SCache[l_lPos]; + + l_lLeft = (size * 2); + memset(buffer, 0x00, l_lLeft); + while (l_lLeft > 0) { + + if (l_SObj == NULL || (l_lPos + 5) > (unsigned int)m_SCache.size()) { + offset = l_SObj->startByte; + if (readFramesToCache(50, -1) == false) { + return false; + } + for (l_lPos = (m_SCache.size() - 50); l_lPos > 0; l_lPos --) { + l_SObj = m_SCache[l_lPos]; + if ((l_SObj->startByte + l_SObj->length) < offset) { + break; + } + } + l_SObj = m_SCache[l_lPos]; + continue; + } + + if (l_SObj->startByte <= offset) { + l_lOffset = (offset - l_SObj->startByte) * 2; + } + + if (l_lOffset >= (l_SObj->length * 2)) { + l_SObj = m_SCache[++ l_lPos]; + continue; + } + + if (l_lLeft > (l_SObj->length * 2)) { + l_lBytesToCopy = ((l_SObj->length * 2) - l_lOffset); + memcpy(buffer, (l_SObj->bytes + l_lOffset), l_lBytesToCopy); + l_lOffset = 0; + buffer += l_lBytesToCopy; + l_lLeft -= l_lBytesToCopy; + } else { + memcpy(buffer, l_SObj->bytes, l_lLeft); + l_lLeft = 0; + } + + l_SObj = m_SCache[++ l_lPos]; + } + + m_lCacheLastPos = --l_lPos; + return true; + } + + return false; } -Mixxx::AudioSourcePointer SoundSourceFFmpeg::open() const { - return Mixxx::AudioSourceFFmpeg::create(getUrl()); +SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + + const SINT filepos = frames2samples(frameIndex); + + int ret = 0; + qint64 i = 0; + + if (filepos < 0 || (unsigned long) filepos < m_lCacheStartByte) { + ret = avformat_seek_file(m_pFormatCtx, + m_iAudioStream, + 0, + 32767 * 2, + 32767 * 2, + AVSEEK_FLAG_BACKWARD); + + + if (ret < 0) { + qDebug() << "SoundSourceFFmpeg::seek: Can't seek to 0 byte!"; + return -1; + } + + clearCache(); + m_lCacheStartByte = 0; + m_lCacheEndByte = 0; + m_lCacheLastPos = 0; + m_lCacheBytePos = 0; + m_lStoredSeekPoint = -1; + + + // Try to find some jump point near to + // where we are located so we don't needed + // to try guess it + if (filepos >= AUDIOSOURCEFFMPEG_POSDISTANCE) { + for (i = 0; i < m_SJumpPoints.size(); i ++) { + if (m_SJumpPoints[i]->startByte >= (unsigned long) filepos && i > 2) { + m_lCacheBytePos = m_SJumpPoints[i - 2]->startByte * 2; + m_lStoredSeekPoint = m_SJumpPoints[i - 2]->pos; + break; + } + } + } + + if (filepos == 0) { + readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE - 50), -1); + } else { + readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE / 2), filepos); + } + } + + + if (m_lCacheEndByte <= (unsigned long) filepos) { + readFramesToCache(100, filepos); + } + + m_iCurrentMixxTs = filepos; + + m_bIsSeeked = TRUE; + + return frameIndex; } + +unsigned int SoundSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { + + if (m_SCache.size() == 0) { + // Make sure we allways start at begining and cache have some + // material that we can consume. + seekSampleFrame(0); + m_bIsSeeked = FALSE; + } + + getBytesFromCache((char *)destination, m_iCurrentMixxTs, size); + + // As this is also Hack + // If we don't seek like we don't on analyzer.. keep + // place in mind.. + if (m_bIsSeeked == FALSE) { + m_iCurrentMixxTs += size; + } + + m_bIsSeeked = FALSE; + return size; +} + +SINT SoundSourceFFmpeg::readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) { + // This is just a hack that simply reuses existing + // functionality. Sample data should be resampled + // directly into AV_SAMPLE_FMT_FLT instead of + // AV_SAMPLE_FMT_S16! + typedef std::vector TempBuffer; + TempBuffer tempBuffer(frames2samples(numberOfFrames)); + const SINT readSamples = read(tempBuffer.size(), &tempBuffer[0]); + for (SINT i = 0; i < readSamples; ++i) { + sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / CSAMPLE(SAMPLE_MAX); + } + return samples2frames(readSamples); +} + +} // namespace Mixxx diff --git a/src/sources/soundsourceffmpeg.h b/src/sources/soundsourceffmpeg.h index 7ec46873369..776cdafd873 100644 --- a/src/sources/soundsourceffmpeg.h +++ b/src/sources/soundsourceffmpeg.h @@ -1,15 +1,88 @@ -#ifndef SOUNDSOURCEFFMPEG_H -#define SOUNDSOURCEFFMPEG_H +#ifndef MIXXX_SOUNDSOURCEFFMPEG_H +#define MIXXX_SOUNDSOURCEFFMPEG_H #include "sources/soundsource.h" -class SoundSourceFFmpeg : public Mixxx::SoundSource { +#include + +// Needed to ensure that macros in get defined. +#ifndef __STDC_CONSTANT_MACROS +#if __cplusplus < 201103L +#define __STDC_CONSTANT_MACROS +#endif +#endif + +#include +#include + +#ifndef __FFMPEGOLDAPI__ +#include +#include +#endif + +// Compability +#include +#include + +#include + +namespace Mixxx { + +struct ffmpegLocationObject { + quint64 pos; + qint64 pts; + quint64 startByte; +}; + +struct ffmpegCacheObject { + quint64 startByte; + quint32 length; + quint8 *bytes; +}; + +class SoundSourceFFmpeg : public SoundSource { public: static QList supportedFileExtensions(); explicit SoundSourceFFmpeg(QUrl url); + ~SoundSourceFFmpeg(); + + Result open() /*override*/; + void close() /*override*/; + + SINT seekSampleFrame(SINT frameIndex) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; + +private: + bool readFramesToCache(unsigned int count, qint64 offset); + bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); + quint64 getSizeofCache(); + void clearCache(); + + unsigned int read(unsigned long size, SAMPLE*); - Mixxx::AudioSourcePointer open() const /*override*/; + AVFormatContext *m_pFormatCtx; + int m_iAudioStream; + AVCodecContext *m_pCodecCtx; + AVCodec *m_pCodec; + + EncoderFfmpegResample *m_pResample; + + qint64 m_iCurrentMixxTs; + + bool m_bIsSeeked; + + quint64 m_lCacheBytePos; + quint64 m_lCacheStartByte; + quint64 m_lCacheEndByte; + quint32 m_lCacheLastPos; + QVector m_SCache; + QVector m_SJumpPoints; + quint64 m_lLastStoredPos; + qint64 m_lStoredSeekPoint; }; -#endif +} // namespace Mixxx + +#endif // MIXXX_SOUNDSOURCEFFMPEG_H diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 642ffcd7216..8465bd800c3 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -1,6 +1,61 @@ #include "sources/soundsourceflac.h" -#include "sources/audiosourceflac.h" +#include "sampleutil.h" +#include "util/math.h" + +namespace Mixxx { + +namespace { + +// begin callbacks (have to be regular functions because normal libFLAC isn't C++-aware) + +FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder*, + FLAC__byte buffer[], size_t* bytes, void* client_data) { + return static_cast(client_data)->flacRead(buffer, bytes); +} + +FLAC__StreamDecoderSeekStatus FLAC_seek_cb(const FLAC__StreamDecoder*, + FLAC__uint64 absolute_byte_offset, void* client_data) { + return static_cast(client_data)->flacSeek( + absolute_byte_offset); +} + +FLAC__StreamDecoderTellStatus FLAC_tell_cb(const FLAC__StreamDecoder*, + FLAC__uint64 *absolute_byte_offset, void* client_data) { + return static_cast(client_data)->flacTell( + absolute_byte_offset); +} + +FLAC__StreamDecoderLengthStatus FLAC_length_cb(const FLAC__StreamDecoder*, + FLAC__uint64 *stream_length, void* client_data) { + return static_cast(client_data)->flacLength(stream_length); +} + +FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder*, void* client_data) { + return static_cast(client_data)->flacEOF(); +} + +FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder*, + const FLAC__Frame* frame, const FLAC__int32* const buffer[], + void* client_data) { + return static_cast(client_data)->flacWrite(frame, buffer); +} + +void FLAC_metadata_cb(const FLAC__StreamDecoder*, + const FLAC__StreamMetadata* metadata, void* client_data) { + static_cast(client_data)->flacMetadata(metadata); +} + +void FLAC_error_cb(const FLAC__StreamDecoder*, + FLAC__StreamDecoderErrorStatus status, void* client_data) { + static_cast(client_data)->flacError(status); +} + +// end callbacks + +const unsigned kBitsPerSampleDefault = 0; + +} QList SoundSourceFLAC::supportedFileExtensions() { QList list; @@ -9,9 +64,380 @@ QList SoundSourceFLAC::supportedFileExtensions() { } SoundSourceFLAC::SoundSourceFLAC(QUrl url) - : SoundSource(url, "flac") { + : SoundSource(url, "flac"), + m_file(getLocalFileName()), + m_decoder(NULL), + m_minBlocksize(0), + m_maxBlocksize(0), + m_minFramesize(0), + m_maxFramesize(0), + m_bitsPerSample(kBitsPerSampleDefault), + m_sampleScaleFactor(kSampleValueZero), + m_decodeSampleBufferReadOffset(0), + m_decodeSampleBufferWriteOffset(0), + m_curFrameIndex(kFrameIndexMin) { +} + +SoundSourceFLAC::~SoundSourceFLAC() { + close(); +} + +Result SoundSourceFLAC::open() { + if (m_file.isOpen()) { + qWarning() << "File is already open:" << m_file.fileName(); + return ERR; + } + + if (!m_file.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file:" << m_file.fileName(); + return ERR; + } + + m_decoder = FLAC__stream_decoder_new(); + if (m_decoder == NULL) { + qWarning() << "SSFLAC: decoder allocation failed!"; + return ERR; + } + FLAC__stream_decoder_set_md5_checking(m_decoder, FALSE); + const FLAC__StreamDecoderInitStatus initStatus( + FLAC__stream_decoder_init_stream(m_decoder, FLAC_read_cb, + FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, + FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); + if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { + qWarning() << "SSFLAC: decoder init failed:" << initStatus; + return ERR; + } + if (!FLAC__stream_decoder_process_until_end_of_metadata(m_decoder)) { + qWarning() << "SSFLAC: process to end of meta failed:" + << FLAC__stream_decoder_get_state(m_decoder); + return ERR; + } + + m_curFrameIndex = kFrameIndexMin; + + return OK; } -Mixxx::AudioSourcePointer SoundSourceFLAC::open() const { - return Mixxx::AudioSourceFLAC::create(getUrl()); +void SoundSourceFLAC::close() { + if (m_decoder) { + FLAC__stream_decoder_finish(m_decoder); + FLAC__stream_decoder_delete(m_decoder); // frees memory + m_decoder = NULL; + } + + m_file.close(); +} + +SINT SoundSourceFLAC::seekSampleFrame(SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + + // clear decode buffer before seeking + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + if (!FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex)) { + qWarning() << "SSFLAC: Seeking error at file" << m_file.fileName(); + } + if ((FLAC__STREAM_DECODER_SEEK_ERROR == FLAC__stream_decoder_get_state(m_decoder)) && + !FLAC__stream_decoder_flush(m_decoder)) { + qWarning() << "SSFLAC: Failed to flush the decoder's input buffer after seeking" << m_file.fileName(); + } + m_curFrameIndex = frameIndex; + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + return m_curFrameIndex; +} + +SINT SoundSourceFLAC::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, + frames2samples(numberOfFrames), false); +} + +SINT SoundSourceFLAC::readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize) { + return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, + true); +} + +SINT SoundSourceFLAC::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize, bool readStereoSamples) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); + + const SINT numberOfFramesTotal = numberOfFrames; + + CSAMPLE* outBuffer = sampleBuffer; + SINT numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { + DEBUG_ASSERT( + m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + // if our buffer from libflac is empty (either because we explicitly cleared + // it or because we've simply used all the samples), ask for a new buffer + if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { + // Documentation of FLAC__stream_decoder_process_single(): + // "Depending on what was decoded, the metadata or write callback + // will be called with the decoded metadata block or audio frame." + // See also: https://xiph.org/flac/api/group__flac__stream__decoder.html#ga9d6df4a39892c05955122cf7f987f856 + if (FLAC__stream_decoder_process_single(m_decoder)) { + if (m_decodeSampleBufferReadOffset + >= m_decodeSampleBufferWriteOffset) { + // EOF + break; + } + } else { + qWarning() << "SSFLAC: decoder_process_single returned false (" + << m_file.fileName() << ")"; + break; + } + } + DEBUG_ASSERT( + m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + const SINT decodeBufferSamples = m_decodeSampleBufferWriteOffset + - m_decodeSampleBufferReadOffset; + const SINT decodeBufferFrames = samples2frames( + decodeBufferSamples); + const SINT framesToCopy = + math_min(decodeBufferFrames, numberOfFramesRemaining); + const SINT samplesToCopy = frames2samples(framesToCopy); + if (readStereoSamples && !isChannelCountStereo()) { + if (isChannelCountMono()) { + SampleUtil::copyMonoToDualMono(outBuffer, + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy); + } else { + SampleUtil::copyMultiToStereo(outBuffer, + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy, getChannelCount()); + } + outBuffer += framesToCopy * 2; // copied 2 samples per frame + } else { + SampleUtil::copy(outBuffer, + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + samplesToCopy); + outBuffer += samplesToCopy; + } + m_decodeSampleBufferReadOffset += samplesToCopy; + m_curFrameIndex += framesToCopy; + numberOfFramesRemaining -= framesToCopy; + DEBUG_ASSERT( + m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + } + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; +} + +// flac callback methods +FLAC__StreamDecoderReadStatus SoundSourceFLAC::flacRead(FLAC__byte buffer[], + size_t* bytes) { + *bytes = m_file.read((char*) buffer, *bytes); + if (*bytes > 0) { + return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; + } else if (*bytes == 0) { + return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; + } else { + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } } + +FLAC__StreamDecoderSeekStatus SoundSourceFLAC::flacSeek(FLAC__uint64 offset) { + if (m_file.seek(offset)) { + return FLAC__STREAM_DECODER_SEEK_STATUS_OK; + } else { + qWarning() << "SSFLAC: An unrecoverable error occurred (" + << m_file.fileName() << ")"; + return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR; + } +} + +FLAC__StreamDecoderTellStatus SoundSourceFLAC::flacTell(FLAC__uint64* offset) { + if (m_file.isSequential()) { + return FLAC__STREAM_DECODER_TELL_STATUS_UNSUPPORTED; + } + *offset = m_file.pos(); + return FLAC__STREAM_DECODER_TELL_STATUS_OK; +} + +FLAC__StreamDecoderLengthStatus SoundSourceFLAC::flacLength( + FLAC__uint64* length) { + if (m_file.isSequential()) { + return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; + } + *length = m_file.size(); + return FLAC__STREAM_DECODER_LENGTH_STATUS_OK; +} + +FLAC__bool SoundSourceFLAC::flacEOF() { + if (m_file.isSequential()) { + return false; + } + return m_file.atEnd(); +} + +FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( + const FLAC__Frame* frame, const FLAC__int32* const buffer[]) { + // decode buffer must be empty before decoding the next frame + DEBUG_ASSERT(m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset); + // reset decode buffer + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + if (getChannelCount() != frame->header.channels) { + qWarning() << "Corrupt or unsupported FLAC file:" + << "Invalid number of channels in FLAC frame header" + << frame->header.channels << "<>" << getChannelCount(); + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; + } + if (getFrameRate() != frame->header.sample_rate) { + qWarning() << "Corrupt or unsupported FLAC file:" + << "Invalid sample rate in FLAC frame header" + << frame->header.sample_rate << "<>" << getFrameRate(); + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; + } + const unsigned maxBlocksize = samples2frames( + m_decodeSampleBuffer.size()); + if (maxBlocksize < frame->header.blocksize) { + qWarning() << "Corrupt or unsupported FLAC file:" + << "Block size in FLAC frame header exceeds the maximum block size" + << frame->header.blocksize << ">" << maxBlocksize; + } + switch (getChannelCount()) { + case 1: { + // optimized code for 1 channel (mono) + DEBUG_ASSERT(1 <= frame->header.channels); + for (unsigned i = 0; i < frame->header.blocksize; ++i) { + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[0][i] * m_sampleScaleFactor; + } + break; + } + case 2: { + // optimized code for 2 channels (stereo) + DEBUG_ASSERT(2 <= frame->header.channels); + for (unsigned i = 0; i < frame->header.blocksize; ++i) { + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[0][i] * m_sampleScaleFactor; + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[1][i] * m_sampleScaleFactor; + } + break; + } + default: { + // generic code for multiple channels + DEBUG_ASSERT(getChannelCount() == frame->header.channels); + for (unsigned i = 0; i < frame->header.blocksize; ++i) { + for (unsigned j = 0; j < frame->header.channels; ++j) { + m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + buffer[j][i] * m_sampleScaleFactor; + } + } + } + } + DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; +} + +void SoundSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { + // https://xiph.org/flac/api/group__flac__stream__decoder.html#ga43e2329c15731c002ac4182a47990f85 + // "...one STREAMINFO block, followed by zero or more other metadata blocks." + // "...by default the decoder only calls the metadata callback for the STREAMINFO block..." + // "...always before the first audio frame (i.e. write callback)." + switch (metadata->type) { + case FLAC__METADATA_TYPE_STREAMINFO: + { + const SINT channelCount = metadata->data.stream_info.channels; + DEBUG_ASSERT(kChannelCountDefault != channelCount); + if (getChannelCount() == kChannelCountDefault) { + // not set before + setChannelCount(channelCount); + } else { + // already set before -> check for consistency + if (getChannelCount() != channelCount) { + qWarning() << "Unexpected channel count:" + << channelCount << " <> " << getChannelCount(); + } + } + const SINT frameRate = metadata->data.stream_info.sample_rate; + DEBUG_ASSERT(kFrameRateDefault != frameRate); + if (getFrameRate() == kFrameRateDefault) { + // not set before + setFrameRate(frameRate); + } else { + // already set before -> check for consistency + if (getFrameRate() != frameRate) { + qWarning() << "Unexpected frame/sample rate:" + << frameRate << " <> " << getFrameRate(); + } + } + const SINT frameCount = metadata->data.stream_info.total_samples; + DEBUG_ASSERT(kFrameCountDefault != frameCount); + if (getFrameCount() == kFrameCountDefault) { + // not set before + setFrameCount(frameCount); + } else { + // already set before -> check for consistency + if (getFrameCount() != frameCount) { + qWarning() << "Unexpected frame count:" + << frameCount << " <> " << getFrameCount(); + } + } + const unsigned bitsPerSample = metadata->data.stream_info.bits_per_sample; + DEBUG_ASSERT(kBitsPerSampleDefault != bitsPerSample); + if (kBitsPerSampleDefault == m_bitsPerSample) { + // not set before + m_bitsPerSample = bitsPerSample; + m_sampleScaleFactor = kSampleValuePeak + / CSAMPLE(FLAC__int32(1) << bitsPerSample); + } else { + // already set before -> check for consistency + if (bitsPerSample != m_bitsPerSample) { + qWarning() << "Unexpected bits per sample:" + << bitsPerSample << " <> " << m_bitsPerSample; + } + } + m_minBlocksize = metadata->data.stream_info.min_blocksize; + m_maxBlocksize = metadata->data.stream_info.max_blocksize; + m_minFramesize = metadata->data.stream_info.min_framesize; + m_maxFramesize = metadata->data.stream_info.max_framesize; + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; + const unsigned decodeSampleBufferSize = + m_maxBlocksize * getChannelCount(); + SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); + break; + } + default: + // Ignore all other metadata types + break; + } +} + +void SoundSourceFLAC::flacError(FLAC__StreamDecoderErrorStatus status) { + QString error; + // not much can be done at this point -- luckly the decoder seems to be + // pretty forgiving -- bkgood + switch (status) { + case FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC: + error = "STREAM_DECODER_ERROR_STATUS_LOST_SYNC"; + break; + case FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER: + error = "STREAM_DECODER_ERROR_STATUS_BAD_HEADER"; + break; + case FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH: + error = "STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH"; + break; + case FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM: + error = "STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM"; + break; + } + qWarning() << "SSFLAC got error" << error << "from libFLAC for file" + << m_file.fileName(); + // not much else to do here... whatever function that initiated whatever + // decoder method resulted in this error will return an error, and the caller + // will bail. libFLAC docs say to not close the decoder here -- bkgood +} + +} // namespace Mixxx diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index a84b754ffed..b089acdea61 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -1,15 +1,72 @@ -#ifndef SOUNDSOURCEFLAC_H -#define SOUNDSOURCEFLAC_H +#ifndef MIXXX_SOUNDSOURCEFLAC_H +#define MIXXX_SOUNDSOURCEFLAC_H #include "sources/soundsource.h" -class SoundSourceFLAC: public Mixxx::SoundSource { +#include "samplebuffer.h" + +#include + +#include + +namespace Mixxx { + +class SoundSourceFLAC: public SoundSource { public: static QList supportedFileExtensions(); explicit SoundSourceFLAC(QUrl url); + ~SoundSourceFLAC(); + + Result open() /*override*/; + void close() /*override*/; + + SINT seekSampleFrame(SINT frameIndex) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) /*override*/; + SINT readSampleFramesStereo(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; + // callback methods + FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t* bytes); + FLAC__StreamDecoderSeekStatus flacSeek(FLAC__uint64 offset); + FLAC__StreamDecoderTellStatus flacTell(FLAC__uint64* offset); + FLAC__StreamDecoderLengthStatus flacLength(FLAC__uint64* length); + FLAC__bool flacEOF(); + FLAC__StreamDecoderWriteStatus flacWrite(const FLAC__Frame *frame, + const FLAC__int32* const buffer[]); + void flacMetadata(const FLAC__StreamMetadata* metadata); + void flacError(FLAC__StreamDecoderErrorStatus status); + +private: + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize, + bool readStereoSamples); + + QFile m_file; + + FLAC__StreamDecoder *m_decoder; + // misc bits about the flac format: + // flac encodes from and decodes to LPCM in blocks, each block is made up of + // subblocks (one for each chan) + // flac stores in 'frames', each of which has a header and a certain number + // of subframes (one for each channel) + unsigned m_minBlocksize; // in time samples (audio samples = time samples * chanCount) + unsigned m_maxBlocksize; + unsigned m_minFramesize; + unsigned m_maxFramesize; + unsigned m_bitsPerSample; + + CSAMPLE m_sampleScaleFactor; + + SampleBuffer m_decodeSampleBuffer; + int m_decodeSampleBufferReadOffset; + int m_decodeSampleBufferWriteOffset; + + SINT m_curFrameIndex; }; -#endif // ifndef SOUNDSOURCEFLAC_H +} // namespace Mixxx + +#endif // MIXXX_SOUNDSOURCEFLAC_H diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index 1e7efb5f97f..5f7602f955c 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -1,15 +1,21 @@ #include "sources/soundsourcemodplug.h" -#include "sources/audiosourcemodplug.h" #include "metadata/trackmetadata.h" #include "util/timer.h" +#include + #include #include -#include +/* read files in 512k chunks */ +#define CHUNKSIZE (1 << 18) + +namespace Mixxx { -QString SoundSourceModPlug::getTypeFromUrl(QUrl url) { +namespace { + +QString getModPlugTypeFromUrl(QUrl url) { const QString type(SoundSource::getTypeFromUrl(url)); if (type == "mod") { return "Protracker"; @@ -30,6 +36,23 @@ QString SoundSourceModPlug::getTypeFromUrl(QUrl url) { } } +// identification of modplug module type +enum ModuleTypes { + NONE = 0x00, + MOD = 0x01, + S3M = 0x02, + XM = 0x04, + MED = 0x08, + IT = 0x20, + STM = 0x100, + OKT = 0x8000 +}; + +} // anonymous namespace + +const SINT SoundSourceModPlug::kChannelCount = 2; // always stereo +const SINT SoundSourceModPlug::kFrameRate = 44100; // always 44.1 kHz + QList SoundSourceModPlug::supportedFileExtensions() { QList list; // ModPlug supports more formats but file name @@ -44,12 +67,28 @@ QList SoundSourceModPlug::supportedFileExtensions() { return list; } -SoundSourceModPlug::SoundSourceModPlug(QUrl url) : - SoundSource(url, getTypeFromUrl(url)) { +unsigned int SoundSourceModPlug::s_bufferSizeLimit = 0; + +// reserve some static space for settings... +void SoundSourceModPlug::configure(unsigned int bufferSizeLimit, + const ModPlug::ModPlug_Settings &settings) { + s_bufferSizeLimit = bufferSizeLimit; + ModPlug::ModPlug_SetSettings(&settings); +} + +SoundSourceModPlug::SoundSourceModPlug(QUrl url) + : SoundSource(url, getModPlugTypeFromUrl(url)), + m_pModFile(NULL), + m_fileLength(0), + m_seekPos(0) { +} + +SoundSourceModPlug::~SoundSourceModPlug() { + close(); } -Result SoundSourceModPlug::parseMetadata( - Mixxx::TrackMetadata* pMetadata) const { +Result SoundSourceModPlug::parseTrackMetadata( + TrackMetadata* pMetadata) const { QFile modFile(getLocalFileNameBytes()); modFile.open(QIODevice::ReadOnly); const QByteArray fileBuf(modFile.readAll()); @@ -74,6 +113,99 @@ QImage SoundSourceModPlug::parseCoverArt() const { return QImage(); } -Mixxx::AudioSourcePointer SoundSourceModPlug::open() const { - return Mixxx::AudioSourceModPlug::create(getUrl()); +Result SoundSourceModPlug::open() { + ScopedTimer t("SoundSourceModPlug::open()"); + + // read module file to byte array + const QString fileName(getLocalFileName()); + QFile modFile(fileName); + qDebug() << "[ModPlug] Loading ModPlug module " << modFile.fileName(); + modFile.open(QIODevice::ReadOnly); + m_fileBuf = modFile.readAll(); + modFile.close(); + // get ModPlugFile descriptor for later access + m_pModFile = ModPlug::ModPlug_Load(m_fileBuf.constData(), + m_fileBuf.length()); + + if (m_pModFile == NULL) { + // an error occured + t.cancel(); + qDebug() << "[ModPlug] Could not load module file: " << fileName; + return ERR; + } + + // estimate size of sample buffer (for better performance) + // beware: module length estimation is unreliable due to loops + // song milliseconds * 2 (bytes per sample) + // * 2 (channels) + // * 44.1 (samples per millisecond) + // + some more to accomodate short loops etc. + // approximate and align with CHUNKSIZE yields: + // (((milliseconds << 1) >> 10 /* to seconds */) + // div 11 /* samples to chunksize ratio */) + // << 19 /* align to chunksize */ + unsigned int estimate = ((ModPlug::ModPlug_GetLength(m_pModFile) >> 8) / 11) + << 18; + estimate = math_min(estimate, s_bufferSizeLimit); + m_sampleBuf.reserve(estimate); + qDebug() << "[ModPlug] Reserved " << m_sampleBuf.capacity() << " #samples"; + + // decode samples to sample buffer + int samplesRead = -1; + int currentSize = 0; + while ((samplesRead != 0) && (m_sampleBuf.size() < s_bufferSizeLimit)) { + // reserve enough space in sample buffer + m_sampleBuf.resize(currentSize + CHUNKSIZE); + samplesRead = ModPlug::ModPlug_Read(m_pModFile, + m_sampleBuf.data() + currentSize, + CHUNKSIZE * 2) / 2; + // adapt to actual size + currentSize += samplesRead; + if (samplesRead != CHUNKSIZE) { + m_sampleBuf.resize(currentSize); + samplesRead = 0; // we reached the end of the file + } + } + qDebug() << "[ModPlug] Filled Sample buffer with " << m_sampleBuf.size() + << " samples."; + qDebug() << "[ModPlug] Sample buffer has " + << m_sampleBuf.capacity() - m_sampleBuf.size() + << " samples unused capacity."; + + setChannelCount(kChannelCount); + setFrameRate(kFrameRate); + setFrameCount(samples2frames(m_sampleBuf.size())); + m_seekPos = 0; + + return OK; +} + +void SoundSourceModPlug::close() { + if (m_pModFile) { + ModPlug::ModPlug_Unload(m_pModFile); + m_pModFile = NULL; + } +} + +SINT SoundSourceModPlug::seekSampleFrame( + SINT frameIndex) { + return m_seekPos = frameIndex; +} + +SINT SoundSourceModPlug::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + const SINT maxFrames = samples2frames(m_sampleBuf.size()); + const SINT readFrames = math_min(maxFrames - m_seekPos, numberOfFrames); + + const SINT readSamples = frames2samples(readFrames); + const SINT readOffset = frames2samples(m_seekPos); + for (SINT i = 0; i < readSamples; ++i) { + sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) + / CSAMPLE(SAMPLE_MAX); + } + + m_seekPos += readFrames; + return readFrames; } + +} // namespace Mixxx diff --git a/src/sources/soundsourcemodplug.h b/src/sources/soundsourcemodplug.h index 102d78d07a1..e3f74209e35 100644 --- a/src/sources/soundsourcemodplug.h +++ b/src/sources/soundsourcemodplug.h @@ -1,8 +1,16 @@ -#ifndef SOUNDSOURCEMODPLUG_H -#define SOUNDSOURCEMODPLUG_H +#ifndef MIXXX_SOUNDSOURCEMODPLUG_H +#define MIXXX_SOUNDSOURCEMODPLUG_H #include "sources/soundsource.h" +namespace ModPlug { +#include +} + +#include + +namespace Mixxx { + // Class for reading tracker files using libmodplug. // The whole file is decoded at once and saved // in RAM to allow seeking and smooth operation in Mixxx. @@ -10,15 +18,38 @@ class SoundSourceModPlug: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); - static QString getTypeFromUrl(QUrl url); + static const SINT kChannelCount; + static const SINT kFrameRate; + + // apply settings for decoding + static void configure(unsigned int bufferSizeLimit, + const ModPlug::ModPlug_Settings &settings); explicit SoundSourceModPlug(QUrl url); + ~SoundSourceModPlug(); - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + Result parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; QImage parseCoverArt() const /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; + Result open() /*override*/; + void close() /*override*/; + + SINT seekSampleFrame(SINT frameIndex) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) /*override*/; + +private: + static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) + + ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor + SINT m_fileLength; // length of file in samples + SINT m_seekPos; // current read position + QByteArray m_fileBuf; // original module file data + std::vector m_sampleBuf; // 16bit stereo samples, 44.1kHz }; -#endif +} // namespace Mixxx + +#endif // MIXXX_SOUNDSOURCEMODPLUG_H diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index a8ccd92e370..5c8b9102370 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -1,6 +1,63 @@ #include "sources/soundsourcemp3.h" -#include "sources/audiosourcemp3.h" +#include "util/math.h" + +#include + +namespace Mixxx { + +namespace { + +// In the worst case up to 29 MP3 frames need to be prefetched +// for accurate seeking: +// http://www.mars.org/mailman/public/mad-dev/2002-May/000634.html +const SINT kSeekFramePrefetchCount = 29; + +const CSAMPLE kMadScale = AudioSource::kSampleValuePeak + / CSAMPLE(MAD_F_ONE); + +inline CSAMPLE madScale(mad_fixed_t sample) { + return sample * kMadScale; + +} // anonymous namespace + +// Optimization: Reserve initial capacity for seek frame list +const SINT kMinutesPerFile = 10; // enough for the majority of files (tunable) +const SINT kSecondsPerMinute = 60; // fixed +const SINT kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 +const SINT kSeekFrameListCapacity = kMinutesPerFile + * kSecondsPerMinute * kMaxMp3FramesPerSecond; + +int decodeFrameHeader( + mad_header* pMadHeader, + mad_stream* pMadStream, + bool skipId3Tag) { + const int result = mad_header_decode(pMadHeader, pMadStream); + if (0 != result) { + if (MAD_RECOVERABLE(pMadStream->error)) { + if ((MAD_ERROR_LOSTSYNC == pMadStream->error) && skipId3Tag) { + long tagsize = id3_tag_query(pMadStream->this_frame, + pMadStream->bufend - pMadStream->this_frame); + if (0 < tagsize) { + // Skip ID3 tag data + mad_stream_skip(pMadStream, tagsize); + // Suppress lost synchronization warnings + return result; + } + } + qWarning() << "Recoverable MP3 header decoding error:" + << mad_stream_errorstr(pMadStream); + } else { + if (MAD_ERROR_BUFLEN != pMadStream->error) { // EOF + qWarning() << "Unrecoverable MP3 header decoding error:" + << mad_stream_errorstr(pMadStream); + } + } + } + return result; +} + +} // anonymous namespace QList SoundSourceMp3::supportedFileExtensions() { QList list; @@ -9,9 +66,490 @@ QList SoundSourceMp3::supportedFileExtensions() { } SoundSourceMp3::SoundSourceMp3(QUrl url) - : SoundSource(url, "mp3") { + : SoundSource(url, "mp3"), + m_file(getLocalFileName()), + m_fileSize(0), + m_pFileData(NULL), + m_avgSeekFrameCount(0), + m_curFrameIndex(kFrameIndexMin), + m_madSynthCount(0) { + m_seekFrameList.reserve(kSeekFrameListCapacity); +} + +SoundSourceMp3::~SoundSourceMp3() { + close(); +} + +Result SoundSourceMp3::open() { + if (m_file.isOpen()) { + qWarning() << "File is already open:" << m_file.fileName(); + return ERR; + } + + if (!m_file.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file:" << m_file.fileName(); + return ERR; + } + + // Get a pointer to the file using memory mapped IO + m_fileSize = m_file.size(); + m_pFileData = m_file.map(0, m_fileSize); + + // Transfer it to the mad stream-buffer: + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); + DEBUG_ASSERT(m_pFileData == m_madStream.this_frame); + + DEBUG_ASSERT(m_seekFrameList.empty()); + m_avgSeekFrameCount = 0; + m_curFrameIndex = kFrameIndexMin; + + // Decode all the headers and calculate audio properties + + mad_units madUnits = MAD_UNITS_44100_HZ; // default value + mad_timer_t madDuration = mad_timer_zero; + unsigned long sumBitrate = 0; + + mad_header madHeader; + mad_header_init(&madHeader); + do { + if (0 != decodeFrameHeader(&madHeader, &m_madStream, true)) { + if (MAD_RECOVERABLE(m_madStream.error)) { + continue; + } else { + if (MAD_ERROR_BUFLEN == m_madStream.error) { + // EOF + break; + } else { + // Abort + mad_header_finish(&madHeader); + return ERR; + } + } + } + + // Grab data from madHeader + const SINT madChannelCount = MAD_NCHANNELS(&madHeader); + if (kChannelCountDefault == getChannelCount()) { + // initially set the number of channels + setChannelCount(madChannelCount); + } else { + // check for consistent number of channels + if ((0 < madChannelCount) + && (getChannelCount() != madChannelCount)) { + qWarning() << "Differing number of channels in some headers:" + << m_file.fileName() << getChannelCount() << "<>" + << madChannelCount; + // Abort + mad_header_finish(&madHeader); + return ERR; + } + } + const SINT madSampleRate = madHeader.samplerate; + if (kFrameRateDefault == getFrameRate()) { + // initially set the frame/sample rate + setFrameRate(madSampleRate); + switch (madSampleRate) { + case 8000: + madUnits = MAD_UNITS_8000_HZ; + break; + case 11025: + madUnits = MAD_UNITS_11025_HZ; + break; + case 12000: + madUnits = MAD_UNITS_12000_HZ; + break; + case 16000: + madUnits = MAD_UNITS_16000_HZ; + break; + case 22050: + madUnits = MAD_UNITS_22050_HZ; + break; + case 24000: + madUnits = MAD_UNITS_24000_HZ; + break; + case 32000: + madUnits = MAD_UNITS_32000_HZ; + break; + case 44100: + madUnits = MAD_UNITS_44100_HZ; + break; + case 48000: + madUnits = MAD_UNITS_48000_HZ; + break; + default: + qWarning() << "Invalid sample rate:" << m_file.fileName() + << madSampleRate; + // Abort + mad_header_finish(&madHeader); + return ERR; + } + } else { + // check for consistent frame/sample rate + if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { + qWarning() << "Differing sample rate in some headers:" + << m_file.fileName() + << getFrameRate() << "<>" << madSampleRate; + // Abort + mad_header_finish(&madHeader); + return ERR; + } + } + + addSeekFrame(m_curFrameIndex, m_madStream.this_frame); + + // Accumulate data from the header + sumBitrate += madHeader.bitrate; + mad_timer_add(&madDuration, madHeader.duration); + + // Update current stream position + m_curFrameIndex = kFrameIndexMin + + mad_timer_count(madDuration, madUnits); + + DEBUG_ASSERT(NULL != m_madStream.this_frame); + DEBUG_ASSERT(0 < (m_madStream.this_frame - m_pFileData)); + } while (quint64(m_madStream.this_frame - m_pFileData) < m_fileSize); + + mad_header_finish(&madHeader); + + if (MAD_ERROR_NONE != m_madStream.error) { + // Unreachable code for recoverable errors + DEBUG_ASSERT(!MAD_RECOVERABLE(m_madStream.error)); + if (MAD_ERROR_BUFLEN != m_madStream.error) { + qWarning() << "Unrecoverable MP3 header error:" + << mad_stream_errorstr(&m_madStream); + // Abort + return ERR; + } + } + + if (m_seekFrameList.empty()) { + // This is not a working MP3 file. + qWarning() << "SSMP3: This is not a working MP3 file:" + << m_file.fileName(); + // Abort + return ERR; + } + + // Initialize the audio stream length + setFrameCount(m_curFrameIndex); + + // Calculate average values + m_avgSeekFrameCount = getFrameCount() / m_seekFrameList.size(); + const unsigned long avgBitrate = sumBitrate / m_seekFrameList.size(); + setBitrate(avgBitrate / 1000); + + // Terminate m_seekFrameList + addSeekFrame(m_curFrameIndex, 0); + + // Reset positions + m_curFrameIndex = kFrameIndexMin; + mad_frame_init(&m_madFrame); + mad_synth_init(&m_madSynth); + + // Restart decoding at the beginning of the audio stream + m_curFrameIndex = restartDecoding(m_seekFrameList.front()); + if (m_curFrameIndex != m_seekFrameList.front().frameIndex) { + qWarning() << "Failed to start decoding:" << m_file.fileName(); + // Abort + return ERR; + } + + return OK; } -Mixxx::AudioSourcePointer SoundSourceMp3::open() const { - return Mixxx::AudioSourceMp3::create(getUrl()); +void SoundSourceMp3::close() { + mad_synth_finish(&m_madSynth); + mad_frame_finish(&m_madFrame); + mad_stream_finish(&m_madStream); + + m_seekFrameList.clear(); + + if (NULL != m_pFileData) { + m_file.unmap(m_pFileData); + m_pFileData = NULL; + } + + m_file.close(); } + +SINT SoundSourceMp3::restartDecoding( + const SeekFrameType& seekFrame) { + qDebug() << "restartDecoding @" << seekFrame.frameIndex; + + // Discard decoded output + m_madSynthCount = 0; + + if (kFrameIndexMin == seekFrame.frameIndex) { + mad_frame_finish(&m_madFrame); + mad_synth_finish(&m_madSynth); + } + mad_stream_finish(&m_madStream); + + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + if (kFrameIndexMin == seekFrame.frameIndex) { + mad_synth_init(&m_madSynth); + mad_frame_init(&m_madFrame); + } + + // Fill input buffer + mad_stream_buffer(&m_madStream, seekFrame.pInputData, + m_fileSize - (seekFrame.pInputData - m_pFileData)); + + if (kFrameIndexMin < seekFrame.frameIndex) { + // Muting is done here to eliminate potential pops/clicks + // from skipping Rob Leslie explains why here: + // http://www.mars.org/mailman/public/mad-dev/2001-August/000321.html + mad_frame_mute(&m_madFrame); + mad_synth_mute(&m_madSynth); + } + + if (0 != decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) { + if (!MAD_RECOVERABLE(m_madStream.error)) { + // Failure -> Seek to EOF + return getFrameCount(); + } + } + + return seekFrame.frameIndex; +} + +void SoundSourceMp3::addSeekFrame( + SINT frameIndex, + const unsigned char* pInputData) { + DEBUG_ASSERT(m_seekFrameList.empty() || + (m_seekFrameList.back().frameIndex < frameIndex)); + DEBUG_ASSERT(m_seekFrameList.empty() || + (NULL == pInputData) || + (0 < (pInputData - m_seekFrameList.back().pInputData))); + SeekFrameType seekFrame; + seekFrame.pInputData = pInputData; + seekFrame.frameIndex = frameIndex; + m_seekFrameList.push_back(seekFrame); +} + +SINT SoundSourceMp3::findSeekFrameIndex( + SINT frameIndex) const { + // Check preconditions + DEBUG_ASSERT(0 < m_avgSeekFrameCount); + DEBUG_ASSERT(!m_seekFrameList.empty()); + DEBUG_ASSERT(kFrameIndexMin == m_seekFrameList.front().frameIndex); + DEBUG_ASSERT(SINT(kFrameIndexMin + getFrameIndexMax()) == m_seekFrameList.back().frameIndex); + + SINT lowerBound = + 0; + SINT upperBound = + m_seekFrameList.size(); + DEBUG_ASSERT(lowerBound < upperBound); + + // Initial guess based on average frame size + SINT seekFrameIndex = + frameIndex / m_avgSeekFrameCount; + if (seekFrameIndex >= upperBound) { + seekFrameIndex = upperBound - 1; + } + + while ((upperBound - lowerBound) > 1) { + DEBUG_ASSERT(seekFrameIndex >= lowerBound); + DEBUG_ASSERT(seekFrameIndex < upperBound); + DEBUG_ASSERT(m_seekFrameList[lowerBound].frameIndex <= frameIndex); + if (m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex) { + lowerBound = seekFrameIndex; + } else { + upperBound = seekFrameIndex; + } + // Next guess halfway between lower and upper bound + seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; + } + + // Check postconditions + DEBUG_ASSERT(seekFrameIndex == lowerBound); + DEBUG_ASSERT(SINT(m_seekFrameList.size()) > seekFrameIndex); + DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); + DEBUG_ASSERT(((seekFrameIndex + 1) >= SINT(m_seekFrameList.size())) || + (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); + + return seekFrameIndex; +} + +SINT SoundSourceMp3::seekSampleFrame(SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + + SINT seekFrameIndex = findSeekFrameIndex( + frameIndex); + DEBUG_ASSERT(SINT(m_seekFrameList.size()) > seekFrameIndex); + const SINT curSeekFrameIndex = findSeekFrameIndex( + m_curFrameIndex); + DEBUG_ASSERT(SINT(m_seekFrameList.size()) > curSeekFrameIndex); + // some consistency checks + DEBUG_ASSERT((curSeekFrameIndex >= seekFrameIndex) || (m_curFrameIndex < frameIndex)); + DEBUG_ASSERT((curSeekFrameIndex <= seekFrameIndex) || (m_curFrameIndex > frameIndex)); + if ((getFrameIndexMax() <= m_curFrameIndex) || // out of range + (frameIndex < m_curFrameIndex) || // seek backward + (seekFrameIndex > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jump forward + + // Adjust the seek frame index for prefetching + // Implementation note: The type SINT is unsigned so + // need to be careful when subtracting! + if (kSeekFramePrefetchCount < seekFrameIndex) { + // Restart decoding kSeekFramePrefetchCount seek frames + // before the expected sync position + seekFrameIndex -= kSeekFramePrefetchCount; + } else { + // Restart decoding at the beginning of the audio stream + seekFrameIndex = 0; + } + + m_curFrameIndex = restartDecoding(m_seekFrameList[seekFrameIndex]); + if (getFrameIndexMax() <= m_curFrameIndex) { + // out of range -> abort + return m_curFrameIndex; + } + DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == seekFrameIndex); + } + + // Decoding starts before the actual target position + DEBUG_ASSERT(m_curFrameIndex <= frameIndex); + + // Skip (= decode and discard) prefetch data + const SINT skipFrameCount = frameIndex - m_curFrameIndex; + skipSampleFrames(skipFrameCount); + DEBUG_ASSERT(m_curFrameIndex == frameIndex); + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + return m_curFrameIndex; +} + +SINT SoundSourceMp3::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + return readSampleFrames(numberOfFrames, + sampleBuffer, frames2samples(numberOfFrames), + false); +} + +SINT SoundSourceMp3::readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize) { + return readSampleFrames(numberOfFrames, + sampleBuffer, sampleBufferSize, + true); +} + +SINT SoundSourceMp3::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize, bool readStereoSamples) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); + + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); + + CSAMPLE* pSampleBuffer = sampleBuffer; + SINT numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { + if (0 >= m_madSynthCount) { + // When all decoded output data has been consumed... + DEBUG_ASSERT(0 == m_madSynthCount); + // ...decode the next MP3 frame + DEBUG_ASSERT(NULL != m_madStream.buffer); + DEBUG_ASSERT(NULL != m_madStream.this_frame); + + // WARNING: Correctly evaluating and handling the result + // of mad_frame_decode() has proven to be extremely tricky. + // Don't change anything at the following lines of code + // unless you know what you are doing!!! + unsigned char const* pMadThisFrame = m_madStream.this_frame; + if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { + if (MAD_RECOVERABLE(m_madStream.error)) { + if (pMadThisFrame != m_madStream.this_frame) { + // Ignore all recoverable errors (and especially + // "lost synchronization" warnings) while skipping + // over prefetched frames after seeking. + if (NULL == pSampleBuffer) { + // Decoded samples will simply be discarded + qDebug() << "Recoverable MP3 frame decoding error while skipping:" + << mad_stream_errorstr(&m_madStream); + } else { + qWarning() << "Recoverable MP3 frame decoding error:" + << mad_stream_errorstr(&m_madStream); + } + } + // Acknowledge error... + m_madStream.error = MAD_ERROR_NONE; + // ...and continue + } else { + if (MAD_ERROR_BUFLEN != m_madStream.error) { + qWarning() << "Unrecoverable MP3 frame decoding error:" + << mad_stream_errorstr(&m_madStream); + } + // Abort + break; + } + } + if (pMadThisFrame == m_madStream.this_frame) { + qDebug() << "Retry decoding MP3 frame @" << m_curFrameIndex; + // Retry + continue; + } + + DEBUG_ASSERT(getChannelCount() == MAD_NCHANNELS(&m_madFrame.header)); + + // Once decoded the frame is synthesized to PCM samples + mad_synth_frame(&m_madSynth, &m_madFrame); + DEBUG_ASSERT(getFrameRate() == m_madSynth.pcm.samplerate); + m_madSynthCount = m_madSynth.pcm.length; + DEBUG_ASSERT(0 < m_madSynthCount); + } + + const SINT synthReadCount = math_min( + m_madSynthCount, numberOfFramesRemaining); + if (NULL != pSampleBuffer) { + DEBUG_ASSERT(m_madSynthCount <= m_madSynth.pcm.length); + const SINT madSynthOffset = + m_madSynth.pcm.length - m_madSynthCount; + DEBUG_ASSERT(madSynthOffset < m_madSynth.pcm.length); + if (isChannelCountMono()) { + for (SINT i = 0; i < synthReadCount; ++i) { + const CSAMPLE sampleValue = madScale( + m_madSynth.pcm.samples[0][madSynthOffset + i]); + *pSampleBuffer = sampleValue; + ++pSampleBuffer; + if (readStereoSamples) { + *pSampleBuffer = sampleValue; + ++pSampleBuffer; + } + } + } else if (isChannelCountStereo() || readStereoSamples) { + for (SINT i = 0; i < synthReadCount; ++i) { + *pSampleBuffer = madScale( + m_madSynth.pcm.samples[0][madSynthOffset + i]); + ++pSampleBuffer; + *pSampleBuffer = madScale( + m_madSynth.pcm.samples[1][madSynthOffset + i]); + ++pSampleBuffer; + } + } else { + for (SINT i = 0; i < synthReadCount; ++i) { + for (SINT j = 0; j < getChannelCount(); ++j) { + *pSampleBuffer = madScale( + m_madSynth.pcm.samples[j][madSynthOffset + i]); + ++pSampleBuffer; + } + } + } + } + // consume decoded output data + m_madSynthCount -= synthReadCount; + m_curFrameIndex += synthReadCount; + numberOfFramesRemaining -= synthReadCount; + } + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; +} + +} // namespace Mixxx diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h index 5da814395e3..b31894105f2 100644 --- a/src/sources/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -1,15 +1,80 @@ -#ifndef SOUNDSOURCEMP3_H -#define SOUNDSOURCEMP3_H +#ifndef MIXXX_SOUNDSOURCEMP3_H +#define MIXXX_SOUNDSOURCEMP3_H #include "sources/soundsource.h" -class SoundSourceMp3: public Mixxx::SoundSource { +#ifdef _MSC_VER +// So mad.h doesn't try to use inline assembly which MSVC doesn't support. +// Notably, FPM_64BIT does not require a 64-bit machine. It merely requires a +// compiler that supports 64-bit types. +#define FPM_64BIT +#endif +#include + +#include + +#include + +namespace Mixxx { + +class SoundSourceMp3: public SoundSource { public: static QList supportedFileExtensions(); explicit SoundSourceMp3(QUrl url); + ~SoundSourceMp3(); + + Result open() /*override*/; + void close() /*override*/; + + SINT seekSampleFrame(SINT frameIndex) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) /*override*/; + SINT readSampleFramesStereo(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize, + bool readStereoSamples); + +private: + QFile m_file; + quint64 m_fileSize; + unsigned char* m_pFileData; - Mixxx::AudioSourcePointer open() const /*override*/; + /** Struct used to store mad frames for seeking */ + struct SeekFrameType { + SINT frameIndex; + const unsigned char* pInputData; + }; + + /** It is not possible to make a precise seek in an mp3 file without decoding the whole stream. + * To have precise seek within a limited range from the current decode position, we keep track + * of past decoded frame, and their exact position. If a seek occurs and it is within the + * range of frames we keep track of a precise seek occurs, otherwise an unprecise seek is performed + */ + typedef std::vector SeekFrameList; + SeekFrameList m_seekFrameList; // ordered-by frameIndex + SINT m_avgSeekFrameCount; // avg. sample frames per MP3 frame + + void addSeekFrame(SINT frameIndex, const unsigned char* pInputData); + + /** Returns the position in m_seekFrameList of the requested frame index. */ + SINT findSeekFrameIndex(SINT frameIndex) const; + + SINT m_curFrameIndex; + + SINT restartDecoding(const SeekFrameType& seekFrame); + + // MAD decoder + mad_stream m_madStream; + mad_frame m_madFrame; + mad_synth m_madSynth; + + SINT m_madSynthCount; // left overs from the previous read }; -#endif +} // namespace Mixxx + +#endif // MIXXX_SOUNDSOURCEMP3_H diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index b62d1edd69c..e64fb7323fc 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -1,6 +1,18 @@ #include "sources/soundsourceoggvorbis.h" -#include "sources/audiosourceoggvorbis.h" +namespace Mixxx { + +namespace { + +// Parameter for ov_info() +// See also: https://xiph.org/vorbis/doc/vorbisfile/ov_info.html +const int kCurrentBitstreamLink = -1; // retrieve ... for the current bitstream + +// Parameter for ov_pcm_total() +// See also: https://xiph.org/vorbis/doc/vorbisfile/ov_pcm_total.html +const int kEntireBitstreamLink = -1; // retrieve ... for the entire physical bitstream + +} // anonymous namespace QList SoundSourceOggVorbis::supportedFileExtensions() { QList list; @@ -8,10 +20,155 @@ QList SoundSourceOggVorbis::supportedFileExtensions() { return list; } -SoundSourceOggVorbis::SoundSourceOggVorbis(QUrl url) : - SoundSource(url, "ogg") { +SoundSourceOggVorbis::SoundSourceOggVorbis(QUrl url) + : SoundSource(url, "ogg"), + m_curFrameIndex(0) { + memset(&m_vf, 0, sizeof(m_vf)); +} + +SoundSourceOggVorbis::~SoundSourceOggVorbis() { + close(); +} + +Result SoundSourceOggVorbis::open() { + close(); // re-open if already open + + const QByteArray qbaFilename(getLocalFileNameBytes()); + if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { + qWarning() << "Failed to open OggVorbis file:" << getUrl(); + return ERR; + } + + if (!ov_seekable(&m_vf)) { + qWarning() << "OggVorbis file is not seekable:" << getUrl(); + return ERR; + } + + // lookup the ogg's channels and sample rate + const vorbis_info* vi = ov_info(&m_vf, kCurrentBitstreamLink); + if (!vi) { + qWarning() << "Failed to read OggVorbis file:" << getUrl(); + return ERR; + } + setChannelCount(vi->channels); + setFrameRate(vi->rate); + if (0 < vi->bitrate_nominal) { + setBitrate(vi->bitrate_nominal / 1000); + } else { + if ((0 < vi->bitrate_lower) && (vi->bitrate_lower == vi->bitrate_upper)) { + setBitrate(vi->bitrate_lower / 1000); + } + } + + ogg_int64_t pcmTotal = ov_pcm_total(&m_vf, kEntireBitstreamLink); + if (0 <= pcmTotal) { + setFrameCount(pcmTotal); + } else { + qWarning() << "Failed to read total length of OggVorbis file:" << getUrl(); + return ERR; + } + + return OK; } -Mixxx::AudioSourcePointer SoundSourceOggVorbis::open() const { - return Mixxx::AudioSourceOggVorbis::create(getUrl()); +void SoundSourceOggVorbis::close() { + const int clearResult = ov_clear(&m_vf); + if (0 != clearResult) { + qWarning() << "Failed to close OggVorbis file" << clearResult; + } } + +SINT SoundSourceOggVorbis::seekSampleFrame( + SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + + const int seekResult = ov_pcm_seek(&m_vf, frameIndex); + if (0 == seekResult) { + m_curFrameIndex = frameIndex; + } else { + qWarning() << "Failed to seek OggVorbis file:" << seekResult; + const ogg_int64_t pcmOffset = ov_pcm_tell(&m_vf); + if (0 <= pcmOffset) { + m_curFrameIndex = pcmOffset; + } else { + // Reset to EOF + m_curFrameIndex = getFrameIndexMax(); + } + } + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + return m_curFrameIndex; +} + +SINT SoundSourceOggVorbis::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, + frames2samples(numberOfFrames), false); +} + +SINT SoundSourceOggVorbis::readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize) { + return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, + true); +} + +SINT SoundSourceOggVorbis::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize, bool readStereoSamples) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); + + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); + + CSAMPLE* pSampleBuffer = sampleBuffer; + SINT numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { + float** pcmChannels; + int currentSection; + // Use 'long' here, because ov_read_float() returns this type. + // This is an exception from the rule not to any types with + // differing sizes on different platforms. + // https://bugs.launchpad.net/mixxx/+bug/1094143 + const long readResult = ov_read_float(&m_vf, &pcmChannels, + numberOfFramesRemaining, ¤tSection); + if (0 < readResult) { + m_curFrameIndex += readResult; + if (isChannelCountMono()) { + if (readStereoSamples) { + for (long i = 0; i < readResult; ++i) { + *pSampleBuffer++ = pcmChannels[0][i]; + *pSampleBuffer++ = pcmChannels[0][i]; + } + } else { + for (long i = 0; i < readResult; ++i) { + *pSampleBuffer++ = pcmChannels[0][i]; + } + } + } else if (isChannelCountStereo() || readStereoSamples) { + for (long i = 0; i < readResult; ++i) { + *pSampleBuffer++ = pcmChannels[0][i]; + *pSampleBuffer++ = pcmChannels[1][i]; + } + } else { + for (long i = 0; i < readResult; ++i) { + for (SINT j = 0; j < getChannelCount(); ++j) { + *pSampleBuffer++ = pcmChannels[j][i]; + } + } + } + numberOfFramesRemaining -= readResult; + } else { + qWarning() << "Failed to read from OggVorbis file:" << readResult; + break; // abort + } + } + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; +} + +} // namespace Mixxx diff --git a/src/sources/soundsourceoggvorbis.h b/src/sources/soundsourceoggvorbis.h index 58e56dfd201..3917f430ff0 100644 --- a/src/sources/soundsourceoggvorbis.h +++ b/src/sources/soundsourceoggvorbis.h @@ -1,15 +1,40 @@ -#ifndef SOUNDSOURCEOGGVORBIS_H -#define SOUNDSOURCEOGGVORBIS_H +#ifndef MIXXX_SOUNDSOURCEOGGVORBIS_H +#define MIXXX_SOUNDSOURCEOGGVORBIS_H + +#define OV_EXCLUDE_STATIC_CALLBACKS +#include #include "sources/soundsource.h" -class SoundSourceOggVorbis: public Mixxx::SoundSource { +namespace Mixxx { + +class SoundSourceOggVorbis: public SoundSource { public: static QList supportedFileExtensions(); explicit SoundSourceOggVorbis(QUrl url); + ~SoundSourceOggVorbis(); + + Result open() /*override*/; + void close() /*override*/; + + SINT seekSampleFrame(SINT frameIndex) /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) /*override*/; + SINT readSampleFramesStereo(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; + +private: + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize, + bool readStereoSamples); + + OggVorbis_File m_vf; + + SINT m_curFrameIndex; }; -#endif +} // namespace Mixxx + +#endif // MIXXX_SOUNDSOURCEOGGVORBIS_H diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index e876f023f32..2af4dfd13ac 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -1,19 +1,19 @@ #include "sources/soundsourceopus.h" -#include "sources/audiosourceopus.h" #include "metadata/trackmetadatataglib.h" -QList SoundSourceOpus::supportedFileExtensions() { - QList list; - list.push_back("opus"); - return list; -} - -SoundSourceOpus::SoundSourceOpus(QUrl url) : - SoundSource(url, "opus") { -} +namespace Mixxx { namespace { + +// Parameter for op_channel_count() +// See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html +const int kCurrentStreamLink = -1; // get ... of the current (stream) link + +// Parameter for op_pcm_total() and op_bitrate() +// See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html +const int kEntireStreamLink = -1; // get ... of the whole/entire stream + class OggOpusFileOwner { public: explicit OggOpusFileOwner(OggOpusFile* pFile) : @@ -29,12 +29,32 @@ class OggOpusFileOwner { OggOpusFileOwner(const OggOpusFileOwner&); // disable copy constructor OggOpusFile* const m_pFile; }; + +} // anonymous namespace + +// Decoded output of opusfile has a fixed sample rate of 48 kHz +const SINT SoundSourceOpus::kFrameRate = 48000; + +QList SoundSourceOpus::supportedFileExtensions() { + QList list; + list.push_back("opus"); + return list; +} + +SoundSourceOpus::SoundSourceOpus(QUrl url) + : SoundSource(url, "opus"), + m_pOggOpusFile(NULL), + m_curFrameIndex(kFrameIndexMin) { +} + +SoundSourceOpus::~SoundSourceOpus() { + close(); } /* Parse the file to get metadata */ -Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { +Result SoundSourceOpus::parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) const { if (OK == readTrackMetadataFromFile(pMetadata, getLocalFileName())) { return OK; } @@ -56,7 +76,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { const OpusTags *l_ptrOpusTags = op_tags(l_ptrOpusFile, -1); pMetadata->setChannels(op_channel_count(l_ptrOpusFile, -1)); - pMetadata->setSampleRate(Mixxx::AudioSourceOpus::kFrameRate); + pMetadata->setSampleRate(Mixxx::SoundSourceOpus::kFrameRate); pMetadata->setBitrate(op_bitrate(l_ptrOpusFile, -1) / 1000); pMetadata->setDuration( op_pcm_total(l_ptrOpusFile, -1) / pMetadata->getSampleRate()); @@ -102,6 +122,144 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { return OK; } -Mixxx::AudioSourcePointer SoundSourceOpus::open() const { - return Mixxx::AudioSourceOpus::create(getUrl()); +Result SoundSourceOpus::open() { + if (m_pOggOpusFile) { + qWarning() << "OggOpus file is already open:" << getUrl(); + return ERR; + } + + const QByteArray qbaFilename(getLocalFileNameBytes()); + int errorCode = 0; + m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); + if (!m_pOggOpusFile) { + qWarning() << "Failed to open OggOpus file:" << getUrl() << "errorCode" + << errorCode; + return ERR; + } + + if (!op_seekable(m_pOggOpusFile)) { + qWarning() << "OggOpus file is not seekable:" << getUrl(); + return ERR; + } + + const int channelCount = op_channel_count(m_pOggOpusFile, kCurrentStreamLink); + if (0 < channelCount) { + setChannelCount(channelCount); + } else { + qWarning() << "Failed to read channel configuration of OggOpus file:" << getUrl(); + return ERR; + } + + const ogg_int64_t pcmTotal = op_pcm_total(m_pOggOpusFile, kEntireStreamLink); + if (0 <= pcmTotal) { + setFrameCount(pcmTotal); + } else { + qWarning() << "Failed to read total length of OggOpus file:" << getUrl(); + return ERR; + } + + const opus_int32 bitrate = op_bitrate(m_pOggOpusFile, kEntireStreamLink); + if (0 < bitrate) { + setBitrate(bitrate / 1000); + } else { + qWarning() << "Failed to determine bitrate of OggOpus file:" << getUrl(); + return ERR; + } + + setFrameRate(kFrameRate); + + m_curFrameIndex = kFrameIndexMin; + + return OK; +} + +void SoundSourceOpus::close() { + if (m_pOggOpusFile) { + op_free(m_pOggOpusFile); + m_pOggOpusFile = NULL; + } +} + +SINT SoundSourceOpus::seekSampleFrame(SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + + int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); + if (0 == seekResult) { + m_curFrameIndex = frameIndex; + } else { + qWarning() << "Failed to seek OggOpus file:" << seekResult; + const ogg_int64_t pcmOffset = op_pcm_tell(m_pOggOpusFile); + if (0 <= pcmOffset) { + m_curFrameIndex = pcmOffset; + } else { + // Reset to EOF + m_curFrameIndex = getFrameIndexMax(); + } + } + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + return m_curFrameIndex; +} + +SINT SoundSourceOpus::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); + + CSAMPLE* pSampleBuffer = sampleBuffer; + SINT numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { + int readResult = op_read_float(m_pOggOpusFile, + pSampleBuffer, + frames2samples(numberOfFramesRemaining), NULL); + if (0 < readResult) { + m_curFrameIndex += readResult; + pSampleBuffer += frames2samples(readResult); + numberOfFramesRemaining -= readResult; + } else { + qWarning() << "Failed to read sample data from OggOpus file:" + << readResult; + break; // abort + } + } + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; } + +SINT SoundSourceOpus::readSampleFramesStereo( + SINT numberOfFrames, CSAMPLE* sampleBuffer, + SINT sampleBufferSize) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, true) <= sampleBufferSize); + + const SINT numberOfFramesTotal = math_min(numberOfFrames, + SINT(getFrameIndexMax() - m_curFrameIndex)); + + CSAMPLE* pSampleBuffer = sampleBuffer; + SINT numberOfFramesRemaining = numberOfFramesTotal; + while (0 < numberOfFramesRemaining) { + int readResult = op_read_float_stereo(m_pOggOpusFile, + pSampleBuffer, + numberOfFramesRemaining * 2); // stereo + if (0 < readResult) { + m_curFrameIndex += readResult; + pSampleBuffer += readResult * 2; // stereo + numberOfFramesRemaining -= readResult; + } else { + qWarning() << "Failed to read sample data from OggOpus file:" + << readResult; + break; // abort + } + } + + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); + return numberOfFramesTotal - numberOfFramesRemaining; +} + +} // namespace Mixxx diff --git a/src/sources/soundsourceopus.h b/src/sources/soundsourceopus.h index da090201de1..491ddbf4190 100644 --- a/src/sources/soundsourceopus.h +++ b/src/sources/soundsourceopus.h @@ -1,17 +1,40 @@ -#ifndef SOUNDSOURCEOPUS_H -#define SOUNDSOURCEOPUS_H +#ifndef MIXXX_SOUNDSOURCEOPUS_H +#define MIXXX_SOUNDSOURCEOPUS_H #include "sources/soundsource.h" +#define OV_EXCLUDE_STATIC_CALLBACKS +#include + +namespace Mixxx { + class SoundSourceOpus: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); + static const SINT kFrameRate; + explicit SoundSourceOpus(QUrl url); + ~SoundSourceOpus(); + + Result parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - Result parseMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; + Result open() /*override*/; + void close() /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) /*override*/; + SINT readSampleFramesStereo(SINT numberOfFrames, + CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; + +private: + OggOpusFile *m_pOggOpusFile; + + SINT m_curFrameIndex; }; -#endif +} // namespace Mixxx + +#endif // MIXXX_SOUNDSOURCEOPUS_H diff --git a/src/sources/soundsourceplugin.h b/src/sources/soundsourceplugin.h index 8dbc66c4ce9..cce1e3e22fd 100644 --- a/src/sources/soundsourceplugin.h +++ b/src/sources/soundsourceplugin.h @@ -2,6 +2,14 @@ #define MIXXX_SOUNDSOURCEPLUGIN_H #include "sources/soundsource.h" +#include "defs_version.h" + +// Getter functions to be declared by all SoundSource plugins +typedef Mixxx::SoundSource* (*getSoundSourceFunc)(QString fileName); +typedef char** (*getSupportedFileExtensionsFunc)(); +typedef int (*getSoundSourceAPIVersionFunc)(); +/// New in version 3 +typedef void (*freeFileExtensionsFunc)(char** fileExts); namespace Mixxx { diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index a08bd3acc88..44c79c3765e 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -1,6 +1,6 @@ #include "sources/soundsourcesndfile.h" -#include "sources/audiosourcesndfile.h" +namespace Mixxx { QList SoundSourceSndFile::supportedFileExtensions() { QList list; @@ -12,9 +12,88 @@ QList SoundSourceSndFile::supportedFileExtensions() { } SoundSourceSndFile::SoundSourceSndFile(QUrl url) - : SoundSource(url) { + : SoundSource(url), + m_pSndFile(NULL) { } -Mixxx::AudioSourcePointer SoundSourceSndFile::open() const { - return Mixxx::AudioSourceSndFile::create(getUrl()); +SoundSourceSndFile::~SoundSourceSndFile() { + close(); } + +Result SoundSourceSndFile::open() { + if (m_pSndFile) { + qWarning() << "File is already open:" << getUrl(); + return ERR; + } + + memset(&m_sfInfo, 0, sizeof(m_sfInfo)); +#ifdef __WINDOWS__ + // Pointer valid until string changed + const QString fileName(getLocalFileName()); + LPCWSTR lpcwFilename = (LPCWSTR) fileName.utf16(); + m_pSndFile = sf_wchar_open(lpcwFilename, SFM_READ, &m_sfInfo); +#else + m_pSndFile = sf_open(getLocalFileNameBytes().constData(), SFM_READ, + &m_sfInfo); +#endif + + if (!m_pSndFile) { // sf_format_check is only for writes + qWarning() << "Error opening libsndfile file:" << getUrl() + << sf_strerror(m_pSndFile); + return ERR; + } + + if (sf_error(m_pSndFile) > 0) { + qWarning() << "Error opening libsndfile file:" << getUrl() + << sf_strerror(m_pSndFile); + return ERR; + } + + setChannelCount(m_sfInfo.channels); + setFrameRate(m_sfInfo.samplerate); + setFrameCount(m_sfInfo.frames); + + return OK; +} + +void SoundSourceSndFile::close() { + if (m_pSndFile) { + const int closeResult = sf_close(m_pSndFile); + if (0 == closeResult) { + m_pSndFile = NULL; + } else { + qWarning() << "Failed to close libsnd file:" << closeResult + << sf_strerror(m_pSndFile) + << getUrl(); + } + } +} + +SINT SoundSourceSndFile::seekSampleFrame( + SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + + const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); + if (0 <= seekResult) { + return seekResult; + } else { + qWarning() << "Failed to seek libsnd file:" << seekResult + << sf_strerror(m_pSndFile); + return sf_seek(m_pSndFile, 0, SEEK_CUR); + } +} + +SINT SoundSourceSndFile::readSampleFrames( + SINT numberOfFrames, CSAMPLE* sampleBuffer) { + const sf_count_t readCount = + sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); + if (0 <= readCount) { + return readCount; + } else { + qWarning() << "Failed to read from libsnd file:" << readCount + << sf_strerror(m_pSndFile); + return 0; + } +} + +} // namespace Mixxx diff --git a/src/sources/soundsourcesndfile.h b/src/sources/soundsourcesndfile.h index 3e32a4f2083..f93f06b59b6 100644 --- a/src/sources/soundsourcesndfile.h +++ b/src/sources/soundsourcesndfile.h @@ -1,15 +1,38 @@ -#ifndef SOUNDSOURCESNDFILE_H -#define SOUNDSOURCESNDFILE_H +#ifndef MIXXX_SOUNDSOURCESNDFILE_H +#define MIXXX_SOUNDSOURCESNDFILE_H #include "sources/soundsource.h" +#ifdef Q_OS_WIN +//Enable unicode in libsndfile on Windows +//(sf_open uses UTF-8 otherwise) +#include +#define ENABLE_SNDFILE_WINDOWS_PROTOTYPES 1 +#endif +#include + +namespace Mixxx { + class SoundSourceSndFile: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); explicit SoundSourceSndFile(QUrl url); + ~SoundSourceSndFile(); + + Result open() /*override*/; + void close() /*override*/; - Mixxx::AudioSourcePointer open() const /*override*/; + SINT seekSampleFrame(SINT frameIndex) /*override*/; + + SINT readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) /*override*/; + +private: + SNDFILE* m_pSndFile; + SF_INFO m_sfInfo; }; -#endif +} // namespace Mixxx + +#endif // MIXXX_SOUNDSOURCESNDFILE_H diff --git a/src/test/coverartcache_test.cpp b/src/test/coverartcache_test.cpp index d9b2ba9ecf7..08525d9d301 100644 --- a/src/test/coverartcache_test.cpp +++ b/src/test/coverartcache_test.cpp @@ -52,9 +52,7 @@ TEST_F(CoverArtCacheTest, loadCover) { SecurityTokenPointer securityToken = Sandbox::openSecurityToken( QDir(kTrackLocationTest), true); SoundSourceProxy proxy(kTrackLocationTest, securityToken); - Mixxx::SoundSourcePointer pSoundSource(proxy.getSoundSource()); - ASSERT_TRUE(pSoundSource); - img = pSoundSource->parseCoverArt(); + img = proxy.parseCoverArt(); EXPECT_EQ(img, res.cover.image); } diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 91b726a437e..2b2cfe267cf 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -26,11 +26,8 @@ class SoundSourceProxyTest: public MixxxTest { SoundSourceProxy::loadPlugins(); } - static Mixxx::SoundSourcePointer openSoundSource(const QString& fileName) { - return SoundSourceProxy(fileName, SecurityTokenPointer()).getSoundSource(); - } - static Mixxx::AudioSourcePointer openAudioSource(const QString& fileName) { - return SoundSourceProxy(fileName, SecurityTokenPointer()).openAudioSource(); + static Mixxx::AudioSourcePointer open(const QString& fileName) { + return SoundSourceProxy(fileName).open(); } }; @@ -43,7 +40,7 @@ TEST_F(SoundSourceProxyTest, open) { const QString filePath(kFilePathPrefix + fileExtension); ASSERT_TRUE(SoundSourceProxy::isFilenameSupported(filePath)); - Mixxx::AudioSourcePointer pAudioSource(openAudioSource(filePath)); + Mixxx::AudioSourcePointer pAudioSource(open(filePath)); ASSERT_TRUE(!pAudioSource.isNull()); EXPECT_LT(0, pAudioSource->getChannelCount()); EXPECT_LT(0, pAudioSource->getFrameRate()); @@ -52,20 +49,18 @@ TEST_F(SoundSourceProxyTest, open) { } TEST_F(SoundSourceProxyTest, readArtist) { - Mixxx::SoundSourcePointer pSoundSource(openSoundSource( - QDir::currentPath().append("/src/test/id3-test-data/artist.mp3"))); - ASSERT_TRUE(!pSoundSource.isNull()); + SoundSourceProxy proxy( + QDir::currentPath().append("/src/test/id3-test-data/artist.mp3")); Mixxx::TrackMetadata trackMetadata; - EXPECT_EQ(OK, pSoundSource->parseMetadata(&trackMetadata)); + EXPECT_EQ(OK, proxy.parseTrackMetadata(&trackMetadata)); EXPECT_EQ("Test Artist", trackMetadata.getArtist()); } TEST_F(SoundSourceProxyTest, TOAL_TPE2) { - Mixxx::SoundSourcePointer pSoundSource(openSoundSource( - QDir::currentPath().append("/src/test/id3-test-data/TOAL_TPE2.mp3"))); - ASSERT_TRUE(!pSoundSource.isNull()); + SoundSourceProxy proxy( + QDir::currentPath().append("/src/test/id3-test-data/TOAL_TPE2.mp3")); Mixxx::TrackMetadata trackMetadata; - EXPECT_EQ(OK, pSoundSource->parseMetadata(&trackMetadata)); + EXPECT_EQ(OK, proxy.parseTrackMetadata(&trackMetadata)); EXPECT_EQ("TITLE2", trackMetadata.getArtist()); EXPECT_EQ("ARTIST", trackMetadata.getAlbum()); EXPECT_EQ("TITLE", trackMetadata.getAlbumArtist()); @@ -92,8 +87,7 @@ TEST_F(SoundSourceProxyTest, seekForward) { const QString filePath(kFilePathPrefix + fileExtension); ASSERT_TRUE(SoundSourceProxy::isFilenameSupported(filePath)); - Mixxx::AudioSourcePointer pContReadSource( - openAudioSource(filePath)); + Mixxx::AudioSourcePointer pContReadSource(open(filePath)); ASSERT_FALSE(pContReadSource.isNull()); const SINT readSampleCount = pContReadSource->frames2samples(kReadFrameCount); SampleBuffer contReadData(readSampleCount); @@ -106,8 +100,7 @@ TEST_F(SoundSourceProxyTest, seekForward) { const SINT contReadFrameCount = pContReadSource->readSampleFrames(kReadFrameCount, &contReadData[0]); - Mixxx::AudioSourcePointer pSeekReadSource( - openAudioSource(filePath)); + Mixxx::AudioSourcePointer pSeekReadSource(open(filePath)); ASSERT_FALSE(pSeekReadSource.isNull()); ASSERT_EQ(pContReadSource->getChannelCount(), pSeekReadSource->getChannelCount()); ASSERT_EQ(pContReadSource->getFrameCount(), pSeekReadSource->getFrameCount()); diff --git a/src/trackinfoobject.cpp b/src/trackinfoobject.cpp index 296e358249c..1aefeb2dea6 100644 --- a/src/trackinfoobject.cpp +++ b/src/trackinfoobject.cpp @@ -250,15 +250,12 @@ void TrackInfoObject::parse(bool parseCoverArt) { } SoundSourceProxy proxy(canonicalLocation, m_pSecurityToken); - Mixxx::SoundSourcePointer pSoundSource(proxy.getSoundSource()); - if (pSoundSource) { - // If we've got a SoundSource then it must have a type! - DEBUG_ASSERT(!pSoundSource->getType().isEmpty()); - setType(pSoundSource->getType()); + if (!proxy.getType().isEmpty()) { + setType(proxy.getType()); // Parse the information stored in the sound file. Mixxx::TrackMetadata trackMetadata; - if (pSoundSource->parseMetadata(&trackMetadata) == OK) { + if (proxy.parseTrackMetadata(&trackMetadata) == OK) { // If Artist, Title and Type fields are not blank, modify them. // Otherwise, keep their current values. // TODO(rryan): Should we re-visit this decision? @@ -274,7 +271,7 @@ void TrackInfoObject::parse(bool parseCoverArt) { } if (parseCoverArt) { - m_coverArt.image = pSoundSource->parseCoverArt(); + m_coverArt.image = proxy.parseCoverArt(); if (!m_coverArt.image.isNull()) { m_coverArt.info.hash = CoverArtUtils::calculateHash( m_coverArt.image); From ef29fd5111b7735e9a53c3d33bc752cb6fac25f6 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 28 Feb 2015 02:19:53 +0100 Subject: [PATCH 215/481] Fix minor issues after refactoring --- src/sources/metadatasource.h | 1 + src/sources/soundsource.h | 4 +--- src/sources/soundsourceflac.cpp | 10 +++++----- src/sources/soundsourcemp3.cpp | 19 +++++++++---------- src/sources/soundsourceoggvorbis.cpp | 2 +- src/sources/soundsourceopus.cpp | 2 +- src/sources/soundsourcesndfile.cpp | 4 ++-- 7 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/sources/metadatasource.h b/src/sources/metadatasource.h index 67f2488442f..4eb399e99a0 100644 --- a/src/sources/metadatasource.h +++ b/src/sources/metadatasource.h @@ -2,6 +2,7 @@ #define METADATASOURCE_H #include "metadata/trackmetadata.h" +#include "util/defs.h" // Result #include diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index 7d2e2235d1e..f6449845067 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -12,10 +12,8 @@ 7 - Mixxx 1.13.0 New SoundSource/AudioSource API */ -#include "sources/audiosource.h" #include "sources/metadatasource.h" - -#include +#include "sources/audiosource.h" namespace Mixxx { diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 8465bd800c3..ba5c7f90056 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -84,18 +84,18 @@ SoundSourceFLAC::~SoundSourceFLAC() { Result SoundSourceFLAC::open() { if (m_file.isOpen()) { - qWarning() << "File is already open:" << m_file.fileName(); + qWarning() << "Cannot reopen FLAC file:" << m_file.fileName(); return ERR; } if (!m_file.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file:" << m_file.fileName(); + qWarning() << "Failed to open FLAC file:" << m_file.fileName(); return ERR; } m_decoder = FLAC__stream_decoder_new(); if (m_decoder == NULL) { - qWarning() << "SSFLAC: decoder allocation failed!"; + qWarning() << "Failed to create FLAC decoder!"; return ERR; } FLAC__stream_decoder_set_md5_checking(m_decoder, FALSE); @@ -104,11 +104,11 @@ Result SoundSourceFLAC::open() { FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { - qWarning() << "SSFLAC: decoder init failed:" << initStatus; + qWarning() << "Failed to initialize FLAC decoder:" << initStatus; return ERR; } if (!FLAC__stream_decoder_process_until_end_of_metadata(m_decoder)) { - qWarning() << "SSFLAC: process to end of meta failed:" + qWarning() << "Failed to process FLAC metadata:" << FLAC__stream_decoder_get_state(m_decoder); return ERR; } diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 5c8b9102370..c96b9594c25 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -82,7 +82,7 @@ SoundSourceMp3::~SoundSourceMp3() { Result SoundSourceMp3::open() { if (m_file.isOpen()) { - qWarning() << "File is already open:" << m_file.fileName(); + qWarning() << "Cannot reopen MP3 file:" << m_file.fileName(); return ERR; } @@ -100,6 +100,8 @@ Result SoundSourceMp3::open() { mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); DEBUG_ASSERT(m_pFileData == m_madStream.this_frame); + mad_frame_init(&m_madFrame); + mad_synth_init(&m_madSynth); DEBUG_ASSERT(m_seekFrameList.empty()); m_avgSeekFrameCount = 0; @@ -245,8 +247,6 @@ Result SoundSourceMp3::open() { // Reset positions m_curFrameIndex = kFrameIndexMin; - mad_frame_init(&m_madFrame); - mad_synth_init(&m_madSynth); // Restart decoding at the beginning of the audio stream m_curFrameIndex = restartDecoding(m_seekFrameList.front()); @@ -260,18 +260,17 @@ Result SoundSourceMp3::open() { } void SoundSourceMp3::close() { - mad_synth_finish(&m_madSynth); - mad_frame_finish(&m_madFrame); - mad_stream_finish(&m_madStream); - - m_seekFrameList.clear(); - - if (NULL != m_pFileData) { + if (m_pFileData) { + mad_synth_finish(&m_madSynth); + mad_frame_finish(&m_madFrame); + mad_stream_finish(&m_madStream); m_file.unmap(m_pFileData); m_pFileData = NULL; } m_file.close(); + + m_seekFrameList.clear(); } SINT SoundSourceMp3::restartDecoding( diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index e64fb7323fc..7251bc332c0 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -31,7 +31,7 @@ SoundSourceOggVorbis::~SoundSourceOggVorbis() { } Result SoundSourceOggVorbis::open() { - close(); // re-open if already open + close(); // reopen if already open const QByteArray qbaFilename(getLocalFileNameBytes()); if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 2af4dfd13ac..a65236b7987 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -124,7 +124,7 @@ Result SoundSourceOpus::parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) cons Result SoundSourceOpus::open() { if (m_pOggOpusFile) { - qWarning() << "OggOpus file is already open:" << getUrl(); + qWarning() << "Cannot reopen OggOpus file:" << getUrl(); return ERR; } diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 44c79c3765e..089c411777e 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -22,7 +22,7 @@ SoundSourceSndFile::~SoundSourceSndFile() { Result SoundSourceSndFile::open() { if (m_pSndFile) { - qWarning() << "File is already open:" << getUrl(); + qWarning() << "Cannot reopen file:" << getUrl(); return ERR; } @@ -62,7 +62,7 @@ void SoundSourceSndFile::close() { if (0 == closeResult) { m_pSndFile = NULL; } else { - qWarning() << "Failed to close libsnd file:" << closeResult + qWarning() << "Failed to close file:" << closeResult << sf_strerror(m_pSndFile) << getUrl(); } From d607f14666787255522377219c22ec2ee5f0ff6d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 28 Feb 2015 18:40:28 +0100 Subject: [PATCH 216/481] Keep track of opening/closing AudioSource through SoundSourceProxy --- src/analyserqueue.cpp | 2 +- src/cachingreaderworker.cpp | 2 +- src/musicbrainz/chromaprinter.cpp | 2 +- src/soundsourceproxy.cpp | 29 +++++++++++++++++++++++------ src/soundsourceproxy.h | 12 ++++++------ src/sources/audiosource.h | 5 ++--- src/test/soundproxy_test.cpp | 10 +++++----- 7 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 3dfe2881752..13cd3b3bc02 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -309,7 +309,7 @@ void AnalyserQueue::run() { // Get the audio SoundSourceProxy soundSourceProxy(nextTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); if (!pAudioSource) { qWarning() << "Failed to open file for analyzing:" << nextTrack->getLocation(); continue; diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 42b701c1f1d..8cb4979b140 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -136,7 +136,7 @@ namespace { Mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); if (pAudioSource.isNull()) { qWarning() << "Failed to open file:" << pTrack->getLocation(); return Mixxx::AudioSourcePointer(); diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 19721a4a958..35678917c64 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -99,7 +99,7 @@ ChromaPrinter::ChromaPrinter(QObject* parent) QString ChromaPrinter::getFingerprint(TrackPointer pTrack) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.open()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); if (pAudioSource.isNull()) { qDebug() << "Skipping invalid file:" << pTrack->getLocation(); return QString(); diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index af7c2addba4..0b3482e9f97 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -312,25 +312,32 @@ QLibrary* SoundSourceProxy::getPlugin(QString lib_filename) return pPlugin; } -Mixxx::AudioSourcePointer SoundSourceProxy::open() const { +Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource() { + if (m_pAudioSource) { + qDebug() << "AudioSource is already open"; + return m_pAudioSource; + } + if (!m_pSoundSource) { - return Mixxx::AudioSourcePointer(); + qDebug() << "No SoundSource available"; + DEBUG_ASSERT(!m_pAudioSource); + return m_pAudioSource; } if (OK != m_pSoundSource->open()) { qWarning() << "Failed to open SoundSource"; - return Mixxx::AudioSourcePointer(); + return m_pAudioSource; } if (!m_pSoundSource->isValid()) { qWarning() << "Invalid file:" << m_pSoundSource->getUrl() << "channels" << m_pSoundSource->getChannelCount() << "frame rate" << m_pSoundSource->getChannelCount(); - return Mixxx::AudioSourcePointer(); + return m_pAudioSource; } if (m_pSoundSource->isEmpty()) { qWarning() << "Empty file:" << m_pSoundSource->getUrl(); - return Mixxx::AudioSourcePointer(); + return m_pAudioSource; } // Overwrite metadata with actual audio properties @@ -345,7 +352,17 @@ Mixxx::AudioSourcePointer SoundSourceProxy::open() const { } } - return m_pSoundSource; + m_pAudioSource = m_pSoundSource; + + return m_pAudioSource; +} + +void SoundSourceProxy::closeAudioSource() { + if (m_pAudioSource) { + DEBUG_ASSERT(m_pSoundSource); + m_pSoundSource->close(); + m_pAudioSource.clear(); + } } // static diff --git a/src/soundsourceproxy.h b/src/soundsourceproxy.h index 867aab405c2..9c8c2acad8a 100644 --- a/src/soundsourceproxy.h +++ b/src/soundsourceproxy.h @@ -59,13 +59,9 @@ class SoundSourceProxy: public Mixxx::MetadataSource { // Opening the audio data through the proxy will // update the some metadata of the track object. // Returns a null pointer on failure. - Mixxx::AudioSourcePointer open() const; + Mixxx::AudioSourcePointer openAudioSource(); - void close() const { - if (m_pSoundSource) { - m_pSoundSource->close(); - } - } + void closeAudioSource(); private: static QRegExp m_supportedFileRegex; @@ -81,6 +77,10 @@ class SoundSourceProxy: public Mixxx::MetadataSource { const SecurityTokenPointer m_pSecurityToken; const Mixxx::SoundSourcePointer m_pSoundSource; + + // Just an alias that keeps track of opening and closing + // the corresponding SoundSource. + Mixxx::AudioSourcePointer m_pAudioSource; }; #endif diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 5e6a5606ad4..8b959d9a3a1 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -12,9 +12,6 @@ namespace Mixxx { -class AudioSource; -typedef QSharedPointer AudioSourcePointer; - // Common interface and base class for audio sources. // // Both the number of channels and the frame rate must @@ -259,6 +256,8 @@ class AudioSource: public UrlResource { SINT m_bitrate; }; +typedef QSharedPointer AudioSourcePointer; + } // namespace Mixxx #endif // MIXXX_AUDIOSOURCE_H diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 2b2cfe267cf..2181b59e706 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -26,8 +26,8 @@ class SoundSourceProxyTest: public MixxxTest { SoundSourceProxy::loadPlugins(); } - static Mixxx::AudioSourcePointer open(const QString& fileName) { - return SoundSourceProxy(fileName).open(); + static Mixxx::AudioSourcePointer openAudioSource(const QString& fileName) { + return SoundSourceProxy(fileName).openAudioSource(); } }; @@ -40,7 +40,7 @@ TEST_F(SoundSourceProxyTest, open) { const QString filePath(kFilePathPrefix + fileExtension); ASSERT_TRUE(SoundSourceProxy::isFilenameSupported(filePath)); - Mixxx::AudioSourcePointer pAudioSource(open(filePath)); + Mixxx::AudioSourcePointer pAudioSource(openAudioSource(filePath)); ASSERT_TRUE(!pAudioSource.isNull()); EXPECT_LT(0, pAudioSource->getChannelCount()); EXPECT_LT(0, pAudioSource->getFrameRate()); @@ -87,7 +87,7 @@ TEST_F(SoundSourceProxyTest, seekForward) { const QString filePath(kFilePathPrefix + fileExtension); ASSERT_TRUE(SoundSourceProxy::isFilenameSupported(filePath)); - Mixxx::AudioSourcePointer pContReadSource(open(filePath)); + Mixxx::AudioSourcePointer pContReadSource(openAudioSource(filePath)); ASSERT_FALSE(pContReadSource.isNull()); const SINT readSampleCount = pContReadSource->frames2samples(kReadFrameCount); SampleBuffer contReadData(readSampleCount); @@ -100,7 +100,7 @@ TEST_F(SoundSourceProxyTest, seekForward) { const SINT contReadFrameCount = pContReadSource->readSampleFrames(kReadFrameCount, &contReadData[0]); - Mixxx::AudioSourcePointer pSeekReadSource(open(filePath)); + Mixxx::AudioSourcePointer pSeekReadSource(openAudioSource(filePath)); ASSERT_FALSE(pSeekReadSource.isNull()); ASSERT_EQ(pContReadSource->getChannelCount(), pSeekReadSource->getChannelCount()); ASSERT_EQ(pContReadSource->getFrameCount(), pSeekReadSource->getFrameCount()); From 9a4a2ccb404d6080bdb2b5a49f0d2ca78886946f Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 1 Mar 2015 05:24:03 +0100 Subject: [PATCH 217/481] changing group to channel --- src/effects/native/autopaneffect.cpp | 4 ++-- src/effects/native/autopaneffect.h | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index e321cde24a8..efa957a107a 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -82,13 +82,13 @@ AutoPanEffect::AutoPanEffect(EngineEffect* pEffect, const EffectManifest& manife AutoPanEffect::~AutoPanEffect() { } -void AutoPanEffect::processGroup(const QString& group, PanGroupState* pGroupState, +void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* pGroupState, const CSAMPLE* pInput, CSAMPLE* pOutput, const unsigned int numSamples, const unsigned int sampleRate, const EffectProcessor::EnableState enableState, const GroupFeatureState& groupFeatures) { - Q_UNUSED(group); + Q_UNUSED(handle); Q_UNUSED(groupFeatures); Q_UNUSED(sampleRate); diff --git a/src/effects/native/autopaneffect.h b/src/effects/native/autopaneffect.h index e13e6d0eb3d..a9db0612647 100644 --- a/src/effects/native/autopaneffect.h +++ b/src/effects/native/autopaneffect.h @@ -76,8 +76,7 @@ struct PanGroupState { CSAMPLE* m_pDelayBuf; }; - -class AutoPanEffect : public GroupEffectProcessor { +class AutoPanEffect : public PerChannelEffectProcessor { public: AutoPanEffect(EngineEffect* pEffect, const EffectManifest& manifest); virtual ~AutoPanEffect(); @@ -86,7 +85,7 @@ class AutoPanEffect : public GroupEffectProcessor { static EffectManifest getManifest(); // See effectprocessor.h - void processGroup(const QString& group, + void processChannel(const ChannelHandle& handle, PanGroupState* pState, const CSAMPLE* pInput, CSAMPLE* pOutput, const unsigned int numSamples, From 3cd2ba6b05b20e36d86fe281116410d80a4ce5ce Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 1 Mar 2015 05:48:32 +0100 Subject: [PATCH 218/481] Renaming Curve parameter as Smoothing parameter as in SoundToys PanMan plug-in --- src/effects/native/autopaneffect.cpp | 26 ++++++++++++-------------- src/effects/native/autopaneffect.h | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index efa957a107a..c9c152db2ca 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -24,20 +24,18 @@ EffectManifest AutoPanEffect::getManifest() { manifest.setDescription(QObject::tr( "Bounce the sound from a channel to another, fastly or softly")); - // TODO(jclaveau) : this doesn't look like a good name but doesn't exist on - // my mixer. Any suggestion? // This parameter controls the easing of the sound from a side to another. - EffectManifestParameter* strength = manifest.addParameter(); - strength->setId("curve"); - strength->setName(QObject::tr("Curve")); - strength->setDescription( + EffectManifestParameter* smoothing = manifest.addParameter(); + smoothing->setId("smoothing"); + smoothing->setName(QObject::tr("Smoothing")); + smoothing->setDescription( QObject::tr("How fast the signal goes from a channel to an other")); - strength->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); - strength->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); - strength->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - strength->setMinimum(0.0); - strength->setMaximum(0.5); // there are two steps per period so max is half - strength->setDefault(0.25); + smoothing->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + smoothing->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + smoothing->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + smoothing->setMinimum(0.0); + smoothing->setMaximum(0.5); // there are two steps per period so max is half + smoothing->setDefault(0.25); // On my mixer, the period is defined as a multiple of the BPM // I wonder if I should implement it as the bouncing is really nice @@ -72,7 +70,7 @@ EffectManifest AutoPanEffect::getManifest() { AutoPanEffect::AutoPanEffect(EngineEffect* pEffect, const EffectManifest& manifest) : - m_pCurveParameter(pEffect->getParameterById("curve")), + m_pSmoothingParameter(pEffect->getParameterById("smoothing")), m_pPeriodParameter(pEffect->getParameterById("period")), m_pDelayParameter(pEffect->getParameterById("delay")) { @@ -108,7 +106,7 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p period *= 2048.0f; } - CSAMPLE stepFrac = m_pCurveParameter->value(); + CSAMPLE stepFrac = m_pSmoothingParameter->value(); if (gs.time > period || enableState == EffectProcessor::ENABLING) { gs.time = 0; diff --git a/src/effects/native/autopaneffect.h b/src/effects/native/autopaneffect.h index a9db0612647..2df08fbe40c 100644 --- a/src/effects/native/autopaneffect.h +++ b/src/effects/native/autopaneffect.h @@ -99,7 +99,7 @@ class AutoPanEffect : public PerChannelEffectProcessor { return getId(); } - EngineEffectParameter* m_pCurveParameter; + EngineEffectParameter* m_pSmoothingParameter; EngineEffectParameter* m_pPeriodParameter; EngineEffectParameter* m_pDelayParameter; From c82e322f571ff22025a039719b1a5e52508bbcb4 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 1 Mar 2015 18:11:38 +0100 Subject: [PATCH 219/481] delay + period has now seconds as unit if beat_length is unavailable + cleaning --- src/effects/native/autopaneffect.cpp | 91 +++++++++------------------- src/effects/native/autopaneffect.h | 1 + 2 files changed, 31 insertions(+), 61 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index c9c152db2ca..16520c78876 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -35,11 +35,9 @@ EffectManifest AutoPanEffect::getManifest() { smoothing->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); smoothing->setMinimum(0.0); smoothing->setMaximum(0.5); // there are two steps per period so max is half - smoothing->setDefault(0.25); + smoothing->setDefault(0.0); - // On my mixer, the period is defined as a multiple of the BPM - // I wonder if I should implement it as the bouncing is really nice - // when it is synced. + // Period EffectManifestParameter* period = manifest.addParameter(); period->setId("period"); period->setName(QObject::tr("Period")); @@ -47,13 +45,11 @@ EffectManifest AutoPanEffect::getManifest() { period->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); period->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); period->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - period->setMinimum(1.0); - period->setMaximum(500.0); - period->setDefault(220.0); + period->setMinimum(0.01); + period->setMaximum(1.0); + period->setDefault(2.0); - // On my mixer, the period is defined as a multiple of the BPM - // I wonder if I should implement it as the bouncing is really nice - // when it is synced. + // Delay : applied on the channel with gain reducing. EffectManifestParameter* delay = manifest.addParameter(); delay->setId("delay"); delay->setName(QObject::tr("delay")); @@ -62,8 +58,8 @@ EffectManifest AutoPanEffect::getManifest() { delay->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); delay->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); delay->setMinimum(0.0); - delay->setMaximum(500.0); - delay->setDefault(300.0); + delay->setMaximum(0.02); // 0.02 * sampleRate => 20ms + delay->setDefault(0.01); return manifest; } @@ -87,8 +83,6 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p const EffectProcessor::EnableState enableState, const GroupFeatureState& groupFeatures) { Q_UNUSED(handle); - Q_UNUSED(groupFeatures); - Q_UNUSED(sampleRate); PanGroupState& gs = *pGroupState; @@ -96,14 +90,14 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p return; } - CSAMPLE period = roundf(m_pPeriodParameter->value()); + CSAMPLE period = m_pPeriodParameter->value(); if (groupFeatures.has_beat_length) { // 1/8, 1/4, 1/2, 1, 2, 4, 8, 16, 32, 64 - double beats = pow(2, floor(period * 9 / 500) - 3); + double beats = pow(2, floor(period * 9 / m_pPeriodParameter->maximum()) - 3); period = groupFeatures.beat_length * beats; } else { - // 500 * 2048 as max period as number of samples - period *= 2048.0f; + // max period is 50 seconds + period *= sampleRate * 25.0; } CSAMPLE stepFrac = m_pSmoothingParameter->value(); @@ -133,19 +127,17 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p gs.frac.setRampingThreshold(positionRampingThreshold); gs.frac.ramped = false; // just for debug - // float delay = round(m_pDelayParameter->value()); - // gs.delay->setDelay(delay); - // gs.delay->process(pInput, gs.m_pDelayBuf, 2048); - - float quarter; + // prepare the delay buffer + float delay = round(m_pDelayParameter->value() * sampleRate); + gs.delay->setDelay(delay); + gs.delay->process(pInput, gs.m_pDelayBuf, numSamples); for (unsigned int i = 0; i + 1 < numSamples; i += 2) { - CSAMPLE periodFraction = (CSAMPLE(gs.time) / period); + CSAMPLE periodFraction = CSAMPLE(gs.time) / period; // current quarter in the trigonometric circle - // float quarter = floorf(periodFraction * 4.0f); - quarter = floorf(periodFraction * 4.0f); + float quarter = floorf(periodFraction * 4.0f); // part of the period fraction being a step (not in the slope) CSAMPLE stepsFractionPart = floorf((quarter+1.0f)/2.0f) * stepFrac; @@ -162,57 +154,34 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p angleFraction = (periodFraction - stepsFractionPart) * a; } - // transform the angleFraction into a sinusoid (but between 0 and 1) + // transforms the angleFraction into a sinusoid (but between 0 and 1) gs.frac.setWithRampingApplied( (sin(M_PI * 2.0f * angleFraction) + 1.0f) / 2.0f); - if (Experiment::isExperiment()) { - // delay on the reducing channel - - if ( quarter == 0.0 || quarter == 3.0 ){ - // channel 1 increasing - pOutput[i] = pInput[i] * gs.frac * lawCoef; - pOutput[i+1] = (gs.m_pDelayBuf[i+1] + pInput[i+1]) / 2 - * (1.0f - gs.frac) * lawCoef; - // pOutput[i+1] = gs.m_pDelayBuf[i+1] * (1.0f - gs.frac) * lawCoef; - - } else { - // channel 2 increasing - // pOutput[i] = pInput[i] * gs.frac * lawCoef; - pOutput[i] = (gs.m_pDelayBuf[i] + pInput[i]) / 2 - * gs.frac * lawCoef; - pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; - } + // delay on the reducing channel + // todo (jclaveau) : this produces clics, especially when the period + // is short. + if (quarter == 0.0 || quarter == 3.0){ + // channel 1 increasing (left) + pOutput[i] = pInput[i] * gs.frac * lawCoef; + pOutput[i+1] = gs.m_pDelayBuf[i+1] * (1.0f - gs.frac) * lawCoef; - } else if (Experiment::isBase()) { - // delay on both sides - pOutput[i] = (gs.m_pDelayBuf[i] + pInput[i]) / 2 * gs.frac * lawCoef; - pOutput[i+1] = (gs.m_pDelayBuf[i+1] + pInput[i+1]) / 2 * (1.0f - gs.frac) * lawCoef; - } else { - // no delay - pOutput[i] = pInput[i] * gs.frac * lawCoef; + // channel 2 increasing (right) + pOutput[i] = gs.m_pDelayBuf[i] * gs.frac * lawCoef; pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; } gs.time++; } - /** - * todo - * apply delay with ramping - * - */ - float delay = round(m_pDelayParameter->value()); - gs.delay->setDelay(delay); - gs.delay->process(pInput, gs.m_pDelayBuf, 2048); - /**/ qDebug() // << "| ramped :" << gs.frac.ramped - << "| quarter :" << quarter + << "| quarter :" << floorf(CSAMPLE(gs.time) / period * 4.0f) << "| delay :" << delay // << "| beat_length :" << groupFeatures.beat_length + << "| beats :" << pow(2, floor(m_pPeriodParameter->value() * 9 / m_pPeriodParameter->maximum()) - 3) // << "| period :" << period << "| frac :" << gs.frac << "| time :" << gs.time diff --git a/src/effects/native/autopaneffect.h b/src/effects/native/autopaneffect.h index 2df08fbe40c..91441116aa2 100644 --- a/src/effects/native/autopaneffect.h +++ b/src/effects/native/autopaneffect.h @@ -60,6 +60,7 @@ class RampedSample { }; static const int panMaxDelay = 3300; // allows a 30 Hz filter at 97346; +// static const int panMaxDelay = 50000; // high for debug; struct PanGroupState { PanGroupState() { From cfc8f4390180c2783857378f37d07b5bc69eb59b Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Mon, 2 Mar 2015 18:28:22 +0100 Subject: [PATCH 220/481] cleaning unrelated skin files --- .../knobs/knob_rotary_s0.png | Bin 393 -> 0 bytes .../knobs/knob_rotary_s1.png | Bin 418 -> 0 bytes .../knobs/knob_rotary_s10.png | Bin 457 -> 0 bytes .../knobs/knob_rotary_s11.png | Bin 448 -> 0 bytes .../knobs/knob_rotary_s12.png | Bin 455 -> 0 bytes .../knobs/knob_rotary_s13.png | Bin 445 -> 0 bytes .../knobs/knob_rotary_s14.png | Bin 453 -> 0 bytes .../knobs/knob_rotary_s15.png | Bin 435 -> 0 bytes .../knobs/knob_rotary_s16.png | Bin 420 -> 0 bytes .../knobs/knob_rotary_s17.png | Bin 423 -> 0 bytes .../knobs/knob_rotary_s18.png | Bin 416 -> 0 bytes .../knobs/knob_rotary_s19.png | Bin 410 -> 0 bytes .../knobs/knob_rotary_s2.png | Bin 454 -> 0 bytes .../knobs/knob_rotary_s20.png | Bin 410 -> 0 bytes .../knobs/knob_rotary_s21.png | Bin 396 -> 0 bytes .../knobs/knob_rotary_s22.png | Bin 403 -> 0 bytes .../knobs/knob_rotary_s23.png | Bin 390 -> 0 bytes .../knobs/knob_rotary_s24.png | Bin 399 -> 0 bytes .../knobs/knob_rotary_s25.png | Bin 390 -> 0 bytes .../knobs/knob_rotary_s26.png | Bin 395 -> 0 bytes .../knobs/knob_rotary_s27.png | Bin 382 -> 0 bytes .../knobs/knob_rotary_s28.png | Bin 377 -> 0 bytes .../knobs/knob_rotary_s29.png | Bin 367 -> 0 bytes .../knobs/knob_rotary_s3.png | Bin 457 -> 0 bytes .../knobs/knob_rotary_s30.png | Bin 362 -> 0 bytes .../knobs/knob_rotary_s31.png | Bin 328 -> 0 bytes .../knobs/knob_rotary_s32.png | Bin 344 -> 0 bytes .../knobs/knob_rotary_s33.png | Bin 369 -> 0 bytes .../knobs/knob_rotary_s34.png | Bin 378 -> 0 bytes .../knobs/knob_rotary_s35.png | Bin 384 -> 0 bytes .../knobs/knob_rotary_s36.png | Bin 401 -> 0 bytes .../knobs/knob_rotary_s37.png | Bin 409 -> 0 bytes .../knobs/knob_rotary_s38.png | Bin 403 -> 0 bytes .../knobs/knob_rotary_s39.png | Bin 409 -> 0 bytes .../knobs/knob_rotary_s4.png | Bin 451 -> 0 bytes .../knobs/knob_rotary_s40.png | Bin 433 -> 0 bytes .../knobs/knob_rotary_s41.png | Bin 425 -> 0 bytes .../knobs/knob_rotary_s42.png | Bin 432 -> 0 bytes .../knobs/knob_rotary_s43.png | Bin 428 -> 0 bytes .../knobs/knob_rotary_s44.png | Bin 442 -> 0 bytes .../knobs/knob_rotary_s45.png | Bin 422 -> 0 bytes .../knobs/knob_rotary_s46.png | Bin 416 -> 0 bytes .../knobs/knob_rotary_s47.png | Bin 428 -> 0 bytes .../knobs/knob_rotary_s48.png | Bin 434 -> 0 bytes .../knobs/knob_rotary_s49.png | Bin 435 -> 0 bytes .../knobs/knob_rotary_s5.png | Bin 463 -> 0 bytes .../knobs/knob_rotary_s50.png | Bin 435 -> 0 bytes .../knobs/knob_rotary_s51.png | Bin 439 -> 0 bytes .../knobs/knob_rotary_s52.png | Bin 446 -> 0 bytes .../knobs/knob_rotary_s53.png | Bin 476 -> 0 bytes .../knobs/knob_rotary_s54.png | Bin 469 -> 0 bytes .../knobs/knob_rotary_s55.png | Bin 463 -> 0 bytes .../knobs/knob_rotary_s56.png | Bin 478 -> 0 bytes .../knobs/knob_rotary_s57.png | Bin 481 -> 0 bytes .../knobs/knob_rotary_s58.png | Bin 471 -> 0 bytes .../knobs/knob_rotary_s59.png | Bin 468 -> 0 bytes .../knobs/knob_rotary_s6.png | Bin 460 -> 0 bytes .../knobs/knob_rotary_s60.png | Bin 473 -> 0 bytes .../knobs/knob_rotary_s61.png | Bin 458 -> 0 bytes .../knobs/knob_rotary_s62.png | Bin 467 -> 0 bytes .../knobs/knob_rotary_s63.png | Bin 467 -> 0 bytes .../knobs/knob_rotary_s7.png | Bin 464 -> 0 bytes .../knobs/knob_rotary_s8.png | Bin 468 -> 0 bytes .../knobs/knob_rotary_s9.png | Bin 473 -> 0 bytes .../style/style_bg_microphone.png | Bin 139 -> 0 bytes .../style/style_bg_sampler.png | Bin 228 -> 0 bytes .../style/style_bg_waveform.png | Bin 15109 -> 0 bytes .../style/style_bg_woverview.png | Bin 183 -> 0 bytes .../style/style_branch_closed.png | Bin 138 -> 0 bytes .../style/style_branch_open.png | Bin 158 -> 0 bytes .../style/style_checkbox_checked.png | Bin 298 -> 0 bytes .../style/style_checkbox_unchecked.png | Bin 117 -> 0 bytes .../style/style_handle_checked.png | Bin 92 -> 0 bytes .../style/style_handle_unchecked.png | Bin 93 -> 0 bytes 74 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s10.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s11.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s12.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s16.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s19.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s2.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s21.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s22.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s28.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s35.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s37.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s38.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s39.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s40.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s47.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s48.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s50.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s53.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s58.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s6.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s8.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s9.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_bg_woverview.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_branch_open.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png delete mode 100644 res/skins/ShadeDark1024x600-Netbook/style/style_handle_unchecked.png diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s0.png deleted file mode 100644 index 69e64c74b2596c856cce1cf20b0c649057438bca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 393 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5LZcg6$oa zWwRG!KhPrSk|4ie1_1?K1BZZ+go5&h2~!rVJ#g^Cr3VjQefaeIFPl_<3Q*YrPZ!6K zid(f4ZwfUl@VGW-+}%*RG*svR|CwqFFN;W4$ItnA#+HR?(x%s4O3f-u@9zD6Zs*M- zA<^dxWfr%XNH^EMO;1byH+ApJ|4SGbXDvG49};}rC?R8o_#Ey@jCDbZ*^%zD9Zt4E zQ8w9_J@-NEpAfya*V)lOjPWwqH8AAH5Sy!jzlsLGunlSLDbUr1k+ z(ewJ@|G1gG>m}~q6wiA3St|0B(vqo59hz6?usq%26<7Qs&@*dcF+*jVaHf@B@duz^ O7(8A5T-G@yGywnz$(h0c diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s1.png deleted file mode 100644 index f06258ae5beb286177a80d9d788350391a630c41..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 418 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(r03aiAnCd{7$WX=I0VS>z8ndXIv4J7A`Hmzy%Pf@%{pEDAIAFKi-B;1-X#Wq zN&<@OW?F@OO%DB@0<>lNy2-&nOXW&}{DK(-6m$(70zwjU3d$QMOj)q|z`+w2?mT$< z{MFkJzyDfXcLcfhl&6bhNX4zviLb?)6a-qtFYbws+oYrV-4oeuCiW|ByrWgC;>&Fu-H?;Sko0FdV@cr8DOJAItcuJSm qYwy0K_N17n-=>%wSO=MYjWuK6riO!g6>{GH>i{Z`2{mD2q0yCvCg4pIM)*sS{~lI!AIzkZq^` zvo#mg-zfLiiRHhUyt+~7&Gl!xZ5j!WuKivv#o0OWR%N~X;>pF{rGMI!Sj@H?o)S3G zSLLy#=V0H-Cm&+=UYM+HTzKET`q{;{!nXO>w|(o0|FwF{UXgn|1p!GQayuO2vJ=F1+~^M^JV+*CJV)iN5_w4z8CT91GZiZ>S;Tum$9%$(E zXI<|esl9rQ(#3_7tIBUbxV>b-+7_m@4>H3TxMXYE7VMtD{Z~V9+RHN^E;xKhJm>an zwxZFc^CvZb$7C)E`Nm!O+OW&SaJtPb520kM%KtMKO{(;MZaL*b+_%|_xEfz%96OT0 z^LEBR`FV3>jJ1_FN-T2v+|)bykBU1o;84`Ku4V|BAcR7Xnq^_jGX#skoIp>E)zD20YEaUe1lW zu|a*CWKwyb{FN8(jQr5Q?3GuqE&tCb f?;{`m$%b+B83~)5iJ87YuQGVL`njxgN@xNAC4;tF diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s13.png deleted file mode 100644 index e0fb3fa1df5cb35363e78da6d707504baf142afc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFA-p zPMZUO49f|wK+^kiB#?C88sM@s5XjI@F!wwg4rI9Q4F-}x91tE%{xxM(aZ_0|-Xk$k(` z`rzlVc_+2_**4Wa{I|v7ZP5j_&I~qxTV@5G50=-$mEC^^r{(k~@%ERn$Uj@Nw*R$K z`roFx+s=Ddoyy%mWBa?sEuF1TPdZGSaq8K}9Xx?6cO6;sRO6J&*1v5loD{5f*%eAO z%YOd&Q1yo4J|kiN*ph^}Et%Kq?+LsWjsLaU)?T%4S$@c0{>rPzxCLqr{^nf#%=R`{ WeC3rJyDtMh$>8bg=d#Wzp$P!Xq_&0t diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s14.png deleted file mode 100644 index fee0a6942c02e0dfcb4afbcc0de57d1f215ffb96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDgXcfmy}llBFByX zPMZUO49f|wK+^kiB#?C88sM@sP%qmG$o4!N4kTUo1_MbTayuLXVR)VciF;oGsrSAX z<^M3&?_La$?Rz&S=v`v)rz9ZTZi%7Xr6kBNn1Mk+L0i|r z+QB0rFeE%7r=YxH!juIE4qmwQ;K}n>AAbM-`)u0DrZ92V@5NU&R@+LP_|*9Qtw{OwW;tbNLz{L9hs1{Sf?LElE!0mvbZy6y4*~8U z7M`DbrE~WQ&%FxE&um^guW07^PnYbvObQG4&dpHp^vO6D!y98JOL_Bu1^v}uSeDq$ eurrH(B787P!t_G&RY{;P89ZJ6T-G@yGywpoK(_q= diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s15.png deleted file mode 100644 index a9f30d663eca38ecd1cbebda0ffc176294911d24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBB#v( zK+yf=Ro4#S3rvWAIAFKivco% z-X#WqN&+&RwgmV;jtlvk9Qr*4Xi;*_jd?(OO?z%BLRU(g~`_Q2cQrUA^>>s!`DSlhMz1Ui8{| z#yutey<)8L>P|zu8y-T*UNc_nu9_))>fb`1#R>{v9D&L@YiNT+efNYH* z_TdhhDi32|Cm zzT3wCiR#+EuiN+)88ZGjxKG&D$n^AXi&=QnzaZ0;QnN*055GEo<=HxuTdM0#K5t=Z zxmVNodGe&@niT=tQs-2rsC|x1SzUX{vWWLVsm=Yvfl5;ylqWH4Pwf#>dv-^_Tevuq z@v8Z%hN&X|0}>m*ny)gQARFs5N48n^^!;}6_07L4Q`O!*&p5y5V4G51oc)dLeP5z) szYJD+-H_uXd7*IUg7ZF;b3fQHuFw~rf6D!$4A657p00i_>zopr033Izr2qf` diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s17.png deleted file mode 100644 index 7280d9a0b883d3ba32398313f081f96398d9daae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 423 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1k5?Pcv{te1g0&F%#8Bh`M*17cGJSLe;+nn zFE&$i5jov|rq-y9Ua6+X$`kSwF*`j=;}Zr!>KL5(+tN;K2 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s18.png deleted file mode 100644 index 07ea2fd98c27f7e87c1d80370cdb2d9a0de87896..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 z&RYXqb_N0&cFTM`&xXUr-42I9*q-M=g5Fmmfu#S#SigHQKt{l`_@H-*Kr;AKQt-zl zporZP?~t#_q2E)0wydy!9RRdct|Z7Wm_a~6*T5klFeD){r=VfNlm!P4Ubyt&`Ku2f ze*gLVZ|@t~lR%XxJY5_^DsGidd@I$Yz|+cjaFK|q(c5gv@AVUNC(Ai~{v-LJich+0 zN$h2b#Yen;Cvqt=IJ_^4;%Q{nQ5Uwfk+8{+J%8r4zU97;HCEzVrioq*JZkcZheyKd z)7hEgD(WAVyVophJ#=lIlJHNF>(}?5n0Vy3OqkBph!ux^8uXvt`Do!o{-VMuf~pSN z-DDj!--vAzd2#(jvci3h=`Kamx6gilvGW^6JBf-+%vw z2Z`kY6&~|+aSW-rRXXt{Q`;jNdpBnRLBuX=m2(w1lEH`Saea14mMo|%=n zxMs$*RL3Ltj+;n|33lz_OsN+x*uQP>hI4yQ<}LgAa>j=nQ+(WidnKR$z-c(I_SU=( znFrpi8@t%Wd9 z{tsjQ?!^Gv0ng$Cp2P*cOZ0yd7yKzH_+t`KP|->U$O!qG9Qr*4X!~3?g&jc0sFnoz z1v3aJ=o(l%1O$d8B<2*9H%yqaV9V|U2hU!(^x(GBm`k*y=PUNdEDYqY0PxfqbNqF&ATB~?*mkYtABPsnFl zt?4CrV)K(39P13@=c$_;PxgJ3-0^J=d+Y(e$M>bbx>sIbsJhUW>+QU)MLIOA}2JSh{gT;r`kuQ7PK`njxgN@xNAsM)%> diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s20.png deleted file mode 100644 index 571a07dcc19ef12fa48991fbd09bea058ee1416c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDU$LkK;*PJ0Kx#0 zE;|E(q}?(f&$HohLAS#pK(^O~2q5Ws4kYeuIms>LYjWuK6re5h)MO3*P+O z2TN7;OJ-ev;{Qlpl)-s2Peq)I(5FoXRx_1eGFbP24(N8xIuIy-I`h!7XY7{>ri!s% zSQp4GvgnqRsAyUC6~LhTfK1e8lgnMY}*pMnQB` zPoki1k4zo@S`Z4O@{A^xJ~tb z+Yb56`7dtv@rlh`v7T#nt4t&B)w-813(rkw-(SRHxN^k;foRQ(3Vzupie^svioR`W zy*!eaIs=3jWaq^Hdq4SR;hBpk>I#!~$-Eb^SM{@6j(#EY7|wb-(Ok ZbhssS+NE^U8=!9(JYD@<);T3K0RRxKnj-)J diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s23.png deleted file mode 100644 index f50be60339603fb705844bb266d502373cda6845..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILm*s_6JZdB-@OtW1C6yP z>zo9%MY1HwFPMQrKtb2QAs{57pkcz41qV)Cc<}1Ohu?qyP2kiQ0xCM>>EaktajSOX zZJ}lb9v5aYQCD3L{oAMh|4&m_h}v2fKcnD=FN4sO(DK+wv0}xR6D#DF3otRH8~J+l zDRKq6T~hkm@t{&ZCM7I2=tnrm<*!12d4uiSy}0+}^8{tym~Ir!Fl*lu#w~_@(wE)d zXkU8uM9g!#pY?);bEOU}-IK)qDeWXXUy*~SsDaEyhL9!aCp>egcA3V!bMYPFlgV@V z|NlQV=SF_u{P$UPw(BG9Sl@cx`Woy}>9E#=u}*Vo>g6o&dHntS0%uBn5~YE@VeoYI Kb6Mw<&;$VcSC4f7 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s24.png deleted file mode 100644 index bdaa5a414364a2d8b1c670c67710f64465d40fb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 399 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx803t!Rj#_mxQ3eZiVhW@V-k>Ix5PW-YjWuK z6rjlmvmO+IY%U4%3ua&tP|!7S2nb0iXqYf%!PWy8E?v3(;K_&I|5l{g2?JFf_jGX# zskl`;@vT&o0#7S@S6hJR(kn*q{u@ar+b&Cc&VT#f+YhW>jV}sp%zZ57*3B@pJP6jh(!IE zw6rOC_Z~x?DbH7J;;ZWX60j@tWx~?FTix+nm4b41Bx-KUTHyL-`wI7*gYOh)^Bxvs zHF_fZbykMmv3C#u$IX=W_b&N$=j#8eIQ#mp*`2fPS$$kgjEy=b2B{VssU+JhzSrU} V*u0Iw;yBPt44$rjF6*2UngElko+khR diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s25.png deleted file mode 100644 index 393a509891b5db1e753f9ce029442e9583bad86f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 390 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#!gXBd2W0r(i2;&+_dsOOyTss+NkE38m5$vK?~t#_q2E)0#?A__ ziU-;vQ4-`A%)lU^VBp{p5Ry>PFk#Aq1E(+CdGPAP?|&1k@;3n09QJf^45_$PJMkt{ zlY)S2yRP|}jy1O*{r{hKZiV2ix#ALh5Jw0NK2xiW_#bP0l+XkK1~!@> diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s26.png deleted file mode 100644 index 6a7e0e737b97f5bd8a28e84d055a2ee3e95017f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 395 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsi**-5ZBNjDgXcfmy}llBB#v( z5C)KR*%=6D0~v0ILx7~$g$RhC_mxN>!~0qkkOZm>dY1@fC|c>b9SRBgnjHE)1!!#3 zUyD0HTO>+?{DK)61Qc`)90EcT3K}L%S+MlLg-Z{feE9nN`h{D6foe{8x;TbZ+^U`U znyE>Fr78VRMp2Z4!A3dx^#ad+TX24!CaGM!^jn!7!*AfVn67A01${{6K&R?cg_=lpi2X>OUun@qg2LO*TPynSTxy;rXUqU>iK ROa}Ui!PC{xWt~$(697?7ndkrj diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s27.png deleted file mode 100644 index a0c644070588c549167b0da9a0a8533b5248cc35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bk#+R zK(@=yK)48y;dVF#B)BUGBItQK9Kr~Cmk4AiTIskQ3JLj|3^dK;i-|VSaIumgzhDLi z0R;mGkARSbf`$oG7VJ52;m(5(zZs8RJ_%HD%+tj&q~ccX#J5sS3Oubp&j^@sMxXo} zpK-gR{lc3+jUV61v~y@K)!5}XdF!n0lR_r$%ePEt@MzRfR?|A5Eu6%9bi%|=hVyf_ zAD=YCH8{yIXX%35tzM==v4&GtM>u;)o(T8qk5=FJ+c*R!Dz{*4-UF zNy`Oqd(V64XuIXkh3>^`Bp9YCM=`KHJU3zSfeIHv=AYeJ{vy4Fc7N_`7eBhQc;`LS zKfm@awC&12vfW)Ujn#lxvO(1Q=f{aFZ=O(oDjmL$-|isChFd@%F?hQAxvX0H|LeGegC_U@y&D2 zFsncCcSd1iriRiBqw|kfY?KYS+rigg8avaHb)kcTmcf1&M@L4cnGsy)zt+YJMVK5~ zAuw0`vD|fUya6FH&ue}-dYmzQ&|Ggv5bB`>4Ju9R?WYzN1k22Jc&&is1 zZ*i1N;R5kRZnI`gSB{#YzA62LSAxx=2&RwjTh+O8=a~Kfe#&Fd*B5OzH|lKiGn_y6 t{xB0Unt9VCWya=%QyvNjZ|+Q&u=CO2Yc9~^bO(Bf!PC{xWt~$(6990XkDCAh diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s29.png deleted file mode 100644 index 5b3f01b10d6a92616515403b8c8e20656f58c42b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDUSf35ZBNjDgXcfmy}n5Aj=7^ za5kLbb~psihA@KOB?1|COT2w=MTdM%1{%WiHtZqLOyQCszhDLi0R;nVhk%5Fh6z&^ z9Juh{!|#gbm)n5Shdo^!Ln?07PP{GDtia=PcqzxCrlnW^|G%u}E!)+1SKL5-HBNhZCg0uY2d9ko&HOw irij)gWtn}{_{!KT%xh&GG7ab(1_n=8KbLh*2~7ZYA&s^G diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s3.png deleted file mode 100644 index 72782ab6a738a7b190593ec722609d79c696e74d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 457 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspXHth)a z^3LL93p*ax96ovKEYW)&i{$>7Te6FEcV2VWQ1?%Do}n;b^S#Hp4~qpN7galdYM6P# zed3Ln^UpgTRx|ClIL|BARcO~>`;685$*UtG&l{&2Tx{jE7c z1-BiX-V!Li>u1gGN~xWt{an^LB{Ts5)H1yl diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s30.png deleted file mode 100644 index c48b7e5e7dc4f8b198cbe10b6af25d107706aac2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 362 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDc1m>5ZBNjDgXcfmy}n*h1?E@ zph>yx3=Ddg2owx_oe=sx1!#iLvzPA^4Aof*aSJutzV>KI zzv`?|(YU^2-=F*Hs~;OJO0CYetdGmR)i$YKCEt0XV!6_P=Xon$Uu&D0xIE_T^B)YX XRXn!~^#2qCJ;UJX>gTe~DWM4f*;a>W diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s31.png deleted file mode 100644 index 4d50a6d306da9c27d3f3b145dd357bec922f273d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 328 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDdPa25ZBNjDgXcfmy}l_9(~J< zQ3h%kC<*cl22u(J4gm=T4HFg|xbWb^-p?K;K)EfRE{-7;w`wQe7HU!8ao)}PL`mWQ z|E;&pPHR}@#Z8!}*08j{D@3nDX6Bk7%Q8nh6Yir|_A8gKRH<;gTe~DWM4f*{yOt diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s32.png deleted file mode 100644 index 95f9935432605e11cbe875e8adb6b63bfbbf771a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDW?FR5ZBNjDU$LkK&0wp2w|X+ zZihqAM0{^W2fa%K3I=~n0&2Hn+?5P8O}He;FPK3v!*k zgA0Jtn><|{Ln?07PQ1<6tRUc0u6e?h_14P&|5u;gTVG@M*`}c_P$g;_N zyc6eL-1wnm$BTDNELN%vuC>p4N-mykGh9`)h2eKkS%PAH>mH8JJ)1aq8SI4F64vq8 z|395{$8C%0{AmdK II;Vst02hgV;s5{u diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s33.png deleted file mode 100644 index 8df606d5406dd440388088dd23566d4dd5207a7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 369 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHDZc=p5ZBNjDgXcfmy}mAuC)h} zKxEPD0%0JNZihpVxj=T%yF?)Av?aj*aa_pPAA8D0bsL~h7(8A5T-G@yGywp!2ah)Z diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s34.png deleted file mode 100644 index a3a4fe1cb26877adc174763146d154416c4a68f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHslWiA5ZBNjDgXcfmy}n5Bd-e) zK(@=yK)48y;dVF#NJ6CG;z92cgFht!xt5dMd~ZdEd`$+LmY3eG0yJE#B*-tAfk8mQ zz`->jB%z>T!juI|4_tWg;rGAu{f!HNDh_$NIEGZ*s-1XS=#T=B%i*(L2cx>&p8mHF zaWZP&68yb&!sOTt#-$D^b9Ucf7qePRAn8$C$I>b3CQgzsJ5-$VkF1as+O)DUx3=cg zYO#||CsJOW&;4c;Tre$Q?uK@cibuICmbT0Pyv3p0XJgP&({|mr-y`29cDiKQtk)LL zXUa}owzx4+=4pacOn-*!oCEU}{Tp9)2(e!3tnxRBef;OsfBTt2(*<>P`*V-qm;SxD u#m}i;WP{|zvJ^q%+@tDh8%_IvF?~J7&vD_<#=k({FnGH9xvXV@SoV+KCsXniP0i*;yo; zq};SC|9|iC{mF6m9Pftu=N2q24Zf#lB<}baHi0Xs&h5yQOP_Y+h|KWlUo+*ao4%yc zrN)Ccw$rD?bvTMIQMy|FZtp!!3$28cZ!{HEZPvI?-!8TFVdQ&MBb@ E0K%b_PXGV_ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s36.png deleted file mode 100644 index b04d702333736cb3437d686ca4746b27b9d545a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 401 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsn`IY5ZBNjDgXcfmy}mgFxLi> zPMZUOBm_Bc4RF~R2I9A;hS2&Dx6p$8+@0O|UBaci`Rj=M7B9Bm#B{ted_$hnG9;QIgw{ zpi|H5f-ID!14AO-XRtB}&u4k^@u%XKLr)wPcXa-qdqyswUrUW$>9ibQpsa;W=FQ97 zTIIK1dGR#wO=Ihg=Ot&$I&-dNMp;FyTwHjzID5(21I*r;Aq{MWcNPh5Y%Js7Dl=o7 zM&N=u8P|_|GX19i|NCUs!iYtVpRR2CyLY3VSG;GuJnNs97uugXgr}x`+qm-9k>q_J XIR)QdJ12hz=qUzIS3j3^P6P0|$?Qz>tK3h6z&^95{dB(t}qYe*a_2D%cHFb%sX*l%fx3V zFSQ-!vI{9(c=6^DX5-HDw{Jd{NzC8=b4B99S?i6l(GWrJE0G?j!gW(Efn4uvQ9v^2U1IRZBp}0SOF+oi zWT45PdITkb7D<-``2{mD2q>uN8aM=mBos7En6hB&feV*z-+l1p!|#7(71+)w}dZ@#-=xg_l~`;C6du<1P=3gIHtKXXPmziQu6 zmhw#Ev5h7x(?(rq!C8C_4s{F?zmE&Qc{g>UfcTcUXd&74#WjTmERkQgFf^E3^1Zxq zV%sA9(4dS9v#T-`b5AWT-Qt~cyd-+stguy2t4q_qvpjO$ttOWH#_a$1$rEqr2DaPWsQYuzjDP9LB`2K~fu!5v5D44jL>PqOcP|FW2zVAB^e!>@Q&RB9Bp}!KRwEp_X^Q;|k3ZwzbpPCrT_GJDmr`$*s@sUI zsO!wLHEtKUm%PM5VO6Sz!(t{DBfbOt+u3jYoiU-scjG+`Az8im0`n%TdARy>GgQdqF$) g@;;k$zK9%UUo9Y%Ait>KE6`&Mp00i_>zopr0Dt$LM*si- diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s4.png deleted file mode 100644 index 8cc83bcf3f0e90228dd17a9fbbb109f0e3026ef0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 451 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsp$aOmu67n@U^m_`>-s!z7*n!SaDGBlmW?&Fd z&^54j2nb0?%qeJ?FlE8g0|!r>y>RK)gC|d)zxwdu_uqdaSW-rRXg#v zP?G_Vi?imU3eB#rMNJ7u{{0u9wN6$EC@Kw_9!r&HAZe?`qxR{-+E47aP3^!zL4E#WSK7QxU>ATTAIJT$|JKG zXH!?SpWb<@d(PtDUMDNv)@?f9$+jkSzyJN56;3A0Hu3jPI5vat+O_1VhUZ^eEENjB z(G#lID&frEdq`sP;s3Fcz0ddETI8?scJkzh&rNq+4zGFHclP`8fDl9Xcu~XfyNgc= ezDhd5l7Cu2GW7FDh7CaPF?hQAxvX~Up-Y?h0P$<&`)ryEY%d%nzD zSLmwV-QTH(90!>=S0;pZOj;4WsBeWx7O$qnEL9_eBjTxEDz_r?|NWo%@W!zfyy;h8 zz7IX(ny7Ks$AD9gtIbB>sqv}KwmbJ_uPn^aGZy-&|36K1V$Qzv-+`WE@O1TaS?83{ F1OVpOth@jK diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s41.png deleted file mode 100644 index 61674b87aa15b34a956ebfa0d1567a3891a5b618..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 425 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsmuVM5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4ac-5U&HxE&5rw9Yq~E<5AQ|*7G5Awb@W&(| zTO-IA$O!qG9Qr*4XiH?rt?NKbWlMtmf*BYD6m$(70s=!43d$QMOqsFZz`+w2?ml?+ z;rHKv^SPhT2P(Ye>EaktajSOHL$M|Wo>orQVByP3Tcc*a|3Bev-($m%H~uQ_$luMp z+{%H=Yt7o%G24WuvOij{ zr_EP3-m&l)r@#&4U9H!gTe~DWM4fA=IY0 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s42.png deleted file mode 100644 index 1fbb754f79f21f9e53bcedfbb1fd1c1af49e9851..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 432 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsr&$+5ZBNjDgXcfmy}nrnBWQ| zoi+ymNeFV;83<(9E%Wg_9R?S4I~)RJdtHbClAhrLY=Mmf!&Hla<|?# z-f=s{c;03f`!AE=JPFUhmAVbD#6u4p+N3$faKk@WQ|6zp=4yS%XW0F@udQ8Z?GpX; zqS?Q_k}1D*ZaThx_v_2geb&oOwq`njxg HN@xNAi6*QB diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s43.png deleted file mode 100644 index ebe7a5011267837056fa5e2a354e8a5d88504a9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHshj|x5ZBNjDgXcfmy}llBHIOC zj_drKHU~hsK+@P^ZpeiZ#PwgOc8WX`Cf|Sqo;_O1@pDR&e#N=Qj(^bZr-i@H$%sq-`sx4tHL~;195HfH{hTqo zHlJzMKHrmSN^@6i&0m(uXspP*CFWOJSLUY5^V0WziRPa?w_6tIHwI5vKbLh*2~7Z> C1E`b$ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s44.png deleted file mode 100644 index f91a7a1452e06f180d3cd41dbb9852d3813b274f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDgXcfmy}llBFByX zPMZUO46{yWh=|M1Kp?|znUCk$a3Dj$TpJ?pb~psW@H__+_r4MdB>f-8`rV5GG6J5( z2fa%S{*)B_F$u_3w9;`q6cX|^IrMu9(7KPeez^c`S11Yc3ua&tP|(&juy6u|dURD0di#WAGfR_VmsLQMudF3Ex{flInpJW%cW z^Iw0K?+nMTcix$PWw(W*99Ud4cimrqvgHwjQhnO9eeZW4(+~)I9(i^3tQ!JTu5g{Z z`gY-)eTao@l@ zd*{8F_FY6IL;dYE@zxVvno>@RT1$I&&RX1Mx%iL#DZz;IfxQlMUN}7}=)FH(zhL`( z|FTQ(+RbH_y;xAWp>@A`x-qY_rhAj;m2+(~x12fIA@nS4yP3}nx7k%MnECFB**#TH RZv=Xg!PC{xWt~$(697~Tv`GK} diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s45.png deleted file mode 100644 index 8e798dee8b1186839a99ca4e929fa9b32f33c4d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 422 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk8u}5ZBNjDgXcfmy}llBB#v( z5C)KR-WuStGZ4t|JR1%qUH1kaQz>~`rV5GvV-0w27gKd zGVGRkhkQ*A{hk7}La4G*0BECZNswPK1A~Bqu7N{9U`RqvL3zW3DGLr9Ja*#3gXgb4 z{QhgU#(Nb|;U!NO$B>F!r4wI^H5u?UziwFA;TJG-LEyK4`kH6&1O|Lw9=(6^o1n;* zE>k0`EpnbLT{-jDIc5$;n_s?8QwkR_U45RS;_Ck3_Uq<1+q^ZTx5P`wK2Klz*i31$ zocn%q!dHtzzCYb8!vNK2P>EdAB4#rtMoAbZ5TjA=(cNZIShGynTOx%WA{;l`PZ)%IT1-M^j5H&df} v;UyI-;r2)#hmUTo1_jdXlD;S1`fV5^N<`eAsInXb`i{ZV)z4*}Q$iB}nS-Zt diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s46.png deleted file mode 100644 index 36daa7aaabe5fe6b9258a9cee059be4fddb0384b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 416 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspJ5k5ZBNjDgXcfmy}llBB#v( zK+>$!8Nzkh83BP5UO$q`of~G#7wN2KVJ^nj?(_KO1i_iEsd_VJsNh?WH!))2Q z^QXF3d+&YKdBW11cV5*3?Isx$6>TqFhx>MF8=j{+e$@EtblUD^%f8wbV*acQ79IKr z_oYl)Egw7cu7JAS)`>qK1xu{^6Fes8+!9WxwZ$x4-x@Y0<+y6BJIZ=vi@h z@idL#pY~I{7u;1o-6-MQ?vlLi@I&r1WhJS1A2&|8krUW#bEE0`S)v1>2}Ev(Lm&*#b0Bf=D@?3he85hCxm=W4*i}2wCLMrj#WT=?yJD|#oo-U3d6}L(!UKDFm5O4|CF*X%8HCe0s`2YWnu@0VF zDjy!OH9o`SB4}|mGwQnfjHl;Q`#YjuXPyZ6@G!aLc34nM^;E;YTAw!C_1-s5JYULi zX=n1UojXjvc`)T!Z#bV8=EC*kV(*Q+M>evDCCPuEQqmu>__2#==F&*yd5Fa9u!A@J{aj8qd4m+{O0RD){AGTQM_z z>ch`s9%}zJr>g%EJ{3E$r=!!I^U;au=gE_|N1hkw&=qY`|M$lQ=s5;YS3j3^P6AURzSAr*>E7~x;Gd|0+HL{5D3Hb97x>z3P`>8wJ86Gv3~br zfb5`miT;mbgFht!*=~nIyv|32d`%Aho&vPaOqFpr&~}BAAirP+0R>$HYX^^jz>x5S zoPzR(2~!pvIC$aGgD1~lefa(7ZzH_ju%?AhGcA!TC2BG{ZhmQ)9VK}{eCEY z?el*phRnN0wQpmks<$vK|8i)b=4O>e58U08(xs2faU8LGxALWo;G&WRv%V~?^2~mCTk(V)3l64>JM1#^CAd K=d#Wzp$P!dSgWT1 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s49.png deleted file mode 100644 index 80991f557d771f79021d8326243ebd2cbca1977f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsiFX%5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#U`shwc%c{Uu#aNQdWB!S57a0rCqc`n@JL>Q3meFdc6X`{dY!&tw2 zF+g_EyTss6Ng-d8fo$VC2e(5ZKz8W&6rgqGrrEqe+Z9TJ{DK(-6m$)&9XtX;5>j&t z${Qw3S#aRsi3^t=Jb(V`!|%WUUY(f30aSaz)5S5Q;#TRzhfGZhJS~nVFBt`%)>*6Z z`EUG2=>V1+|5nu8%Qc^|TIKYss-ug%Qr1tI^}JH5h`*q-xH-W2RmTgH7q?$8xtaUl zRQlzfCtvp23D&+{Bb}+zu(E_a)=73^iry&CKFl;f^TdNfDO(gF_+(@V>*PCRk4=NYg3buoV~W{0UjzcF~a L`njxgN@xNA=}WPn diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s5.png deleted file mode 100644 index a99fefcc2a34e2a834cd0e3e9eaeb21d736c9c69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llBB#v( zK+yf=fXWsg!$fyaa`x;b0Z2U z=zRsG(f?tr-@OfAt7IrL%*j0t*)rn_6NE|r6kBN zn1Mk+LD#_A!6P6rBq67uykWwW1qTkEIC1*yg-Z{fzxwdu_uqd$Wea};)!*@SaSW-r zRXQXJI7!pSsU;^-DF9j)xO zhR@bSdnfHV#N~FO&m(Yl(2>XcY`ahY4Qn;s7e3SdK;-7VyHqOXXu5Pvt~($PRj!82l*-$S|&R2>F^E`aK0`omS-9nLyhWOM?7@83Yt`4XhnJ0zwiJ za|((ZCQO;TVCjK_7cO0U@Z|Zc55NEV^ocD7Dn9S&;uunKD|g~sDJKVk*7S>EUSUFl zQn!BZKbqEdOYHf1z4yiEYIH1Eqwa4~UL!I?N^tGg{ie6#v{ z$m-vGjeYO6m%aZy=a}4K2a#=WW*Y@78UA_c_eQhu4Ws{?$9B9|1MRq0#(ArsoqWJa z^sM<)R_`s7U3@Dy2&twOIOtqkvHcs@N3|-R!>!DxO|=BKZdo|Zr7-{YS@kQoznxe$ zjny;h-Qqvit0UK$KU=%Y-Bb4AbdBdif$b8OB8p{yEPvm4k~8@^^A{bl|9VqmL6O1W M>FVdQ&MBb@0GyYs`~Uy| diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s51.png deleted file mode 100644 index 4fded0ba158a603a1dd2aaf41ac08b43ac9d53b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 439 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsnP(S5ZBNjDU$LkK;*PJ07zO+ za0Qaymm`6s%g#W(Y%3tc^K3Yfbln>aB!S57aEQy!ARxnWqd$=JJQwb9A`Hmzy%Pf@ zy{|?2KaBOe2Vw`kOAP*$6!JA0$TqHX05U?qrvPpAY@MzRaz{y!UoeA!g06wJgGWF} zctTD=dBcP$a~CW-aPY*1I}e^bfA!(_-+$2x3ugjVpZ9ce45_$PI`OSolY)SY<4Ly`Kl8~&mWaaUHe4=}WlKj^z6@6bO9x>xx zXW_3Vw(CJna@-adZ9g9af0jc>oN^de36x%wbN$6XSK~sgTe~DWM4f2Z^u~ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s52.png deleted file mode 100644 index d979b1b4f9723b2b709532b99d88bcc761013dc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsfqxf5ZBNjDU$LkK;*PJKsVD0 z$grH?3K4PM8sM@s5XiP$<^vG{l5U4XTy_Ql8IBwMeQ!kr8J_3Dfu#49NDz4~%Ku@k z-@OxDNu3ZffKR&(-p zda;#Ww4Z7F?f!mj=S9<-Q-x0ir$wcRtSso; z64>!MswQcEh{dW6_os;O$T-_+_>9y4hQ6?L!y)&**W4%maaQNjsV$K&J(le;-J{`p zR^?BT^5}Is*>(zkXL6_1@6!5SA?z@D=ago}65*#dO0O7{nw1**?9%0D_T+xr!{`$t W!TbE@Q&RB9q>!)4KoPI=5fFCh_Y|NT&Teg8 z2XvBpNswPK1A~BqlCFWZgGWGMNJ36QdBcP$a~CW-aPY*1OAnqrdH(9dhu?qy{R=X> z768<8*VDx@q~ccT#G7JG20Sj#f=5^{DlOX47#;KJ|NZnJFVPE?FEjJgXPG>3RJQNB zRFY&q@44xT>((wRi_$H(ZgKNkEGQ`}V$s^lxcg6y=^LZ+{*a=1Gj=b2+kG=TOjuR! zty9zFA4~$eGmd;%YPxxGr!fDP5TnUfru!G)(x?!cx~IsVvt-M|f3Ixzcel;4XzuvD zK>eM5t6|bT_w+@oNB&>%V(PZr<+)(W!d)o^^%tI$f8>1hul#h^`t09v#R$ zYuI&?H}zq|OqYdkCTJh|(Cl=?YumMSpL1c=_m&Du`|Y~FFc;`i22WQ%mvv4FO#tny B$jATy diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s54.png deleted file mode 100644 index 38d0ee1af9d11ae541c4170c8f62b44b71e18633..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 469 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsg?kr5ZBNjDgXcfmy}llBB#v( zK+vtzPF-*Y|nEb zHQrYuLFBb4|A(=D_hNwTfM@YR?-GMQB?W&>3i+B06tP?4?f*Cq$PWFU0(8LZ9Hs3* z_o$Tw`2{mD2q@?pSUY%l281N!6jU@!m@;j_+AUiT9K3Mp!IS5&K79E7_uoI0{S$5h zb=>lFaSW-rRXXveSd#&d%VnlVO;rY_`xU@|oQka zCpEcgJ}OQ6Z#!fy&Q&f@eqLNFwQBM16U(dItup7ae%Z3z@0QK_ez!RiL2^7UpH&T9 z&$O-BR>phLeoJBFPk~s?3AZoQ8a*?sGuyxC?(F-@*}rO4yS;X6)Sk`y^}07@^(T#u wF{izs1Szc$Xy)e<(Eb(jNO4n*aa+ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s55.png deleted file mode 100644 index 8d745ea5ecc806c4c04388b0435b531e61ae3318..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 463 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsk#845ZBNjDgXcfmy}llB2^zl zr_BLChUElTAnAQM5=c644RF~R2xQnT^YJ_z4rD-(+u;xh!}DCY$B8iCJ24)o!XP64 z4`coA#Q@m>&*FpLB?f;=3jUZB@--PKVqE9o|2PiF4*i}2v|4?0uqx0cDkVXF!3+!n z3TnCr)(##4fguTrIRy<9rYtyc@WhFe7alx+_2I+szyJQ-oANIXsQ!+pi(^Q|t#+nM^yH%R6Xp2_#s3~Lxy1Lk_EBZBiTe@V)(&^!h5LJ^T@wo~Da`Y~XdhB^ zAgk*gheqlP!^nc<{oVEpe%#l6{p+L2+XbH#Pkl5|-qw3u`eVrNr7E@$eM54)7*_LC mi9}Q%3f*dF_SNj;3iHNx$=jxx6-R*nWbkzLb6Mw<&;$T0lEBme diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s56.png deleted file mode 100644 index 7e0b2eda3b9c6dbc62f64778e632420629efdd26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 478 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mg^)dvK zj_drKHU|J1mJ?ior1#}WqbhqK!+C3f%g#U`1CHDdhXC2Wx1xch=ecm78&N=p_mxQh z$8o-QVt{O)o6+9aqWtcG*a6Sty)H%sy-N)Klob3i2`FM*=MeZhA>?av==T($8)R~q z9|Sr{y(Gvln1Mk+L_t&6z}msXGaxV|p`c;Hlm%N496WpB(v>?89z1#V;lt10fB*IT z%S#7px$o)X7*cVobmDWNCI_CD;tmC~FB=+pxsMgS|6h})tQ>U5{>;a-c~X+eFO==O zrk$Lkz#QmVvpYTQ`5oi=5B|E&OPv}l`PjT?4e|ef1>^r z7b%;t#zV$6jE=>MhJQ13-z?aB(Ao50yN7n^9xJXTaeDDH)7^MeFP&6BpTDS4&25sq z?#UX?-kIUKC8c@ZAMLj|CfsskWxCjQUB!9J#k+s}`YziRy;qv9DZX{ZV%GceRROnS z@_W{Xxz7w|neK5~XyF{AH327TCN0;jH3&8PI>X%2S^Dk0IDx4^uQGVL`njxgN@xNA DL0P}$ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s57.png deleted file mode 100644 index 9943eeb5dd15a0e0278ed3a1c5fdea45a04b0d92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 481 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}llBKwuT zj_drKHU|LNmJ?ior1#}WAnCj{z-4D3kl}eY97wwE4F-}x}uceyG=pu8}IkV*2R;j zozd!Xk+PY{&}do1=cqVY^l!%bH(I+7I?EnxKhg0#BKFCLJ#+ToQC~1G=w0~po3$p7 zkC;}@ac3^vKSL}^-8n>}c+pk+5{Cpc7k`!{-qrrkG|cw>5j@==HZLqFQfPbZmTKw8 z!Z~~IcQ1{9{w?s(v>aSz&SAQgA<$lok%Gl&4V`H#n$q%4!89ZJ6 KT-G@yGywnhJfLui@9p78gAzzb2zo!5laB{0qEYLk_ zB|(0{3=9GaY8tu*)(!z7aS1sE4HKp;SbE^#*$bDhJb3c_#miS8KK%asPpRa=b)b$r zo-U3d6}L(!zT|5%5NMgcC?v$i=A+xesxN=FZI}>fc|9gboFyt=akR{09E_P>Hq)$ diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s59.png deleted file mode 100644 index b6dcbdb4327f3e255b5b9b0ba482e8b02e019e39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 468 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsm1`G5ZBNjDgXcfmy}llBKwuT zPMZTDY?B5@Aj4&6AdvJt9j0Ke4dU(!g2=iZ4uLQ{&xL!O2=l!Y<8vblDB^u3637NJ z{2#{p-2*WKp2P*cOAP*$6#Ov>$W^q`@qZi#WQTlB4*i}2wEbpx(q5orR7!&Uf*BYD z6f|@V9DD;p64EmYiW??OS#aRsi4$ipT)Ok%`Ku3~e*gW)Rh<+FRDaLY#WAGfR_Vl> z)0!LvoV&vgQ;4OiN>@=+Qgr-@S@8yKp%(GhXCCe|{T9Lb^noYmkH|yfMSeXxPkpVE z>wB8rtD5+2*Ty{jrBM^%TYcLuTk=KD@4cVYoVO$-$7I^qn9!NTG4RF~R2xP#K+u;x(+xJ#9kn}tk?t3Q&$nd@r2_$`PMtfh2 z^1BBT33wLoempejU1IR3q~MQ9KoN}~W1y~(ugRg`Q-JpFZ;-MDIzzc6$S;_IK|n!U z*TCArGaxV|p`c;Hlm%N496WL1(t`(2UVZrREaktajSIV>uF5} zJS~@%I=G_Nzo?45Tk-O@{ldu;o0(q!QG9A`>>E^g?xgq8nwifzz0)Vn5^1mQ2@EZ7 z+~WAsF35}3U&GB!ylMaEEcdWfr;ZKm|L$*J=}?#TWd?_-!THVxgBj;H|6`0i-R`wv zrl{~|5ASE2PcXmpynW*Fz6SkgH@)10&$NG=VE$%NOK0&JWm`qV>ApSB?V?uprT*oe zDpv7wF;`=b@>`+vX8X*~Pptp@Us-(nYQ0+-2lrGZrmdKI;Vst04D>&TmS$7 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s60.png deleted file mode 100644 index 9d74037d9a09ba96305d458a83fe4a5d59e4b8b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 473 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHsrCS$5ZBNjDgXcfmy}mAuC)h} zjvM`*HU|J1mJ?ioBoMjm476S74P` zuS9~#Yf=6WWBu;M0NDZ0;sc(<1-(lQ{*)B_F$pN5Xr-f@X$545d`%Aho&t2k_h~I_ zflg8@3GxeOU=Wa2(AG7ub_fWGOGqy$ZkRA-!R7-8Pn^AQ>A{oduReVE^!x9>1?RqL z0Cn8bNJ5jIaXm@8a&pZ;;r{$KyB$UGP^Vv6r-RBchZb*Kgc<`qElv8%yT#^++ zOoy2E37+3LY3HBJ)o+eiR4iH8ZS>_z(aubz|BFq(uKw(D>5}8~)t~m5Nf{env3cpC z?va}Gc3F4cxwk3z+P`s}+4Z8&;fg`-qs4CZKj%&K?mKfNR5*4fd-U(h>t1~}g_?!F zkDl+2;+nQ*HBXkvjqsf!3cnIo<=zyqKQ8{DUi#<$!sbgrZ!&ng`njxgN@xNAN*c+I diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s61.png deleted file mode 100644 index 4b8abe16349c9c41a2ef0dcf5521760daad1c1cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 458 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspEQ;90Fu}oCpIcI2R6N_}qvBvVHHw z07?IcvDULZ{O*C+0Z-zB-X#WqN&>QVGp$0tCWn4c0oohn^|l%443&}~zhDLi0R;_h zT?2=Jkc9M{g7St56Q?X#d*I-Svls3>c>dzmhflx%x*yVB15|&@)5S5Q;#TRzmqJYr zJk75canEbeh|`eMe)HRYVWxzu!)Nwo_f;n6r7jT)6>yC-6Yg?mI&=Au<@3Gky)N+^ ztz2vqdW&tPx{Z*?&%H|3M_wi02z{=tJmIePT5(>ZSDvB)%R=tY_|JJFX5SWOYhyp# zOA~)>Q9mWRtUIT7){mt5Q@@lPs`HJz;BKtuc4Bhl21m8v=bsdJRqvnr)4t@wfyV;2 z0w<)F`Rioq)K9;^_|N;PzDK9d-0^ae^z}u#ANH8+?Vq$ICHAq#Hp{e*;5J>Z`TM$D h1#1nqdg%$X>QCG&<@>TsUkB(*22WQ%mvv4FO#pHHxKIE9 diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s62.png deleted file mode 100644 index b9896bf982f8de94c18cf4b67b7137f3362dd544..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s63.png deleted file mode 100644 index b9896bf982f8de94c18cf4b67b7137f3362dd544..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 467 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)h!3-oX$H;yMQgs17A+Di6QvUz{4bU3W;uunKt9H_D zp(XF`iL;jJfr79Eo-%gz? z$=+|mBLB?itITDWZI=wcuKgbA;jO+m{)F_li!(YmKJ#>+HRI%!ow4G!d+vLD;(jYw z@n(@wqr~AYoku0^x7#QDs1Hwjw5{{qndY^>D>JL~Q$w5ov3hSzoH=ow(WI#C*r!1a oFPc|g+d7Bg!~cwIg^c|SeLrLb8GeMU0Q!`{)78&qol`;+0L`qz-~a#s diff --git a/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png b/res/skins/ShadeDark1024x600-Netbook/knobs/knob_rotary_s7.png deleted file mode 100644 index d0777a130427a4498121e2ce215c748b927ea096..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 464 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)u!3-poxYjEHspZhelFbOL>NTe`&yL$ z!&tw2F+gs>v-qHQiNT+ef@8= z$K|q82Y1I&)&P->S^xiY&uys^33%Q*r8;fak|k3X`FYMeUU*=t(B6O``#HteZk;j^ zRdKoQm*VQiEn>2tBV%@~>YAPoR-@dH{{vf&{OftCsc^ByHl2})_xF}7?h|ZlHCUsI zH?pi;ss61nrMLQ!-K8DpA9Po8NogFeanXL_+paO;h|K2;9v(fJuVaketh2?p{BI57 z+tU|#vXS%F4BpOXX8%n6+kdaU6z@Jw(yVsNM4SB^OLoLvzUjR`bA9P|jjq=(QgXkz m3C(tE+9MfdkWBYH{(<>pkq`^g8YIR z7z7kFbPcQ>JUjzJ64DDACQMndW$W$(2QOTD@Z|Zc4RFn7=T_Kedu zi?7;G$#{_4!Nqw*VzQl~@5a}U=lzqByXyUUvys)4*eS~EHx^#nA9wu*_nj~9r@sZB r2s^XQb=uBN6{$b2T)A~cm?b~pu#}lm@)LESM;Sa_{an^LB{Ts5qrgFht&e@qJbnhX?iI}{T5IswQI{hk7}U9(d65705H zB|(0{3=9Ga8oCD74jus^3F$cncfZMfB*d}75dN))bPmD z#WAGfR_Vmt;wA%umi`3IwHi`cxq*&rHqQG0pLzH0_zBbXjGw5TfAdUlBa2aHWN78Q zRX!U3w%)&!+@sjCSY1@8(05AHler5nJX88``E7qvsT#|ss)YZA6~@23FPWKUI-dX8 zx^%(v^DFHdYi>^F+OW2YIWtG$mqh2YcX=H$=RQXmp5NXZl4Q5p^i$k%jnJgl${Zz? zXWs;q&0T*B+e7Sh#_6yHR_Gz1>9OEzknF#bPgQu&X%Q~loCID}e B%s2o5 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_microphone.png deleted file mode 100644 index d39f1544686eb0e7d9646125799c7b7cec89ad22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^K0s{3!3-qDE%demDb4_&5LbIA&;S4bhs9;D<9X}= z6l5w1@(X5QD4TrN0?5<%ba4!+xb^nTMn(n!0hSH+zxmf*T=`*cN`px3q+O;}YjkE$ lOg5VNX7L(#MurbLOiN8!wJW+0odRlQ@O1TaS?83{1OOjEE8qYC diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_sampler.png deleted file mode 100644 index 374f20eec6f925ca115226be5742e8d7a98c1162..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^zkoP}gBeK9h`Vb8q}T#{LR{^gJj3F$nRvStfg&?K zT^vIyZoR#;kc&Zq$HDRYfBRe^!ERTH+j|o>$j(Z!Sd{zX#f!Nw7AI}IW?g~`zSMl# zG}ZcR#XT2yRPcXZxRkh@EHbdQy{mI}BBvIr30QcWP5$+`xx3%@ea|5vDybN?4(Lb* MPgg&ebxsLQ0LgGs3;+NC diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png b/res/skins/ShadeDark1024x600-Netbook/style/style_bg_waveform.png deleted file mode 100644 index 962f91fbccb6e784a5002aa9efa6815e7d68fae7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15109 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 diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png b/res/skins/ShadeDark1024x600-Netbook/style/style_branch_closed.png deleted file mode 100644 index 17ac682340e02bf53e85a7101cce4dc43f299b35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^96-#;!3HGxgLCzORG_DeV@SoV)(abX8w_|D4hC#y zZcdT7mN?gAo@#bWn}{rl0G+92289?P_yFV mI+GcM(vHfQ^{WzxHrCu@fjdmuxx$(dHlQtL6RZPlZ0b0o5>FVdQ I&MBb@08?%}rvLx| diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_checked.png deleted file mode 100644 index c75ef514746d6540cf0d194e0e33f9b3dbf7cdc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 298 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OG6Q@t<7|2UN4KM)l%yh>pB zlECmOk>P(T!^Z@MA4v?ql0j@B14#Z)V|W|S@HUR&dlJJJpqey>4+$XluM`mXb0Wjv zREB3U46kBAYJgHe5r`~M9H{tI%V&@aq)LMPf*CkDIVB{lta^c9{f2{w4j(>o`ufeA z_y7J)h_yZiR2Amw;uunK%lDMKP=f-4^Tmh_-@BAL?);yS7Nb!A)kt`Ug2j(N9=pED zZ=HSed2yYLW#<0NY1$LaX9Z6AF4b%@PbDjDR*Uc)I$ztaD0e0sv0+dDH*^ diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_checkbox_unchecked.png deleted file mode 100644 index bea88dec076ede1dc0129ccffdfc28423e76c738..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1O*aCb)T>t<7zj^a!{-~ZRpa@e* zkY6x^1A~9h3LsC$)5S5Q;?~jQhP*)Dp$%vEXWBI}{}N>nTHnLK$HJh?`9=9AP=>+N L)z4*}Q$iB}#T_8a diff --git a/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png b/res/skins/ShadeDark1024x600-Netbook/style/style_handle_checked.png deleted file mode 100644 index 1d6afaf116119d481fb7397e4d95f64844dc50b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^EI`c1!3HEbvi3>O=$B>F!ThA>N1&XxF!ThA>N1d25{m=~vu q9a(ktvcpdohMTt=tg0HjFEhE?O^W Date: Mon, 2 Mar 2015 18:34:45 +0100 Subject: [PATCH 221/481] removing unused change --- src/sampleutil.cpp | 11 +++-------- src/sampleutil.h | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/sampleutil.cpp b/src/sampleutil.cpp index 047a07845ac..6f80efb613a 100644 --- a/src/sampleutil.cpp +++ b/src/sampleutil.cpp @@ -249,8 +249,7 @@ void SampleUtil::copyWithGain(CSAMPLE* _RESTRICT pDest, const CSAMPLE* _RESTRICT // static void SampleUtil::copyWithRampingGain(CSAMPLE* _RESTRICT pDest, const CSAMPLE* _RESTRICT pSrc, - CSAMPLE_GAIN old_gain, CSAMPLE_GAIN new_gain, - int iNumSamples, bool left/*=true*/, bool right/*=true*/) { + CSAMPLE_GAIN old_gain, CSAMPLE_GAIN new_gain, int iNumSamples) { if (old_gain == CSAMPLE_GAIN_ONE && new_gain == CSAMPLE_GAIN_ONE) { copy(pDest, pSrc, iNumSamples); return; @@ -267,12 +266,8 @@ void SampleUtil::copyWithRampingGain(CSAMPLE* _RESTRICT pDest, const CSAMPLE* _R // note: LOOP VECTORIZED. for (int i = 0; i < iNumSamples / 2; ++i) { const CSAMPLE_GAIN gain = start_gain + gain_delta * i; - if( left ){ - pDest[i * 2] = pSrc[i * 2] * gain; - } - if( right ){ - pDest[i * 2 + 1] = pSrc[i * 2 + 1] * gain; - } + pDest[i * 2] = pSrc[i * 2] * gain; + pDest[i * 2 + 1] = pSrc[i * 2 + 1] * gain; } } else { // note: LOOP VECTORIZED. diff --git a/src/sampleutil.h b/src/sampleutil.h index 45b0c61e94a..fa8d920d9c5 100644 --- a/src/sampleutil.h +++ b/src/sampleutil.h @@ -135,7 +135,7 @@ class SampleUtil { // if pDest == pSrc! static void copyWithRampingGain(CSAMPLE* pDest, const CSAMPLE* pSrc, CSAMPLE_GAIN old_gain, CSAMPLE_GAIN new_gain, - int iNumSamples, bool left=true, bool right=true); + int iNumSamples); // Add each sample of pSrc, multiplied by the gain, to pDest static void addWithGain(CSAMPLE* pDest, const CSAMPLE* pSrc, From 9cd45171fea7666a2d1a96fea8a463cfc3b24764 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 5 Mar 2015 00:06:34 +0100 Subject: [PATCH 222/481] Abort decoding of invalid FLAC files to avoid memory corruption --- src/sources/soundsourceflac.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index ba5c7f90056..5201ba2ef1a 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -303,6 +303,7 @@ FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( qWarning() << "Corrupt or unsupported FLAC file:" << "Block size in FLAC frame header exceeds the maximum block size" << frame->header.blocksize << ">" << maxBlocksize; + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } switch (getChannelCount()) { case 1: { From d437912debd3562924e53f6a9bad52318a9e2213 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 5 Mar 2015 00:07:11 +0100 Subject: [PATCH 223/481] Fix usage of SampleBuffer in AnalyserQueue --- src/analyserqueue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 13cd3b3bc02..9693da4f451 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -195,7 +195,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi while (it.hasNext()) { Analyser* an = it.next(); //qDebug() << typeid(*an).name() << ".process()"; - an->process(&m_sampleBuffer[0], framesRead * kAnalysisChannels); + an->process(m_sampleBuffer.data(), m_sampleBuffer.size()); //qDebug() << "Done " << typeid(*an).name() << ".process()"; } } else { From 012e59ef1fd59d37b925a3bf2306ac38d046dbeb Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 5 Mar 2015 00:18:29 +0100 Subject: [PATCH 224/481] Immediately close() a SoundSource if open() fails --- src/soundsourceproxy.cpp | 4 ++-- src/sources/soundsource.h | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index 0b3482e9f97..4d1fa2db8d7 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -320,11 +320,11 @@ Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource() { if (!m_pSoundSource) { qDebug() << "No SoundSource available"; - DEBUG_ASSERT(!m_pAudioSource); return m_pAudioSource; } - if (OK != m_pSoundSource->open()) { + const Mixxx::SoundSourceOpener opener(m_pSoundSource); + if (OK != opener.getResult()) { qWarning() << "Failed to open SoundSource"; return m_pAudioSource; } diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index f6449845067..bdbdaee550f 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -46,6 +46,33 @@ class SoundSource: public MetadataSource, public AudioSource { typedef QSharedPointer SoundSourcePointer; +// Helper class to close a SoundSource immediately if opening fails. +// The failure upon opening a SoundSource might occur after some +// resources for decoding have already been allocated. Closing the +// SoundSource will free all those resources early. It is safe to +// repeatedly invoke close() on a SoundSource at any time. +class SoundSourceOpener { +public: + explicit SoundSourceOpener(SoundSourcePointer pSoundSource) + : m_pSoundSource(pSoundSource), + m_result(ERR) { + m_result = m_pSoundSource->open(); + } + ~SoundSourceOpener() { + if (OK != m_result) { + m_pSoundSource->close(); + } + } + + Result getResult() const { + return m_result; + } + +private: + const SoundSourcePointer m_pSoundSource; + Result m_result; +}; + } //namespace Mixxx #endif // MIXXX_SOUNDSOURCE_H From 120030fa18375383521783e22c3151f671eeed0e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 5 Mar 2015 00:43:15 +0100 Subject: [PATCH 225/481] Make SoundSourceOpener exception-safe --- src/soundsourceproxy.cpp | 4 ++-- src/sources/soundsource.h | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index 4d1fa2db8d7..d20f37ed342 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -323,8 +323,8 @@ Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource() { return m_pAudioSource; } - const Mixxx::SoundSourceOpener opener(m_pSoundSource); - if (OK != opener.getResult()) { + Mixxx::SoundSourceOpener opener(m_pSoundSource); + if (OK != opener.open()) { qWarning() << "Failed to open SoundSource"; return m_pAudioSource; } diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index bdbdaee550f..7af8021e7fd 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -55,16 +55,28 @@ class SoundSourceOpener { public: explicit SoundSourceOpener(SoundSourcePointer pSoundSource) : m_pSoundSource(pSoundSource), - m_result(ERR) { - m_result = m_pSoundSource->open(); + m_result(OK) { + // The SoundSource must be opened in a member function and + // not here! If SoundSource::open() would be invoked from + // within this constructor and fails with an exception then + // the destructor of this class would never be invoked by + // the C++ runtime. } ~SoundSourceOpener() { + // Closes the SoundSource if open() failed. if (OK != m_result) { m_pSoundSource->close(); } } - Result getResult() const { + Result open() { + // Initialization of m_result with ERR before the + // invocation of SoundSource::open() is required to + // guarantee the invocation of SoundSource::close() + // in the destructor if an exception is thrown by + // SoundSource::open()! + m_result = ERR; + m_result = m_pSoundSource->open(); return m_result; } From 83af124555c5edafa8406926bf7c5c48cd657e61 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Mar 2015 16:08:24 +0100 Subject: [PATCH 226/481] Update parsing of metadata for Opus files --- src/sources/soundsourceopus.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index a65236b7987..a660c4c9e48 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -51,11 +51,10 @@ SoundSourceOpus::~SoundSourceOpus() { close(); } -/* - Parse the file to get metadata - */ Result SoundSourceOpus::parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) const { - if (OK == readTrackMetadataFromFile(pMetadata, getLocalFileName())) { + if (OK == SoundSource::parseTrackMetadata(pMetadata)) { + // Done if the default implementation in the base class + // supports Opus files. return OK; } From fbaf6b40f0c3cbc86d40ef4f16efd70897b91657 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 6 Mar 2015 17:11:39 +0100 Subject: [PATCH 227/481] Delete obsolete #include directive --- src/sources/soundsourceopus.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index a660c4c9e48..283f77a6c65 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -1,7 +1,5 @@ #include "sources/soundsourceopus.h" -#include "metadata/trackmetadatataglib.h" - namespace Mixxx { namespace { From 4db855e0cd663ebfdc6c68436b8114c2deb207a1 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 8 Mar 2015 00:54:11 +0100 Subject: [PATCH 228/481] tests with the delay --- src/effects/native/autopaneffect.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index 16520c78876..15e6b2d918e 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -131,6 +131,9 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p float delay = round(m_pDelayParameter->value() * sampleRate); gs.delay->setDelay(delay); gs.delay->process(pInput, gs.m_pDelayBuf, numSamples); + SampleUtil::copy1WithRampingGain(pOutput, + gs.m_pDelayBuf, pInput, pInput, + numSamples); for (unsigned int i = 0; i + 1 < numSamples; i += 2) { @@ -161,6 +164,7 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p // delay on the reducing channel // todo (jclaveau) : this produces clics, especially when the period // is short. + /* if (quarter == 0.0 || quarter == 3.0){ // channel 1 increasing (left) pOutput[i] = pInput[i] * gs.frac * lawCoef; @@ -171,6 +175,14 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p pOutput[i] = gs.m_pDelayBuf[i] * gs.frac * lawCoef; pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; } + */ + // pOutput[i+1] = gs.m_pDelayBuf[i+1] * (1.0f - gs.frac) * lawCoef; + // pOutput[i] = gs.m_pDelayBuf[i] * gs.frac * lawCoef; + + // pOutput[i] = pInput[i] * gs.frac * lawCoef; + // pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; + pOutput[i] = pOutput[i] * gs.frac * lawCoef; + pOutput[i+1] = pOutput[i+1] * (1.0f - gs.frac) * lawCoef; gs.time++; } From 1559d518bbfcf3825151f002f5e7a358f87c1d4b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 9 Mar 2015 20:46:11 +0100 Subject: [PATCH 229/481] Harmonize the behavior of SoundSource::open() The new parameter channelCountHint can be used to explicitly preconfigure the decoders for different use cases. Currently Mixxx always reads stereo sample data. --- plugins/soundsourcem4a/soundsourcem4a.cpp | 15 ++--- plugins/soundsourcem4a/soundsourcem4a.h | 3 +- .../soundsourcemediafoundation.cpp | 27 ++++++--- .../soundsourcemediafoundation.h | 5 +- plugins/soundsourcewv/soundsourcewv.cpp | 16 ++--- plugins/soundsourcewv/soundsourcewv.h | 3 +- src/analyserqueue.cpp | 2 +- src/cachingreaderworker.cpp | 6 +- src/musicbrainz/chromaprinter.cpp | 2 +- src/soundsourceproxy.cpp | 5 +- src/soundsourceproxy.h | 2 +- src/sources/soundsource.cpp | 15 +++++ src/sources/soundsource.h | 60 ++++++------------- src/sources/soundsourcecoreaudio.cpp | 10 ++-- src/sources/soundsourcecoreaudio.h | 5 +- src/sources/soundsourceffmpeg.cpp | 8 +-- src/sources/soundsourceffmpeg.h | 3 +- src/sources/soundsourceflac.cpp | 8 +-- src/sources/soundsourceflac.h | 3 +- src/sources/soundsourcemodplug.cpp | 2 +- src/sources/soundsourcemodplug.h | 3 +- src/sources/soundsourcemp3.cpp | 8 +-- src/sources/soundsourcemp3.h | 3 +- src/sources/soundsourceoggvorbis.cpp | 4 +- src/sources/soundsourceoggvorbis.h | 3 +- src/sources/soundsourceopus.cpp | 9 +-- src/sources/soundsourceopus.h | 3 +- src/sources/soundsourcesndfile.cpp | 8 +-- src/sources/soundsourcesndfile.h | 3 +- 29 files changed, 115 insertions(+), 129 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 00e045980b0..ebe5b5f4016 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -116,12 +116,8 @@ SoundSourceM4A::~SoundSourceM4A() { close(); } -Result SoundSourceM4A::open() { - if (MP4_INVALID_FILE_HANDLE != m_hFile) { - qWarning() << "Cannot reopen M4A file:" << getUrl(); - return ERR; - } - +Result SoundSourceM4A::tryOpen(SINT channelCountHint) { + DEBUG_ASSERT(MP4_INVALID_FILE_HANDLE == m_hFile); /* open MP4 file, check for >= ver 1.9.1 */ #if MP4V2_PROJECT_version_hex <= 0x00010901 m_hFile = MP4Read(getLocalFileNameBytes().constData(), 0); @@ -160,7 +156,12 @@ Result SoundSourceM4A::open() { NeAACDecConfigurationPtr pDecoderConfig = NeAACDecGetCurrentConfiguration( m_hDecoder); pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ - pDecoderConfig->downMatrix = 1; /* 5.1 -> stereo */ + if ((kChannelCountZero < channelCountHint) && (2 >= channelCountHint)) { + pDecoderConfig->downMatrix = 1; /* multi -> stereo */ + } else { + pDecoderConfig->downMatrix = 0; + } + pDecoderConfig->defObjectType = LC; if (!NeAACDecSetConfiguration(m_hDecoder, pDecoderConfig)) { qWarning() << "Failed to configure AAC decoder!"; diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 8a4337ed75b..ce512008f83 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -30,7 +30,6 @@ class SoundSourceM4A: public SoundSourcePlugin { explicit SoundSourceM4A(QUrl url); ~SoundSourceM4A(); - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -39,6 +38,8 @@ class SoundSourceM4A: public SoundSourcePlugin { CSAMPLE* sampleBuffer) /*override*/; private: + Result tryOpen(SINT channelCountHint) /*override*/; + bool isValidSampleBlockId(MP4SampleId sampleBlockId) const; void restartDecoding(MP4SampleId sampleBlockId); diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 5f682655cde..b76b91b0763 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -33,7 +33,6 @@ namespace const bool sDebug = false; -const int kNumChannels = 2; const int kSampleRate = 44100; const int kLeftoverSize = 4096; // in CSAMPLE's, this seems to be the size MF AAC const int kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give @@ -104,7 +103,6 @@ SoundSourceMediaFoundation::SoundSourceMediaFoundation(QUrl url) // these are always the same, might as well just stick them here // -bkgood // AudioSource properties - setChannelCount(kNumChannels); setFrameRate(kSampleRate); // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for @@ -117,7 +115,7 @@ SoundSourceMediaFoundation::~SoundSourceMediaFoundation() { close(); } -Result SoundSourceMediaFoundation::open() { +Result SoundSourceMediaFoundation::tryOpen(SINT channelCountHint) { if (SUCCEEDED(m_hrCoInitialize)) { qWarning() << "Cannot reopen MediaFoundation file" << getUrl(); return ERR; @@ -158,7 +156,7 @@ Result SoundSourceMediaFoundation::open() { return ERR; } - if (!configureAudioStream()) { + if (!configureAudioStream(channelCountHint)) { qWarning() << "SSMF: Error configuring audio stream."; return ERR; } @@ -436,7 +434,7 @@ SINT SoundSourceMediaFoundation::readSampleFrames( If anything in here fails, just bail. I'm not going to decode HRESULTS. -- Bill */ -bool SoundSourceMediaFoundation::configureAudioStream() { +bool SoundSourceMediaFoundation::configureAudioStream(SINT channelCountHint) { HRESULT hr(S_OK); // deselect all streams, we only want the first @@ -505,10 +503,21 @@ bool SoundSourceMediaFoundation::configureAudioStream() { return false; } - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, kNumChannels); - if (FAILED(hr)) { - qWarning() << "SSMF: failed to set number of channels"; - return false; + if (kChannelCountZero < channelCountHint) { + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channelCountHint); + if (FAILED(hr)) { + qWarning() << "SSMF: failed to set number of channels"; + return false; + } + setChannelCount(channelCountHint); + } else { + UINT32 numChannels = 0; + hr = m_pAudioType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &numChannels); + if (FAILED(hr) || (0 >= numChannels)) { + qWarning() << "SSMF: failed to get number of channels"; + return false; + } + setChannelCount(numChannels); } hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, kSampleRate); diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 36bdf8682fc..023b3f35b5a 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -41,7 +41,6 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { explicit SoundSourceMediaFoundation(QUrl url); ~SoundSourceMediaFoundation(); - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -49,7 +48,9 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: - bool configureAudioStream(); + Result tryOpen(SINT channelCountHint) /*override*/; + + bool configureAudioStream(SINT channelCountHint); void copyFrames(CSAMPLE *dest, size_t *destFrames, const CSAMPLE *src, size_t srcFrames); diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 64477e010b4..18f5f894a41 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -18,16 +18,16 @@ SoundSourceWV::~SoundSourceWV() { close(); } -Result SoundSourceWV::open() { - if (m_wpc) { - qWarning() << "Cannot reopen WavPack file:" << getUrl(); - return ERR; - } - +Result SoundSourceWV::tryOpen(SINT channelCountHint) { + DEBUG_ASSERT(!m_wpc); char msg[80]; // hold possible error message + int openFlags = OPEN_WVC | OPEN_NORMALIZE; + if ((kChannelCountZero < channelCountHint) && (2 >= channelCountHint)) { + // mono or stereo + openFlags |= OPEN_2CH_MAX; + } m_wpc = WavpackOpenFileInput( - getLocalFileNameBytes().constData(), msg, - OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); + getLocalFileNameBytes().constData(), msg, openFlags, 0); if (!m_wpc) { qDebug() << "SSWV::open: failed to open file : " << msg; return ERR; diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 8a18f4baba1..b501d0e4ac0 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -20,7 +20,6 @@ class SoundSourceWV: public SoundSourcePlugin { explicit SoundSourceWV(QUrl url); ~SoundSourceWV(); - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -29,6 +28,8 @@ class SoundSourceWV: public SoundSourcePlugin { CSAMPLE* sampleBuffer) /*override*/; private: + Result tryOpen(SINT channelCountHint) /*override*/; + WavpackContext* m_wpc; CSAMPLE m_sampleScaleFactor; diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 9693da4f451..35ab4d9e613 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -309,7 +309,7 @@ void AnalyserQueue::run() { // Get the audio SoundSourceProxy soundSourceProxy(nextTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource(kAnalysisChannels)); if (!pAudioSource) { qWarning() << "Failed to open file for analyzing:" << nextTrack->getLocation(); continue; diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 8cb4979b140..1e5a9b0c890 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -134,9 +134,9 @@ void CachingReaderWorker::run() { namespace { - Mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack) { + Mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack, SINT channelCountHint) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource(channelCountHint)); if (pAudioSource.isNull()) { qWarning() << "Failed to open file:" << pTrack->getLocation(); return Mixxx::AudioSourcePointer(); @@ -169,7 +169,7 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { return; } - m_pAudioSource = openAudioSourceForReading(pTrack); + m_pAudioSource = openAudioSourceForReading(pTrack, kChunkChannels); if (m_pAudioSource.isNull()) { // Must unlock before emitting to avoid deadlock qDebug() << m_group << "CachingReaderWorker::loadTrack() load failed for\"" diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 35678917c64..e0fc928d081 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -99,7 +99,7 @@ ChromaPrinter::ChromaPrinter(QObject* parent) QString ChromaPrinter::getFingerprint(TrackPointer pTrack) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource(kFingerprintChannels)); if (pAudioSource.isNull()) { qDebug() << "Skipping invalid file:" << pTrack->getLocation(); return QString(); diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index d20f37ed342..9785f3afb1a 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -312,7 +312,7 @@ QLibrary* SoundSourceProxy::getPlugin(QString lib_filename) return pPlugin; } -Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource() { +Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource(SINT channelCountHint) { if (m_pAudioSource) { qDebug() << "AudioSource is already open"; return m_pAudioSource; @@ -323,8 +323,7 @@ Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource() { return m_pAudioSource; } - Mixxx::SoundSourceOpener opener(m_pSoundSource); - if (OK != opener.open()) { + if (OK != m_pSoundSource->open(channelCountHint)) { qWarning() << "Failed to open SoundSource"; return m_pAudioSource; } diff --git a/src/soundsourceproxy.h b/src/soundsourceproxy.h index 9c8c2acad8a..f10bb020c63 100644 --- a/src/soundsourceproxy.h +++ b/src/soundsourceproxy.h @@ -59,7 +59,7 @@ class SoundSourceProxy: public Mixxx::MetadataSource { // Opening the audio data through the proxy will // update the some metadata of the track object. // Returns a null pointer on failure. - Mixxx::AudioSourcePointer openAudioSource(); + Mixxx::AudioSourcePointer openAudioSource(SINT channelCountHint = Mixxx::AudioSource::kChannelCountDefault); void closeAudioSource(); diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index 2d463695979..ff0b658d678 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -20,6 +20,21 @@ SoundSource::SoundSource(QUrl url, QString type) DEBUG_ASSERT(getUrl().isValid()); } +Result SoundSource::open(SINT channelCountHint) { + close(); // reopening is not supported + Result result; + try { + result = tryOpen(channelCountHint); + } catch (...) { + close(); + throw; + } + if (OK != result) { + close(); + } + return result; +} + Result SoundSource::parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) const { return readTrackMetadataFromFile(pMetadata, getLocalFileName()); } diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index 7af8021e7fd..6f94d7cf227 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -31,9 +31,15 @@ class SoundSource: public MetadataSource, public AudioSource { QImage parseCoverArt() const /*override*/; // Opens the AudioSource for reading audio data. - virtual Result open() = 0; - - // Closes the AudioSource. + // + // Since reopening is not supported close() will be called + // implicitly before the AudioSource is actually opened. + Result open(SINT channelCountHint = kChannelCountDefault); + + // Closes the AudioSource and frees all resources. + // + // Might be called even if the AudioSource has never been + // opened, has already been closed, or if opening has failed. virtual void close() = 0; protected: @@ -41,50 +47,20 @@ class SoundSource: public MetadataSource, public AudioSource { SoundSource(QUrl url, QString type); private: + // Tries to open the AudioSource for reading audio data + // according to the "Template Method" design pattern. If + // tryOpen() fails all (partially) allocated resources + // will be freed by close(). Implementing classes do not + // need to free resources in tryOpen() themselves, but + // should instead be prepared for the following invocation + // of close(). + virtual Result tryOpen(SINT channelCountHint) = 0; + const QString m_type; }; typedef QSharedPointer SoundSourcePointer; -// Helper class to close a SoundSource immediately if opening fails. -// The failure upon opening a SoundSource might occur after some -// resources for decoding have already been allocated. Closing the -// SoundSource will free all those resources early. It is safe to -// repeatedly invoke close() on a SoundSource at any time. -class SoundSourceOpener { -public: - explicit SoundSourceOpener(SoundSourcePointer pSoundSource) - : m_pSoundSource(pSoundSource), - m_result(OK) { - // The SoundSource must be opened in a member function and - // not here! If SoundSource::open() would be invoked from - // within this constructor and fails with an exception then - // the destructor of this class would never be invoked by - // the C++ runtime. - } - ~SoundSourceOpener() { - // Closes the SoundSource if open() failed. - if (OK != m_result) { - m_pSoundSource->close(); - } - } - - Result open() { - // Initialization of m_result with ERR before the - // invocation of SoundSource::open() is required to - // guarantee the invocation of SoundSource::close() - // in the destructor if an exception is thrown by - // SoundSource::open()! - m_result = ERR; - m_result = m_pSoundSource->open(); - return m_result; - } - -private: - const SoundSourcePointer m_pSoundSource; - Result m_result; -}; - } //namespace Mixxx #endif // MIXXX_SOUNDSOURCE_H diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index 1540eaa3675..19b2dd54dbd 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -2,8 +2,6 @@ #include "util/math.h" -const SINT SoundSourceCoreAudio::kChannelCount = 2; // always stereo - QList SoundSourceCoreAudio::supportedFileExtensions() { QList list; list.push_back("m4a"); @@ -27,9 +25,7 @@ SoundSourceCoreAudio::~SoundSourceCoreAudio() { } // soundsource overrides -Result AudioSourceCoreAudio::open() { - close(); // safety first - +Result AudioSourceCoreAudio::tryOpen(SINT channelCountHint) { const QString fileName(getLocalFileName()); //Open the audio file. @@ -69,8 +65,10 @@ Result AudioSourceCoreAudio::open() { } // create the output format + const UInt32 numChannels = + (channelCountZero < channelCountHint) ? channelCountHint : 2; m_outputFormat = CAStreamBasicDescription(m_inputFormat.mSampleRate, - kChannelCount, CAStreamBasicDescription::kPCMFormatFloat32, true); + numChannels, CAStreamBasicDescription::kPCMFormatFloat32, true); // set the client format err = ExtAudioFileSetProperty(m_audioFile, diff --git a/src/sources/soundsourcecoreaudio.h b/src/sources/soundsourcecoreaudio.h index 7d36096756a..7dbb2d4afaa 100644 --- a/src/sources/soundsourcecoreaudio.h +++ b/src/sources/soundsourcecoreaudio.h @@ -20,14 +20,11 @@ class SoundSourceCoreAudio : public Mixxx::SoundSource { public: - static const kChannelCount; - static QList supportedFileExtensions(); explicit SoundSourceCoreAudio(QUrl url); ~SoundSourceCoreAudio(); - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -36,6 +33,8 @@ class SoundSourceCoreAudio : public Mixxx::SoundSource { CSAMPLE* sampleBuffer) /*override*/; private: + Result tryOpen(SINT channelCountHint) /*override*/; + ExtAudioFileRef m_audioFile; CAStreamBasicDescription m_inputFormat; CAStreamBasicDescription m_outputFormat; diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index e07998293ca..2c6ab802c6f 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -60,18 +60,14 @@ SoundSourceFFmpeg::~SoundSourceFFmpeg() { close(); } -Result SoundSourceFFmpeg::open() { - if (m_pFormatCtx) { - qWarning() << "Cannot reopen FFmpeg file:" << getUrl(); - return ERR; - } - +Result SoundSourceFFmpeg::tryOpen(SINT channelCountHint) { unsigned int i; AVDictionary *l_iFormatOpts = NULL; const QByteArray qBAFilename(getLocalFileNameBytes()); qDebug() << "New SoundSourceFFmpeg :" << qBAFilename; + DEBUG_ASSERT(!m_pFormatCtx); m_pFormatCtx = avformat_alloc_context(); #if LIBAVCODEC_VERSION_INT < 3622144 diff --git a/src/sources/soundsourceffmpeg.h b/src/sources/soundsourceffmpeg.h index 776cdafd873..085130bc9e6 100644 --- a/src/sources/soundsourceffmpeg.h +++ b/src/sources/soundsourceffmpeg.h @@ -47,7 +47,6 @@ class SoundSourceFFmpeg : public SoundSource { explicit SoundSourceFFmpeg(QUrl url); ~SoundSourceFFmpeg(); - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -55,6 +54,8 @@ class SoundSourceFFmpeg : public SoundSource { SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: + Result tryOpen(SINT channelCountHint) /*override*/; + bool readFramesToCache(unsigned int count, qint64 offset); bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); quint64 getSizeofCache(); diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 5201ba2ef1a..e0eb232d441 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -82,12 +82,8 @@ SoundSourceFLAC::~SoundSourceFLAC() { close(); } -Result SoundSourceFLAC::open() { - if (m_file.isOpen()) { - qWarning() << "Cannot reopen FLAC file:" << m_file.fileName(); - return ERR; - } - +Result SoundSourceFLAC::tryOpen(SINT /*channelCountHint*/) { + DEBUG_ASSERT(!m_file.isOpen()); if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open FLAC file:" << m_file.fileName(); return ERR; diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index b089acdea61..aede958b7d6 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -18,7 +18,6 @@ class SoundSourceFLAC: public SoundSource { explicit SoundSourceFLAC(QUrl url); ~SoundSourceFLAC(); - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -40,6 +39,8 @@ class SoundSourceFLAC: public SoundSource { void flacError(FLAC__StreamDecoderErrorStatus status); private: + Result tryOpen(SINT channelCountHint) /*override*/; + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer, SINT sampleBufferSize, bool readStereoSamples); diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index 5f7602f955c..13d85c23e57 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -113,7 +113,7 @@ QImage SoundSourceModPlug::parseCoverArt() const { return QImage(); } -Result SoundSourceModPlug::open() { +Result SoundSourceModPlug::tryOpen(SINT /*channelCountHint*/) { ScopedTimer t("SoundSourceModPlug::open()"); // read module file to byte array diff --git a/src/sources/soundsourcemodplug.h b/src/sources/soundsourcemodplug.h index e3f74209e35..7f818c9f8fe 100644 --- a/src/sources/soundsourcemodplug.h +++ b/src/sources/soundsourcemodplug.h @@ -32,7 +32,6 @@ class SoundSourceModPlug: public Mixxx::SoundSource { QImage parseCoverArt() const /*override*/; - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -41,6 +40,8 @@ class SoundSourceModPlug: public Mixxx::SoundSource { CSAMPLE* sampleBuffer) /*override*/; private: + Result tryOpen(SINT channelCountHint = kChannelCountDefault) /*override*/; + static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index c96b9594c25..cfbdd62ead0 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -80,12 +80,8 @@ SoundSourceMp3::~SoundSourceMp3() { close(); } -Result SoundSourceMp3::open() { - if (m_file.isOpen()) { - qWarning() << "Cannot reopen MP3 file:" << m_file.fileName(); - return ERR; - } - +Result SoundSourceMp3::tryOpen(SINT /*channelCountHint*/) { + DEBUG_ASSERT(!m_file.isOpen()); if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file:" << m_file.fileName(); return ERR; diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h index b31894105f2..2bd8ef32316 100644 --- a/src/sources/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -24,7 +24,6 @@ class SoundSourceMp3: public SoundSource { explicit SoundSourceMp3(QUrl url); ~SoundSourceMp3(); - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -39,6 +38,8 @@ class SoundSourceMp3: public SoundSource { bool readStereoSamples); private: + Result tryOpen(SINT channelCountHint) /*override*/; + QFile m_file; quint64 m_fileSize; unsigned char* m_pFileData; diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index 7251bc332c0..c3e46ce3766 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -30,9 +30,7 @@ SoundSourceOggVorbis::~SoundSourceOggVorbis() { close(); } -Result SoundSourceOggVorbis::open() { - close(); // reopen if already open - +Result SoundSourceOggVorbis::tryOpen(SINT /*channelCountHint*/) { const QByteArray qbaFilename(getLocalFileNameBytes()); if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { qWarning() << "Failed to open OggVorbis file:" << getUrl(); diff --git a/src/sources/soundsourceoggvorbis.h b/src/sources/soundsourceoggvorbis.h index 3917f430ff0..01d933907d6 100644 --- a/src/sources/soundsourceoggvorbis.h +++ b/src/sources/soundsourceoggvorbis.h @@ -15,7 +15,6 @@ class SoundSourceOggVorbis: public SoundSource { explicit SoundSourceOggVorbis(QUrl url); ~SoundSourceOggVorbis(); - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -26,6 +25,8 @@ class SoundSourceOggVorbis: public SoundSource { CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; private: + Result tryOpen(SINT channelCountHint) /*override*/; + SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer, SINT sampleBufferSize, bool readStereoSamples); diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 283f77a6c65..8dfa4151f6a 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -119,14 +119,11 @@ Result SoundSourceOpus::parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) cons return OK; } -Result SoundSourceOpus::open() { - if (m_pOggOpusFile) { - qWarning() << "Cannot reopen OggOpus file:" << getUrl(); - return ERR; - } - +Result SoundSourceOpus::tryOpen(SINT /*channelCountHint*/) { const QByteArray qbaFilename(getLocalFileNameBytes()); int errorCode = 0; + + DEBUG_ASSERT(!m_pOggOpusFile); m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); if (!m_pOggOpusFile) { qWarning() << "Failed to open OggOpus file:" << getUrl() << "errorCode" diff --git a/src/sources/soundsourceopus.h b/src/sources/soundsourceopus.h index 491ddbf4190..a879b44320e 100644 --- a/src/sources/soundsourceopus.h +++ b/src/sources/soundsourceopus.h @@ -19,7 +19,6 @@ class SoundSourceOpus: public Mixxx::SoundSource { Result parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) const /*override*/; - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -30,6 +29,8 @@ class SoundSourceOpus: public Mixxx::SoundSource { CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; private: + Result tryOpen(SINT channelCountHint) /*override*/; + OggOpusFile *m_pOggOpusFile; SINT m_curFrameIndex; diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 089c411777e..796fc51b92b 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -20,12 +20,8 @@ SoundSourceSndFile::~SoundSourceSndFile() { close(); } -Result SoundSourceSndFile::open() { - if (m_pSndFile) { - qWarning() << "Cannot reopen file:" << getUrl(); - return ERR; - } - +Result SoundSourceSndFile::tryOpen(SINT /*channelCountHint*/) { + DEBUG_ASSERT(!m_pSndFile); memset(&m_sfInfo, 0, sizeof(m_sfInfo)); #ifdef __WINDOWS__ // Pointer valid until string changed diff --git a/src/sources/soundsourcesndfile.h b/src/sources/soundsourcesndfile.h index f93f06b59b6..eda5f78377b 100644 --- a/src/sources/soundsourcesndfile.h +++ b/src/sources/soundsourcesndfile.h @@ -20,7 +20,6 @@ class SoundSourceSndFile: public Mixxx::SoundSource { explicit SoundSourceSndFile(QUrl url); ~SoundSourceSndFile(); - Result open() /*override*/; void close() /*override*/; SINT seekSampleFrame(SINT frameIndex) /*override*/; @@ -29,6 +28,8 @@ class SoundSourceSndFile: public Mixxx::SoundSource { CSAMPLE* sampleBuffer) /*override*/; private: + Result tryOpen(SINT channelCountHint) /*override*/; + SNDFILE* m_pSndFile; SF_INFO m_sfInfo; }; From 0665cbfb8fb090e7ec603d93ccc6ad53eca620ea Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 9 Mar 2015 21:34:29 +0100 Subject: [PATCH 230/481] Remove implicit assumptions from MP4SampleId calculations Consequently use kSampleBlockIdMin for block offset calculations --- plugins/soundsourcem4a/soundsourcem4a.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index ebe5b5f4016..b3773e4a8dc 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -135,11 +135,13 @@ Result SoundSourceM4A::tryOpen(SINT channelCountHint) { return ERR; } - m_maxSampleBlockId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); - if (MP4_INVALID_SAMPLE_ID == m_maxSampleBlockId) { - qWarning() << "Failed to read file structure:" << getUrl(); + const MP4SampleId numberOfSamples = + MP4GetTrackNumberOfSamples(m_hFile, m_trackId); + if (0 >= numberOfSamples) { + qWarning() << "Failed to read number of samples from file:" << getUrl(); return ERR; } + m_maxSampleBlockId = kSampleBlockIdMin + (numberOfSamples - 1); // Determine the maximum input size (in bytes) of a // sample block for the selected track. From 4fd9bed4373788d716231d91d3882c553e216af4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 9 Mar 2015 22:47:53 +0100 Subject: [PATCH 231/481] Cleanup and fix M4A buffer length/offset calculations --- plugins/soundsourcem4a/soundsourcem4a.cpp | 128 ++++++++++------------ plugins/soundsourcem4a/soundsourcem4a.h | 6 +- 2 files changed, 60 insertions(+), 74 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index b3773e4a8dc..a84f9bd8e74 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -104,11 +104,11 @@ SoundSourceM4A::SoundSourceM4A(QUrl url) m_trackId(MP4_INVALID_TRACK_ID), m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), - m_inputBufferOffset(0), m_inputBufferLength(0), + m_inputBufferOffset(0), m_hDecoder(NULL), - m_sampleBufferReadOffset(0), - m_sampleBufferWriteOffset(0), + m_sampleBufferLength(0), + m_sampleBufferOffset(0), m_curFrameIndex(kFrameIndexMin) { } @@ -235,11 +235,9 @@ void SoundSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { m_curSampleBlockId = sampleBlockId; m_curFrameIndex = getFrameIndexForSampleBlockId(m_curSampleBlockId); // discard input buffer - m_inputBufferOffset = 0; m_inputBufferLength = 0; // discard previously decoded sample data - m_sampleBufferReadOffset = 0; - m_sampleBufferWriteOffset = 0; + m_sampleBufferLength = 0; } @@ -267,17 +265,16 @@ SINT SoundSourceM4A::seekSampleFrame(SINT frameIndex) { restartDecoding(sampleBlockId); DEBUG_ASSERT(m_curSampleBlockId == sampleBlockId); } - // decoding starts before the actual target position + // Decoding starts before the actual target position DEBUG_ASSERT(m_curFrameIndex <= frameIndex); - // prefetch (decode and discard) all samples up to the target position + // Prefetch (decode and discard) all samples up to the target position const SINT prefetchFrameCount = frameIndex - m_curFrameIndex; const SINT skipFrameCount = skipSampleFrames(prefetchFrameCount); DEBUG_ASSERT(skipFrameCount <= prefetchFrameCount); if (skipFrameCount != prefetchFrameCount) { qWarning() << "Failed to skip over prefetched sample frames after seeking @" << m_curFrameIndex; - // Abort - return m_curFrameIndex; + return m_curFrameIndex; //abort } } DEBUG_ASSERT(m_curFrameIndex == frameIndex); @@ -290,49 +287,41 @@ SINT SoundSourceM4A::readSampleFrames( const SINT numberOfFramesTotal = math_min(numberOfFrames, SINT(getFrameIndexMax() - m_curFrameIndex)); + const SINT numberOfSamplesTotal = frames2samples(numberOfFramesTotal); CSAMPLE* pSampleBuffer = sampleBuffer; - SINT numberOfFramesRemaining = numberOfFramesTotal; - while (0 < numberOfFramesRemaining) { - DEBUG_ASSERT(m_sampleBufferReadOffset <= - m_sampleBufferWriteOffset); - if (m_sampleBufferReadOffset < m_sampleBufferWriteOffset) { + SINT numberOfSamplesRemaining = numberOfSamplesTotal; + while (0 < numberOfSamplesRemaining) { + + DEBUG_ASSERT(0 <= m_sampleBufferLength); + if (0 < m_sampleBufferLength) { // Consume previously decoded sample data - const SINT numberOfSamplesDecoded = - m_sampleBufferWriteOffset - - m_sampleBufferReadOffset; - const SINT numberOfFramesDecoded = - samples2frames(numberOfSamplesDecoded); - const SINT numberOfFramesRead = - math_min(numberOfFramesRemaining, numberOfFramesDecoded); const SINT numberOfSamplesRead = - frames2samples(numberOfFramesRead); + math_min(numberOfSamplesRemaining, m_sampleBufferLength); + DEBUG_ASSERT(numberOfSamplesRead <= m_sampleBufferLength); if (pSampleBuffer) { const CSAMPLE* const pDecodeBuffer = - &m_sampleBuffer[m_sampleBufferReadOffset]; + m_sampleBuffer.data() + m_sampleBufferOffset; SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); pSampleBuffer += numberOfSamplesRead; - m_sampleBufferReadOffset += numberOfSamplesRead; } - m_curFrameIndex += numberOfFramesRead; + m_sampleBufferLength -= numberOfSamplesRead; + m_sampleBufferOffset += numberOfSamplesRead; + m_curFrameIndex += samples2frames(numberOfSamplesRead); DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - numberOfFramesRemaining -= numberOfFramesRead; - if (0 == numberOfFramesRemaining) { + DEBUG_ASSERT(numberOfSamplesRemaining >= numberOfSamplesRead); + numberOfSamplesRemaining -= numberOfSamplesRead; + if (0 == numberOfSamplesRemaining) { break; // exit loop } - // All previously decoded sample data has been consumed - DEBUG_ASSERT(m_sampleBufferReadOffset == m_sampleBufferWriteOffset); } - m_sampleBufferReadOffset = 0; - m_sampleBufferWriteOffset = 0; - - DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); - if (m_inputBufferOffset >= m_inputBufferLength) { - // reset input buffer - m_inputBufferOffset = 0; - m_inputBufferLength = 0; + // All previously decoded sample data has been consumed now + DEBUG_ASSERT(0 == m_sampleBufferLength); + + if (0 == m_inputBufferLength) { + // Fill input buffer from file if (isValidSampleBlockId(m_curSampleBlockId)) { - // fill input buffer with next block of samples + // Read data for next sample block into input buffer u_int8_t* pInputBuffer = &m_inputBuffer[0]; u_int32_t inputBufferLength = m_inputBuffer.size(); // in/out parameter if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleBlockId, @@ -347,9 +336,13 @@ SINT SoundSourceM4A::readSampleFrames( } ++m_curSampleBlockId; m_inputBufferLength = inputBufferLength; + m_inputBufferOffset = 0; } } - DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); + DEBUG_ASSERT(0 <= m_inputBufferLength); + if (0 == m_inputBufferLength) { + break; // EOF + } // NOTE(uklotzde): The sample buffer for NeAACDecDecode2 has to // be big enough for a whole block of decoded samples, which @@ -357,31 +350,26 @@ SINT SoundSourceM4A::readSampleFrames( // we need to use a temporary buffer. CSAMPLE* pDecodeBuffer; // in/out parameter SINT decodeBufferCapacityInBytes; - if (pSampleBuffer && (numberOfFramesRemaining >= kFramesPerSampleBlock)) { - // decode samples directly into sampleBuffer + if (pSampleBuffer && (numberOfSamplesRemaining >= frames2samples(kFramesPerSampleBlock))) { + // Decode samples directly into sampleBuffer pDecodeBuffer = pSampleBuffer; - decodeBufferCapacityInBytes = frames2samples( - numberOfFramesRemaining) * sizeof(*pSampleBuffer); + decodeBufferCapacityInBytes = + numberOfSamplesRemaining * sizeof(*pDecodeBuffer); } else { - // decode next block of samples into temporary buffer - pDecodeBuffer = &m_sampleBuffer[m_sampleBufferWriteOffset]; - const int sampleBufferCapacity = - m_sampleBuffer.size() - - m_sampleBufferWriteOffset; - DEBUG_ASSERT(sampleBufferCapacity >= - int(frames2samples(kFramesPerSampleBlock))); + // Decode next sample block into temporary buffer + pDecodeBuffer = m_sampleBuffer.data(); decodeBufferCapacityInBytes = - sampleBufferCapacity * - sizeof(*pDecodeBuffer); + m_sampleBuffer.size() * sizeof(*pDecodeBuffer); } NeAACDecFrameInfo decFrameInfo; - void* pDecodeResult = NeAACDecDecode2(m_hDecoder, &decFrameInfo, + void* pDecodeResult = NeAACDecDecode2( + m_hDecoder, &decFrameInfo, &m_inputBuffer[m_inputBufferOffset], - m_inputBufferLength - m_inputBufferOffset, + m_inputBufferLength, reinterpret_cast(&pDecodeBuffer), decodeBufferCapacityInBytes); - // verify the decoding result + // Verify the decoding result if (0 != decFrameInfo.error) { qWarning() << "AAC decoding error:" << decFrameInfo.error @@ -391,7 +379,7 @@ SINT SoundSourceM4A::readSampleFrames( } DEBUG_ASSERT(pDecodeResult == pDecodeBuffer); // verify the in/out parameter - // verify the decoded sample data for consistency + // Verify the decoded sample data for consistency if (getChannelCount() != decFrameInfo.channels) { qWarning() << "Corrupt or unsupported AAC file:" << "Unexpected number of channels" << decFrameInfo.channels @@ -405,37 +393,35 @@ SINT SoundSourceM4A::readSampleFrames( break; // abort } - // consume input data + // Consume input data + m_inputBufferLength -= decFrameInfo.bytesconsumed; m_inputBufferOffset += decFrameInfo.bytesconsumed; - // consume decoded output data + // Consume decoded output data const SINT numberOfSamplesDecoded = decFrameInfo.samples; - const SINT numberOfFramesDecoded = - samples2frames(numberOfSamplesDecoded); - const SINT numberOfFramesRead = - math_min(numberOfFramesRemaining, numberOfFramesDecoded); const SINT numberOfSamplesRead = - frames2samples(numberOfFramesRead); + math_min(numberOfSamplesRemaining, numberOfSamplesDecoded); + DEBUG_ASSERT(numberOfSamplesDecoded >= numberOfSamplesRead); if (pDecodeBuffer == pSampleBuffer) { pSampleBuffer += numberOfSamplesRead; } else { - m_sampleBufferWriteOffset += numberOfSamplesDecoded; if (pSampleBuffer) { SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); pSampleBuffer += numberOfSamplesRead; } - m_sampleBufferReadOffset += numberOfSamplesRead; + m_sampleBufferLength = numberOfSamplesDecoded - numberOfSamplesRead; + m_sampleBufferOffset = numberOfSamplesRead; } - - m_curFrameIndex += numberOfFramesRead; + m_curFrameIndex += samples2frames(numberOfSamplesRead); DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - numberOfFramesRemaining -= numberOfFramesRead; + DEBUG_ASSERT(numberOfSamplesRemaining >= numberOfSamplesRead); + numberOfSamplesRemaining -= numberOfSamplesRead; } DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); - return numberOfFramesTotal - numberOfFramesRemaining; + DEBUG_ASSERT(numberOfSamplesTotal >= numberOfSamplesRemaining); + return samples2frames(numberOfSamplesTotal - numberOfSamplesRemaining); } } // namespace Mixxx diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index ce512008f83..f89573d0f5e 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -51,14 +51,14 @@ class SoundSourceM4A: public SoundSourcePlugin { typedef std::vector InputBuffer; InputBuffer m_inputBuffer; - SINT m_inputBufferOffset; SINT m_inputBufferLength; + SINT m_inputBufferOffset; NeAACDecHandle m_hDecoder; SampleBuffer m_sampleBuffer; - int m_sampleBufferReadOffset; - int m_sampleBufferWriteOffset; + SINT m_sampleBufferLength; + SINT m_sampleBufferOffset; SINT m_curFrameIndex; }; From 6fc054088c222dbe5ad45042f80f0bfeb4baf0bc Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 9 Mar 2015 22:58:54 +0100 Subject: [PATCH 232/481] Cleanup and fix FLAC buffer length/offset calculations --- src/sources/soundsourceflac.cpp | 95 +++++++++++++++------------------ src/sources/soundsourceflac.h | 4 +- 2 files changed, 45 insertions(+), 54 deletions(-) diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index e0eb232d441..80b2305e3d3 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -73,8 +73,8 @@ SoundSourceFLAC::SoundSourceFLAC(QUrl url) m_maxFramesize(0), m_bitsPerSample(kBitsPerSampleDefault), m_sampleScaleFactor(kSampleValueZero), - m_decodeSampleBufferReadOffset(0), - m_decodeSampleBufferWriteOffset(0), + m_decodeSampleBufferLength(0), + m_decodeSampleBufferOffset(0), m_curFrameIndex(kFrameIndexMin) { } @@ -129,8 +129,7 @@ SINT SoundSourceFLAC::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); // clear decode buffer before seeking - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; + m_decodeSampleBufferLength = 0; if (!FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex)) { qWarning() << "SSFLAC: Seeking error at file" << m_file.fileName(); } @@ -163,63 +162,59 @@ SINT SoundSourceFLAC::readSampleFrames( DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(getSampleBufferSize(numberOfFrames, readStereoSamples) <= sampleBufferSize); - const SINT numberOfFramesTotal = numberOfFrames; + const SINT numberOfFramesTotal = + math_min(numberOfFrames, getFrameIndexMax() - m_curFrameIndex); CSAMPLE* outBuffer = sampleBuffer; SINT numberOfFramesRemaining = numberOfFramesTotal; while (0 < numberOfFramesRemaining) { - DEBUG_ASSERT( - m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - // if our buffer from libflac is empty (either because we explicitly cleared + DEBUG_ASSERT(0 <= m_decodeSampleBufferLength); + // If our buffer from libflac is empty (either because we explicitly cleared // it or because we've simply used all the samples), ask for a new buffer - if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { + if (0 == m_decodeSampleBufferLength) { // Documentation of FLAC__stream_decoder_process_single(): // "Depending on what was decoded, the metadata or write callback // will be called with the decoded metadata block or audio frame." // See also: https://xiph.org/flac/api/group__flac__stream__decoder.html#ga9d6df4a39892c05955122cf7f987f856 - if (FLAC__stream_decoder_process_single(m_decoder)) { - if (m_decodeSampleBufferReadOffset - >= m_decodeSampleBufferWriteOffset) { - // EOF - break; - } - } else { + if (!FLAC__stream_decoder_process_single(m_decoder)) { qWarning() << "SSFLAC: decoder_process_single returned false (" << m_file.fileName() << ")"; - break; + break; // abort } } - DEBUG_ASSERT( - m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - const SINT decodeBufferSamples = m_decodeSampleBufferWriteOffset - - m_decodeSampleBufferReadOffset; - const SINT decodeBufferFrames = samples2frames( - decodeBufferSamples); + DEBUG_ASSERT(0 <= m_decodeSampleBufferLength); + if (0 == m_decodeSampleBufferLength) { + break; // EOF + } + const SINT framesToCopy = - math_min(decodeBufferFrames, numberOfFramesRemaining); + math_min(numberOfFramesRemaining, + samples2frames(m_decodeSampleBufferLength)); const SINT samplesToCopy = frames2samples(framesToCopy); - if (readStereoSamples && !isChannelCountStereo()) { - if (isChannelCountMono()) { - SampleUtil::copyMonoToDualMono(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy); + DEBUG_ASSERT(samplesToCopy <= m_decodeSampleBufferLength); + if (outBuffer) { + if (readStereoSamples && !isChannelCountStereo()) { + if (isChannelCountMono()) { + SampleUtil::copyMonoToDualMono(outBuffer, + m_decodeSampleBuffer.data() + m_decodeSampleBufferOffset, + framesToCopy); + } else { + SampleUtil::copyMultiToStereo(outBuffer, + m_decodeSampleBuffer.data() + m_decodeSampleBufferOffset, + framesToCopy, getChannelCount()); + } + outBuffer += framesToCopy * 2; // copied 2 samples per frame } else { - SampleUtil::copyMultiToStereo(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy, getChannelCount()); + SampleUtil::copy(outBuffer, + m_decodeSampleBuffer.data() + m_decodeSampleBufferOffset, + samplesToCopy); + outBuffer += samplesToCopy; } - outBuffer += framesToCopy * 2; // copied 2 samples per frame - } else { - SampleUtil::copy(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - samplesToCopy); - outBuffer += samplesToCopy; } - m_decodeSampleBufferReadOffset += samplesToCopy; + m_decodeSampleBufferLength -= samplesToCopy; + m_decodeSampleBufferOffset += samplesToCopy; m_curFrameIndex += framesToCopy; numberOfFramesRemaining -= framesToCopy; - DEBUG_ASSERT( - m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); } DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); @@ -277,10 +272,7 @@ FLAC__bool SoundSourceFLAC::flacEOF() { FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( const FLAC__Frame* frame, const FLAC__int32* const buffer[]) { // decode buffer must be empty before decoding the next frame - DEBUG_ASSERT(m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset); - // reset decode buffer - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; + DEBUG_ASSERT(0 == m_decodeSampleBufferLength); if (getChannelCount() != frame->header.channels) { qWarning() << "Corrupt or unsupported FLAC file:" << "Invalid number of channels in FLAC frame header" @@ -301,12 +293,13 @@ FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( << frame->header.blocksize << ">" << maxBlocksize; return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } + m_decodeSampleBufferOffset = 0; switch (getChannelCount()) { case 1: { // optimized code for 1 channel (mono) DEBUG_ASSERT(1 <= frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + m_decodeSampleBuffer[m_decodeSampleBufferLength++] = buffer[0][i] * m_sampleScaleFactor; } break; @@ -315,9 +308,9 @@ FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( // optimized code for 2 channels (stereo) DEBUG_ASSERT(2 <= frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + m_decodeSampleBuffer[m_decodeSampleBufferLength++] = buffer[0][i] * m_sampleScaleFactor; - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + m_decodeSampleBuffer[m_decodeSampleBufferLength++] = buffer[1][i] * m_sampleScaleFactor; } break; @@ -327,13 +320,12 @@ FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( DEBUG_ASSERT(getChannelCount() == frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { for (unsigned j = 0; j < frame->header.channels; ++j) { - m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = + m_decodeSampleBuffer[m_decodeSampleBufferLength++] = buffer[j][i] * m_sampleScaleFactor; } } } } - DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; } @@ -399,8 +391,7 @@ void SoundSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { m_maxBlocksize = metadata->data.stream_info.max_blocksize; m_minFramesize = metadata->data.stream_info.min_framesize; m_maxFramesize = metadata->data.stream_info.max_framesize; - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; + m_decodeSampleBufferLength = 0; const unsigned decodeSampleBufferSize = m_maxBlocksize * getChannelCount(); SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index aede958b7d6..84a6fd521d2 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -62,8 +62,8 @@ class SoundSourceFLAC: public SoundSource { CSAMPLE m_sampleScaleFactor; SampleBuffer m_decodeSampleBuffer; - int m_decodeSampleBufferReadOffset; - int m_decodeSampleBufferWriteOffset; + SINT m_decodeSampleBufferLength; + SINT m_decodeSampleBufferOffset; SINT m_curFrameIndex; }; From e1f0d4d17fce83be30dcb5978e875f8976a9808c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 10 Mar 2015 19:40:41 +0100 Subject: [PATCH 233/481] FifoSampleBuffer for temporary buffering of decoded audio data Neat, simple, and efficient implementation. Verified by debug assertions and unit tests. Might also be used as a ring buffer if moving/copying of buffered data is acceptable (see comments). --- build/depends.py | 1 + plugins/soundsourcem4a/SConscript | 1 + plugins/soundsourcem4a/soundsourcem4a.cpp | 85 ++++++------ plugins/soundsourcem4a/soundsourcem4a.h | 7 +- src/fifosamplebuffer.cpp | 94 +++++++++++++ src/fifosamplebuffer.h | 81 +++++++++++ src/sources/soundsourceflac.cpp | 86 +++++------- src/sources/soundsourceflac.h | 6 +- src/test/fifosamplebuffer_test.cpp | 161 ++++++++++++++++++++++ 9 files changed, 426 insertions(+), 96 deletions(-) create mode 100644 src/fifosamplebuffer.cpp create mode 100644 src/fifosamplebuffer.h create mode 100644 src/test/fifosamplebuffer_test.cpp diff --git a/build/depends.py b/build/depends.py index 1d0c79a952f..25d2438e39c 100644 --- a/build/depends.py +++ b/build/depends.py @@ -869,6 +869,7 @@ def sources(self, build): "sampleutil.cpp", "samplebuffer.cpp", + "fifosamplebuffer.cpp", "trackinfoobject.cpp", "track/beatgrid.cpp", diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index 8c5893fc1ae..4a0313251b8 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -16,6 +16,7 @@ m4a_sources = [ "sources/soundsourceplugin.cpp", "sources/audiosource.cpp", "samplebuffer.cpp", + "fifosamplebuffer.cpp", "sampleutil.cpp", "metadata/trackmetadata.cpp", "metadata/trackmetadatataglib.cpp" diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index a84f9bd8e74..cc797c6d09e 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -107,8 +107,6 @@ SoundSourceM4A::SoundSourceM4A(QUrl url) m_inputBufferLength(0), m_inputBufferOffset(0), m_hDecoder(NULL), - m_sampleBufferLength(0), - m_sampleBufferOffset(0), m_curFrameIndex(kFrameIndexMin) { } @@ -197,9 +195,9 @@ Result SoundSourceM4A::tryOpen(SINT channelCountHint) { setFrameCount(getFrameCountForSampleBlockId(m_maxSampleBlockId)); // Resize temporary buffer for decoded sample data - const SINT sampleBufferSize = + const SINT sampleBufferCapacity = frames2samples(kFramesPerSampleBlock); - SampleBuffer(sampleBufferSize).swap(m_sampleBuffer); + FifoSampleBuffer(sampleBufferCapacity).swap(m_sampleBuffer); // Invalidate current position to enforce the following // seek operation @@ -234,11 +232,12 @@ void SoundSourceM4A::restartDecoding(MP4SampleId sampleBlockId) { NeAACDecPostSeekReset(m_hDecoder, sampleBlockId); m_curSampleBlockId = sampleBlockId; m_curFrameIndex = getFrameIndexForSampleBlockId(m_curSampleBlockId); - // discard input buffer + + // Discard input buffer m_inputBufferLength = 0; - // discard previously decoded sample data - m_sampleBufferLength = 0; + // Discard previously decoded sample data + m_sampleBuffer.reset(); } SINT SoundSourceM4A::seekSampleFrame(SINT frameIndex) { @@ -273,8 +272,10 @@ SINT SoundSourceM4A::seekSampleFrame(SINT frameIndex) { skipSampleFrames(prefetchFrameCount); DEBUG_ASSERT(skipFrameCount <= prefetchFrameCount); if (skipFrameCount != prefetchFrameCount) { - qWarning() << "Failed to skip over prefetched sample frames after seeking @" << m_curFrameIndex; - return m_curFrameIndex; //abort + qWarning() + << "Failed to skip over prefetched sample frames after seeking @" + << m_curFrameIndex; + return m_curFrameIndex; // abort } } DEBUG_ASSERT(m_curFrameIndex == frameIndex); @@ -293,30 +294,24 @@ SINT SoundSourceM4A::readSampleFrames( SINT numberOfSamplesRemaining = numberOfSamplesTotal; while (0 < numberOfSamplesRemaining) { - DEBUG_ASSERT(0 <= m_sampleBufferLength); - if (0 < m_sampleBufferLength) { + if (!m_sampleBuffer.isEmpty()) { // Consume previously decoded sample data - const SINT numberOfSamplesRead = - math_min(numberOfSamplesRemaining, m_sampleBufferLength); - DEBUG_ASSERT(numberOfSamplesRead <= m_sampleBufferLength); + const std::pair readBuffer( + m_sampleBuffer.shrinkHead(numberOfSamplesRemaining)); if (pSampleBuffer) { - const CSAMPLE* const pDecodeBuffer = - m_sampleBuffer.data() + m_sampleBufferOffset; - SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); - pSampleBuffer += numberOfSamplesRead; + SampleUtil::copy(pSampleBuffer, readBuffer.first, readBuffer.second); + pSampleBuffer += readBuffer.second; } - m_sampleBufferLength -= numberOfSamplesRead; - m_sampleBufferOffset += numberOfSamplesRead; - m_curFrameIndex += samples2frames(numberOfSamplesRead); + m_curFrameIndex += samples2frames(readBuffer.second); DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfSamplesRemaining >= numberOfSamplesRead); - numberOfSamplesRemaining -= numberOfSamplesRead; + DEBUG_ASSERT(numberOfSamplesRemaining >= readBuffer.second); + numberOfSamplesRemaining -= readBuffer.second; if (0 == numberOfSamplesRemaining) { break; // exit loop } } // All previously decoded sample data has been consumed now - DEBUG_ASSERT(0 == m_sampleBufferLength); + DEBUG_ASSERT(m_sampleBuffer.isEmpty()); if (0 == m_inputBufferLength) { // Fill input buffer from file @@ -349,18 +344,22 @@ SINT SoundSourceM4A::readSampleFrames( // contains up to kFramesPerSampleBlock frames. Otherwise // we need to use a temporary buffer. CSAMPLE* pDecodeBuffer; // in/out parameter - SINT decodeBufferCapacityInBytes; - if (pSampleBuffer && (numberOfSamplesRemaining >= frames2samples(kFramesPerSampleBlock))) { + SINT decodeBufferCapacity; + const SINT decodeBufferCapacityMin = frames2samples(kFramesPerSampleBlock); + if (pSampleBuffer && (decodeBufferCapacityMin <= numberOfSamplesRemaining)) { // Decode samples directly into sampleBuffer pDecodeBuffer = pSampleBuffer; - decodeBufferCapacityInBytes = - numberOfSamplesRemaining * sizeof(*pDecodeBuffer); + decodeBufferCapacity = numberOfSamplesRemaining; } else { // Decode next sample block into temporary buffer - pDecodeBuffer = m_sampleBuffer.data(); - decodeBufferCapacityInBytes = - m_sampleBuffer.size() * sizeof(*pDecodeBuffer); + const SINT growTailCount = math_max( + numberOfSamplesRemaining, decodeBufferCapacityMin); + const std::pair writeBuffer( + m_sampleBuffer.growTail(growTailCount)); + pDecodeBuffer = writeBuffer.first; + decodeBufferCapacity = writeBuffer.second; } + DEBUG_ASSERT(decodeBufferCapacityMin <= decodeBufferCapacity); NeAACDecFrameInfo decFrameInfo; void* pDecodeResult = NeAACDecDecode2( @@ -368,7 +367,7 @@ SINT SoundSourceM4A::readSampleFrames( &m_inputBuffer[m_inputBufferOffset], m_inputBufferLength, reinterpret_cast(&pDecodeBuffer), - decodeBufferCapacityInBytes); + decodeBufferCapacity * sizeof(*pDecodeBuffer)); // Verify the decoding result if (0 != decFrameInfo.error) { qWarning() << "AAC decoding error:" @@ -398,21 +397,27 @@ SINT SoundSourceM4A::readSampleFrames( m_inputBufferOffset += decFrameInfo.bytesconsumed; // Consume decoded output data - const SINT numberOfSamplesDecoded = - decFrameInfo.samples; - const SINT numberOfSamplesRead = - math_min(numberOfSamplesRemaining, numberOfSamplesDecoded); - DEBUG_ASSERT(numberOfSamplesDecoded >= numberOfSamplesRead); + const SINT numberOfSamplesDecoded = decFrameInfo.samples; + DEBUG_ASSERT(numberOfSamplesDecoded <= decodeBufferCapacity); + SINT numberOfSamplesRead; if (pDecodeBuffer == pSampleBuffer) { + numberOfSamplesRead = math_min(numberOfSamplesDecoded, numberOfSamplesRemaining); pSampleBuffer += numberOfSamplesRead; } else { + m_sampleBuffer.shrinkTail(decodeBufferCapacity - numberOfSamplesDecoded); + const std::pair readBuffer( + m_sampleBuffer.shrinkHead(numberOfSamplesRemaining)); + numberOfSamplesRead = readBuffer.second; if (pSampleBuffer) { - SampleUtil::copy(pSampleBuffer, pDecodeBuffer, numberOfSamplesRead); + SampleUtil::copy(pSampleBuffer, readBuffer.first, numberOfSamplesRead); pSampleBuffer += numberOfSamplesRead; } - m_sampleBufferLength = numberOfSamplesDecoded - numberOfSamplesRead; - m_sampleBufferOffset = numberOfSamplesRead; } + // The decoder might decode more samples than actually needed + // at the end of the file! When the end of the file has been + // reached decoding can be restarted by seeking to a new + // position. + DEBUG_ASSERT(numberOfSamplesDecoded >= numberOfSamplesRead); m_curFrameIndex += samples2frames(numberOfSamplesRead); DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(numberOfSamplesRemaining >= numberOfSamplesRead); diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index f89573d0f5e..438bfe366c8 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -2,7 +2,8 @@ #define MIXXX_SOUNDSOURCEM4A_H #include "sources/soundsourceplugin.h" -#include "samplebuffer.h" + +#include "fifosamplebuffer.h" #ifdef __MP4V2__ #include @@ -56,9 +57,7 @@ class SoundSourceM4A: public SoundSourcePlugin { NeAACDecHandle m_hDecoder; - SampleBuffer m_sampleBuffer; - SINT m_sampleBufferLength; - SINT m_sampleBufferOffset; + FifoSampleBuffer m_sampleBuffer; SINT m_curFrameIndex; }; diff --git a/src/fifosamplebuffer.cpp b/src/fifosamplebuffer.cpp new file mode 100644 index 00000000000..b03943ae4c5 --- /dev/null +++ b/src/fifosamplebuffer.cpp @@ -0,0 +1,94 @@ +#include "fifosamplebuffer.h" + +#include "sampleutil.h" + +#define DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer() \ + DEBUG_ASSERT(0 <= m_readOffset); \ + DEBUG_ASSERT(m_readOffset <= m_writeOffset); \ + DEBUG_ASSERT(m_writeOffset <= m_sampleBuffer.size()); \ + DEBUG_ASSERT(m_sampleBuffer.size() == m_shadowBuffer.size()) + +FifoSampleBuffer::FifoSampleBuffer() + : m_readOffset(0), + m_writeOffset(0) { + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); +} + +FifoSampleBuffer::FifoSampleBuffer(SINT capacity) + : m_sampleBuffer(capacity), + m_shadowBuffer(capacity), + m_readOffset(0), + m_writeOffset(0) { + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); +} + +void FifoSampleBuffer::swap(FifoSampleBuffer& other) { + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); + std::swap(m_sampleBuffer, other.m_sampleBuffer); + std::swap(m_shadowBuffer, other.m_shadowBuffer); + std::swap(m_readOffset, other.m_readOffset); + std::swap(m_writeOffset, other.m_writeOffset); + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); +} + +void FifoSampleBuffer::swapBuffers() { + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); + SampleUtil::copy( + m_shadowBuffer.data(), + m_sampleBuffer.data() + m_readOffset, + m_writeOffset - m_readOffset); + m_sampleBuffer.swap(m_shadowBuffer); + m_writeOffset -= m_readOffset; + m_readOffset = 0; + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); +} + +void FifoSampleBuffer::reset() { + m_readOffset = 0; + m_writeOffset = 0; +} + +void FifoSampleBuffer::trim() { + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); + if (0 < m_readOffset) { + if (isEmpty()) { + reset(); + } else { + swapBuffers(); + } + } + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); +} + +std::pair FifoSampleBuffer::growTail(SINT size) { + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); + if (isEmpty()) { + reset(); + } else { + if ((m_sampleBuffer.size() - m_writeOffset) < size) { + trim(); + } + } + CSAMPLE* const resultData = m_sampleBuffer.data() + m_writeOffset; + const SINT resultSize = math_min(size, m_sampleBuffer.size() - m_writeOffset); + m_writeOffset += resultSize; + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); + return std::make_pair(resultData, resultSize); +} + +SINT FifoSampleBuffer::shrinkTail(SINT size) { + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); + const SINT resultSize = math_min(size, getSize()); + m_writeOffset -= resultSize; + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); + return resultSize; +} + +std::pair FifoSampleBuffer::shrinkHead(SINT size) { + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); + const CSAMPLE* const resultData = m_sampleBuffer.data() + m_readOffset; + const SINT resultSize = math_min(size, getSize()); + m_readOffset += resultSize; + DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); + return std::make_pair(resultData, resultSize); +} diff --git a/src/fifosamplebuffer.h b/src/fifosamplebuffer.h new file mode 100644 index 00000000000..4f7ce9d35a3 --- /dev/null +++ b/src/fifosamplebuffer.h @@ -0,0 +1,81 @@ +#ifndef FIFOSAMPLEBUFFER_H +#define FIFOSAMPLEBUFFER_H + +#include "samplebuffer.h" + +// A FIFO sample buffer with a fixed capacity, range checking, +// and double-buffering. +// +// Maximum performance is achieved when consuming all buffered +// samples before the capacity is exhausted. This use case does +// not require any internal copying/moving of buffered samples. +class FifoSampleBuffer { + Q_DISABLE_COPY(FifoSampleBuffer); + +public: + FifoSampleBuffer(); + explicit FifoSampleBuffer(SINT capacity); + + void swap(FifoSampleBuffer& other); + + bool isEmpty() const { + return m_writeOffset <= m_readOffset; + } + + // Returns the current size of the buffer, i.e. the number of + // buffered samples that have been written and are available + // for reading. + SINT getSize() const { + return m_writeOffset - m_readOffset; + } + + // Discards all buffered samples and resets the buffer to its + // initial state. + void reset(); + + // Moves all buffered samples to the beginning of the internal buffer. + // + // This function will be called implicitly by growTail() when needed. + void trim(); + + // Reserves space at the buffer's tail for writing samples. The internal + // buffer is trimmed if necessary to provide a continuous memory region. + // + // Returns a pointer to the continuous memory region and the actual number + // of samples that have been reserved. The pointer is valid for writing as + // long as no modifying member function is called! + std::pair growTail(SINT size); + + // Shrinks the buffer from the tail discarding the buffered samples. + // + // Returns the actual number of buffered samples that have been discarded. + SINT shrinkTail(SINT size); + + // Shrinks the buffer from the head for reading buffered samples. + // + // Returns a pointer to the continuous memory region and the actual + // number of buffered samples that have been dropped. The pointer is + // valid for reading as long as no modifying member function is called! + std::pair shrinkHead(SINT size); + +private: + void swapBuffers(); + + SampleBuffer m_sampleBuffer; + SampleBuffer m_shadowBuffer; + SINT m_readOffset; + SINT m_writeOffset; +}; + +namespace std +{ + +// Template specialization of std::swap for FifoSampleBuffer. +template<> +inline void swap(FifoSampleBuffer& lhs, FifoSampleBuffer& rhs) { + lhs.swap(rhs); +} + +} + +#endif // FIFOSAMPLEBUFFER_H diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 80b2305e3d3..9e877c893e3 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -73,8 +73,6 @@ SoundSourceFLAC::SoundSourceFLAC(QUrl url) m_maxFramesize(0), m_bitsPerSample(kBitsPerSampleDefault), m_sampleScaleFactor(kSampleValueZero), - m_decodeSampleBufferLength(0), - m_decodeSampleBufferOffset(0), m_curFrameIndex(kFrameIndexMin) { } @@ -128,8 +126,9 @@ SINT SoundSourceFLAC::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - // clear decode buffer before seeking - m_decodeSampleBufferLength = 0; + // Discard decoded sample data before seeking + m_sampleBuffer.reset(); + if (!FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex)) { qWarning() << "SSFLAC: Seeking error at file" << m_file.fileName(); } @@ -164,14 +163,14 @@ SINT SoundSourceFLAC::readSampleFrames( const SINT numberOfFramesTotal = math_min(numberOfFrames, getFrameIndexMax() - m_curFrameIndex); + const SINT numberOfSamplesTotal = frames2samples(numberOfFramesTotal); CSAMPLE* outBuffer = sampleBuffer; - SINT numberOfFramesRemaining = numberOfFramesTotal; - while (0 < numberOfFramesRemaining) { - DEBUG_ASSERT(0 <= m_decodeSampleBufferLength); + SINT numberOfSamplesRemaining = numberOfSamplesTotal; + while (0 < numberOfSamplesRemaining) { // If our buffer from libflac is empty (either because we explicitly cleared // it or because we've simply used all the samples), ask for a new buffer - if (0 == m_decodeSampleBufferLength) { + if (m_sampleBuffer.isEmpty()) { // Documentation of FLAC__stream_decoder_process_single(): // "Depending on what was decoded, the metadata or write callback // will be called with the decoded metadata block or audio frame." @@ -182,44 +181,37 @@ SINT SoundSourceFLAC::readSampleFrames( break; // abort } } - DEBUG_ASSERT(0 <= m_decodeSampleBufferLength); - if (0 == m_decodeSampleBufferLength) { + if (m_sampleBuffer.isEmpty()) { break; // EOF } - const SINT framesToCopy = - math_min(numberOfFramesRemaining, - samples2frames(m_decodeSampleBufferLength)); - const SINT samplesToCopy = frames2samples(framesToCopy); - DEBUG_ASSERT(samplesToCopy <= m_decodeSampleBufferLength); + const std::pair readBuffer( + m_sampleBuffer.shrinkHead(numberOfSamplesRemaining)); + const SINT framesToCopy = samples2frames(readBuffer.second); if (outBuffer) { if (readStereoSamples && !isChannelCountStereo()) { if (isChannelCountMono()) { SampleUtil::copyMonoToDualMono(outBuffer, - m_decodeSampleBuffer.data() + m_decodeSampleBufferOffset, + readBuffer.first, framesToCopy); } else { SampleUtil::copyMultiToStereo(outBuffer, - m_decodeSampleBuffer.data() + m_decodeSampleBufferOffset, + readBuffer.first, framesToCopy, getChannelCount()); } outBuffer += framesToCopy * 2; // copied 2 samples per frame } else { - SampleUtil::copy(outBuffer, - m_decodeSampleBuffer.data() + m_decodeSampleBufferOffset, - samplesToCopy); - outBuffer += samplesToCopy; + SampleUtil::copy(outBuffer, readBuffer.first, readBuffer.second); + outBuffer += readBuffer.second; } } - m_decodeSampleBufferLength -= samplesToCopy; - m_decodeSampleBufferOffset += samplesToCopy; m_curFrameIndex += framesToCopy; - numberOfFramesRemaining -= framesToCopy; + numberOfSamplesRemaining -= readBuffer.second; } DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining); - return numberOfFramesTotal - numberOfFramesRemaining; + DEBUG_ASSERT(numberOfSamplesTotal >= numberOfSamplesRemaining); + return samples2frames(numberOfSamplesTotal - numberOfSamplesRemaining); } // flac callback methods @@ -272,56 +264,55 @@ FLAC__bool SoundSourceFLAC::flacEOF() { FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( const FLAC__Frame* frame, const FLAC__int32* const buffer[]) { // decode buffer must be empty before decoding the next frame - DEBUG_ASSERT(0 == m_decodeSampleBufferLength); - if (getChannelCount() != frame->header.channels) { + if (frame->header.channels != getChannelCount()) { qWarning() << "Corrupt or unsupported FLAC file:" << "Invalid number of channels in FLAC frame header" << frame->header.channels << "<>" << getChannelCount(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - if (getFrameRate() != frame->header.sample_rate) { + if (frame->header.sample_rate != getFrameRate()) { qWarning() << "Corrupt or unsupported FLAC file:" << "Invalid sample rate in FLAC frame header" << frame->header.sample_rate << "<>" << getFrameRate(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - const unsigned maxBlocksize = samples2frames( - m_decodeSampleBuffer.size()); - if (maxBlocksize < frame->header.blocksize) { + if (frame->header.blocksize > m_maxBlocksize) { qWarning() << "Corrupt or unsupported FLAC file:" << "Block size in FLAC frame header exceeds the maximum block size" - << frame->header.blocksize << ">" << maxBlocksize; + << frame->header.blocksize << ">" << m_maxBlocksize; return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - m_decodeSampleBufferOffset = 0; + + DEBUG_ASSERT(m_sampleBuffer.isEmpty()); + const std::pair writeBuffer( + m_sampleBuffer.growTail( + frames2samples(frame->header.blocksize))); + CSAMPLE* pSampleBuffer = writeBuffer.first; switch (getChannelCount()) { case 1: { // optimized code for 1 channel (mono) DEBUG_ASSERT(1 <= frame->header.channels); - for (unsigned i = 0; i < frame->header.blocksize; ++i) { - m_decodeSampleBuffer[m_decodeSampleBufferLength++] = - buffer[0][i] * m_sampleScaleFactor; + for (SINT i = 0; i < writeBuffer.second; ++i) { + *pSampleBuffer++ = buffer[0][i] * m_sampleScaleFactor; } break; } case 2: { // optimized code for 2 channels (stereo) DEBUG_ASSERT(2 <= frame->header.channels); - for (unsigned i = 0; i < frame->header.blocksize; ++i) { - m_decodeSampleBuffer[m_decodeSampleBufferLength++] = - buffer[0][i] * m_sampleScaleFactor; - m_decodeSampleBuffer[m_decodeSampleBufferLength++] = - buffer[1][i] * m_sampleScaleFactor; + DEBUG_ASSERT(0 == (writeBuffer.second % 2)); + for (SINT i = 0; i < (writeBuffer.second / 2); ++i) { + *pSampleBuffer++ = buffer[0][i] * m_sampleScaleFactor; + *pSampleBuffer++ = buffer[1][i] * m_sampleScaleFactor; } break; } default: { // generic code for multiple channels DEBUG_ASSERT(getChannelCount() == frame->header.channels); - for (unsigned i = 0; i < frame->header.blocksize; ++i) { + for (SINT i = 0; i < samples2frames(writeBuffer.second); ++i) { for (unsigned j = 0; j < frame->header.channels; ++j) { - m_decodeSampleBuffer[m_decodeSampleBufferLength++] = - buffer[j][i] * m_sampleScaleFactor; + *pSampleBuffer++ = buffer[j][i] * m_sampleScaleFactor; } } } @@ -391,10 +382,9 @@ void SoundSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { m_maxBlocksize = metadata->data.stream_info.max_blocksize; m_minFramesize = metadata->data.stream_info.min_framesize; m_maxFramesize = metadata->data.stream_info.max_framesize; - m_decodeSampleBufferLength = 0; - const unsigned decodeSampleBufferSize = + const SINT sampleBufferCapacity = m_maxBlocksize * getChannelCount(); - SampleBuffer(decodeSampleBufferSize).swap(m_decodeSampleBuffer); + FifoSampleBuffer(sampleBufferCapacity).swap(m_sampleBuffer); break; } default: diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index 84a6fd521d2..08771c03da2 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -3,7 +3,7 @@ #include "sources/soundsource.h" -#include "samplebuffer.h" +#include "fifosamplebuffer.h" #include @@ -61,9 +61,7 @@ class SoundSourceFLAC: public SoundSource { CSAMPLE m_sampleScaleFactor; - SampleBuffer m_decodeSampleBuffer; - SINT m_decodeSampleBufferLength; - SINT m_decodeSampleBufferOffset; + FifoSampleBuffer m_sampleBuffer; SINT m_curFrameIndex; }; diff --git a/src/test/fifosamplebuffer_test.cpp b/src/test/fifosamplebuffer_test.cpp new file mode 100644 index 00000000000..a82050701ec --- /dev/null +++ b/src/test/fifosamplebuffer_test.cpp @@ -0,0 +1,161 @@ +#include "test/mixxxtest.h" + +#include "fifosamplebuffer.h" + +#include + +#include + +class FifoSampleBufferTest: public MixxxTest { +public: + FifoSampleBufferTest() + : m_writeValue(CSAMPLE_ZERO), + m_readValue(CSAMPLE_ZERO) { + } + +protected: + static const SINT kCapacity; + + SINT write(FifoSampleBuffer* pSampleBuffer, SINT writeCount) { + const std::pair writeBuffer( + pSampleBuffer->growTail(writeCount)); + for (SINT i = 0; i < writeBuffer.second; ++i) { + writeBuffer.first[i] = m_writeValue; + m_writeValue += CSAMPLE_ONE; + } + return writeBuffer.second; + } + + SINT readAndVerify(FifoSampleBuffer* pSampleBuffer, SINT readCount) { + const std::pairreadBuffer( + pSampleBuffer->shrinkHead(readCount)); + for (SINT i = 0; i < readBuffer.second; ++i) { + EXPECT_EQ(readBuffer.first[i], m_readValue); + m_readValue += CSAMPLE_ONE; + } + return readBuffer.second; + } + + SINT shrinkTail(FifoSampleBuffer* pSampleBuffer, SINT shrinkCount) { + const SINT shrinkResult = pSampleBuffer->shrinkTail(shrinkCount); + for (SINT i = 0; i < shrinkResult; ++i) { + m_writeValue -= CSAMPLE_ONE; + } + return shrinkResult; + } + + void reset(FifoSampleBuffer* pSampleBuffer) { + pSampleBuffer->reset(); + m_writeValue = CSAMPLE_ZERO; + m_readValue = CSAMPLE_ZERO; + } + +private: + CSAMPLE m_writeValue; + CSAMPLE m_readValue; +}; + +const SINT FifoSampleBufferTest::kCapacity = 100; + +TEST_F(FifoSampleBufferTest, emptyWithoutCapacity) { + FifoSampleBuffer sampleBuffer; + EXPECT_TRUE(sampleBuffer.isEmpty()); + + sampleBuffer.trim(); + EXPECT_TRUE(sampleBuffer.isEmpty()); + + sampleBuffer.reset(); + EXPECT_TRUE(sampleBuffer.isEmpty()); + + const std::pair writeResult( + sampleBuffer.growTail(10)); + EXPECT_EQ(writeResult.first, static_cast(NULL)); + EXPECT_EQ(writeResult.second, 0); + EXPECT_TRUE(sampleBuffer.isEmpty()); + + const std::pair readResult( + sampleBuffer.shrinkHead(10)); + EXPECT_EQ(readResult.first, static_cast(NULL)); + EXPECT_EQ(readResult.second, 0); + EXPECT_TRUE(sampleBuffer.isEmpty()); +} + +TEST_F(FifoSampleBufferTest, emptyWithCapacity) { + FifoSampleBuffer sampleBuffer(kCapacity); + EXPECT_TRUE(sampleBuffer.isEmpty()); + + sampleBuffer.trim(); + EXPECT_TRUE(sampleBuffer.isEmpty()); + + sampleBuffer.reset(); + EXPECT_TRUE(sampleBuffer.isEmpty()); +} + +TEST_F(FifoSampleBufferTest, readWriteTrim) { + FifoSampleBuffer sampleBuffer(kCapacity); + + SINT writeCount1 = write(&sampleBuffer, kCapacity + 10); + EXPECT_EQ(writeCount1, kCapacity); // buffer is full + EXPECT_FALSE(sampleBuffer.isEmpty()); + + SINT readCount1 = readAndVerify(&sampleBuffer, kCapacity - 10); + EXPECT_EQ(readCount1, kCapacity - 10); + EXPECT_FALSE(sampleBuffer.isEmpty()); + + SINT writeCount2 = write(&sampleBuffer, kCapacity); + EXPECT_EQ(writeCount2, readCount1); // buffer has been refilled + EXPECT_FALSE(sampleBuffer.isEmpty()); + + SINT readCount2 = readAndVerify(&sampleBuffer, kCapacity - 10); + EXPECT_EQ(readCount2, kCapacity - 10); + EXPECT_FALSE(sampleBuffer.isEmpty()); + + sampleBuffer.trim(); + + SINT writeCount3 = write(&sampleBuffer, kCapacity); + EXPECT_EQ(writeCount3, readCount2); // buffer has been refilled + EXPECT_FALSE(sampleBuffer.isEmpty()); + + SINT readCount3 = readAndVerify(&sampleBuffer, kCapacity + 10); + EXPECT_EQ(readCount3, kCapacity); // whole buffer has been read + EXPECT_TRUE(sampleBuffer.isEmpty()); +} + +TEST_F(FifoSampleBufferTest, shrink) { + FifoSampleBuffer sampleBuffer(kCapacity); + + SINT writeCount1 = write(&sampleBuffer, kCapacity - 10); + EXPECT_EQ(writeCount1, kCapacity - 10); + EXPECT_FALSE(sampleBuffer.isEmpty()); + SINT shrinkCount1 = shrinkTail(&sampleBuffer, 10); + EXPECT_EQ(shrinkCount1, 10); + SINT readCount1 = readAndVerify(&sampleBuffer, 10); + EXPECT_EQ(readCount1, 10); + EXPECT_FALSE(sampleBuffer.isEmpty()); + SINT readCount2 = readAndVerify(&sampleBuffer, kCapacity - 40); + EXPECT_EQ(readCount2, kCapacity - 40); + EXPECT_FALSE(sampleBuffer.isEmpty()); + SINT readCount3 = readAndVerify(&sampleBuffer, 20); + EXPECT_EQ(readCount3, 10); + EXPECT_TRUE(sampleBuffer.isEmpty()); + + SINT writeCount2 = write(&sampleBuffer, 20); + EXPECT_EQ(writeCount2, 20); + EXPECT_FALSE(sampleBuffer.isEmpty()); + SINT shrinkCount2 = shrinkTail(&sampleBuffer, 21); + EXPECT_EQ(shrinkCount2, 20); + EXPECT_TRUE(sampleBuffer.isEmpty()); +} + +TEST_F(FifoSampleBufferTest, reset) { + FifoSampleBuffer sampleBuffer(kCapacity); + + SINT writeCount = write(&sampleBuffer, 10); + EXPECT_EQ(writeCount, 10); + EXPECT_FALSE(sampleBuffer.isEmpty()); + reset(&sampleBuffer); + EXPECT_TRUE(sampleBuffer.isEmpty()); + SINT readCount = readAndVerify(&sampleBuffer, 10); + EXPECT_EQ(readCount, 0); + EXPECT_TRUE(sampleBuffer.isEmpty()); +} From 4c87b40e35520d7d72f319c1e14b5d30955ffdd8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 10 Mar 2015 22:57:52 +0100 Subject: [PATCH 234/481] Read from FifoSampleBuffer on both ends head/tail (FIFO/LIFO) --- src/fifosamplebuffer.cpp | 5 ++-- src/fifosamplebuffer.h | 10 ++++--- src/test/fifosamplebuffer_test.cpp | 48 ++++++++++++++++-------------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/fifosamplebuffer.cpp b/src/fifosamplebuffer.cpp index b03943ae4c5..1dee51c0197 100644 --- a/src/fifosamplebuffer.cpp +++ b/src/fifosamplebuffer.cpp @@ -76,12 +76,13 @@ std::pair FifoSampleBuffer::growTail(SINT size) { return std::make_pair(resultData, resultSize); } -SINT FifoSampleBuffer::shrinkTail(SINT size) { +std::pair FifoSampleBuffer::shrinkTail(SINT size) { DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); const SINT resultSize = math_min(size, getSize()); m_writeOffset -= resultSize; + const CSAMPLE* const resultData = m_sampleBuffer.data() + m_writeOffset; DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - return resultSize; + return std::make_pair(resultData, resultSize); } std::pair FifoSampleBuffer::shrinkHead(SINT size) { diff --git a/src/fifosamplebuffer.h b/src/fifosamplebuffer.h index 4f7ce9d35a3..4ecd99df74a 100644 --- a/src/fifosamplebuffer.h +++ b/src/fifosamplebuffer.h @@ -3,7 +3,7 @@ #include "samplebuffer.h" -// A FIFO sample buffer with a fixed capacity, range checking, +// A FIFO/LIFO sample buffer with a fixed capacity, range checking, // and double-buffering. // // Maximum performance is achieved when consuming all buffered @@ -46,10 +46,12 @@ class FifoSampleBuffer { // long as no modifying member function is called! std::pair growTail(SINT size); - // Shrinks the buffer from the tail discarding the buffered samples. + // Shrinks the buffer from the tail for reading buffered samples. // - // Returns the actual number of buffered samples that have been discarded. - SINT shrinkTail(SINT size); + // Returns a pointer to the continuous memory region and the actual + // number of buffered samples that have been dropped. The pointer is + // valid for reading as long as no modifying member function is called! + std::pair shrinkTail(SINT size); // Shrinks the buffer from the head for reading buffered samples. // diff --git a/src/test/fifosamplebuffer_test.cpp b/src/test/fifosamplebuffer_test.cpp index a82050701ec..862526086ee 100644 --- a/src/test/fifosamplebuffer_test.cpp +++ b/src/test/fifosamplebuffer_test.cpp @@ -16,9 +16,9 @@ class FifoSampleBufferTest: public MixxxTest { protected: static const SINT kCapacity; - SINT write(FifoSampleBuffer* pSampleBuffer, SINT writeCount) { + SINT writeTail(FifoSampleBuffer* pSampleBuffer, SINT size) { const std::pair writeBuffer( - pSampleBuffer->growTail(writeCount)); + pSampleBuffer->growTail(size)); for (SINT i = 0; i < writeBuffer.second; ++i) { writeBuffer.first[i] = m_writeValue; m_writeValue += CSAMPLE_ONE; @@ -26,9 +26,9 @@ class FifoSampleBufferTest: public MixxxTest { return writeBuffer.second; } - SINT readAndVerify(FifoSampleBuffer* pSampleBuffer, SINT readCount) { + SINT readHeadAndVerify(FifoSampleBuffer* pSampleBuffer, SINT size) { const std::pairreadBuffer( - pSampleBuffer->shrinkHead(readCount)); + pSampleBuffer->shrinkHead(size)); for (SINT i = 0; i < readBuffer.second; ++i) { EXPECT_EQ(readBuffer.first[i], m_readValue); m_readValue += CSAMPLE_ONE; @@ -36,12 +36,14 @@ class FifoSampleBufferTest: public MixxxTest { return readBuffer.second; } - SINT shrinkTail(FifoSampleBuffer* pSampleBuffer, SINT shrinkCount) { - const SINT shrinkResult = pSampleBuffer->shrinkTail(shrinkCount); - for (SINT i = 0; i < shrinkResult; ++i) { + SINT readTailAndVerify(FifoSampleBuffer* pSampleBuffer, SINT size) { + const std::pairreadBuffer( + pSampleBuffer->shrinkTail(size)); + for (SINT i = readBuffer.second; i-- > 0; ) { m_writeValue -= CSAMPLE_ONE; + EXPECT_EQ(readBuffer.first[i], m_writeValue); } - return shrinkResult; + return readBuffer.second; } void reset(FifoSampleBuffer* pSampleBuffer) { @@ -94,29 +96,29 @@ TEST_F(FifoSampleBufferTest, emptyWithCapacity) { TEST_F(FifoSampleBufferTest, readWriteTrim) { FifoSampleBuffer sampleBuffer(kCapacity); - SINT writeCount1 = write(&sampleBuffer, kCapacity + 10); + SINT writeCount1 = writeTail(&sampleBuffer, kCapacity + 10); EXPECT_EQ(writeCount1, kCapacity); // buffer is full EXPECT_FALSE(sampleBuffer.isEmpty()); - SINT readCount1 = readAndVerify(&sampleBuffer, kCapacity - 10); + SINT readCount1 = readHeadAndVerify(&sampleBuffer, kCapacity - 10); EXPECT_EQ(readCount1, kCapacity - 10); EXPECT_FALSE(sampleBuffer.isEmpty()); - SINT writeCount2 = write(&sampleBuffer, kCapacity); + SINT writeCount2 = writeTail(&sampleBuffer, kCapacity); EXPECT_EQ(writeCount2, readCount1); // buffer has been refilled EXPECT_FALSE(sampleBuffer.isEmpty()); - SINT readCount2 = readAndVerify(&sampleBuffer, kCapacity - 10); + SINT readCount2 = readHeadAndVerify(&sampleBuffer, kCapacity - 10); EXPECT_EQ(readCount2, kCapacity - 10); EXPECT_FALSE(sampleBuffer.isEmpty()); sampleBuffer.trim(); - SINT writeCount3 = write(&sampleBuffer, kCapacity); + SINT writeCount3 = writeTail(&sampleBuffer, kCapacity); EXPECT_EQ(writeCount3, readCount2); // buffer has been refilled EXPECT_FALSE(sampleBuffer.isEmpty()); - SINT readCount3 = readAndVerify(&sampleBuffer, kCapacity + 10); + SINT readCount3 = readHeadAndVerify(&sampleBuffer, kCapacity + 10); EXPECT_EQ(readCount3, kCapacity); // whole buffer has been read EXPECT_TRUE(sampleBuffer.isEmpty()); } @@ -124,25 +126,25 @@ TEST_F(FifoSampleBufferTest, readWriteTrim) { TEST_F(FifoSampleBufferTest, shrink) { FifoSampleBuffer sampleBuffer(kCapacity); - SINT writeCount1 = write(&sampleBuffer, kCapacity - 10); + SINT writeCount1 = writeTail(&sampleBuffer, kCapacity - 10); EXPECT_EQ(writeCount1, kCapacity - 10); EXPECT_FALSE(sampleBuffer.isEmpty()); - SINT shrinkCount1 = shrinkTail(&sampleBuffer, 10); + SINT shrinkCount1 = readTailAndVerify(&sampleBuffer, 10); EXPECT_EQ(shrinkCount1, 10); - SINT readCount1 = readAndVerify(&sampleBuffer, 10); + SINT readCount1 = readHeadAndVerify(&sampleBuffer, 10); EXPECT_EQ(readCount1, 10); EXPECT_FALSE(sampleBuffer.isEmpty()); - SINT readCount2 = readAndVerify(&sampleBuffer, kCapacity - 40); + SINT readCount2 = readHeadAndVerify(&sampleBuffer, kCapacity - 40); EXPECT_EQ(readCount2, kCapacity - 40); EXPECT_FALSE(sampleBuffer.isEmpty()); - SINT readCount3 = readAndVerify(&sampleBuffer, 20); + SINT readCount3 = readHeadAndVerify(&sampleBuffer, 20); EXPECT_EQ(readCount3, 10); EXPECT_TRUE(sampleBuffer.isEmpty()); - SINT writeCount2 = write(&sampleBuffer, 20); + SINT writeCount2 = writeTail(&sampleBuffer, 20); EXPECT_EQ(writeCount2, 20); EXPECT_FALSE(sampleBuffer.isEmpty()); - SINT shrinkCount2 = shrinkTail(&sampleBuffer, 21); + SINT shrinkCount2 = readTailAndVerify(&sampleBuffer, 21); EXPECT_EQ(shrinkCount2, 20); EXPECT_TRUE(sampleBuffer.isEmpty()); } @@ -150,12 +152,12 @@ TEST_F(FifoSampleBufferTest, shrink) { TEST_F(FifoSampleBufferTest, reset) { FifoSampleBuffer sampleBuffer(kCapacity); - SINT writeCount = write(&sampleBuffer, 10); + SINT writeCount = writeTail(&sampleBuffer, 10); EXPECT_EQ(writeCount, 10); EXPECT_FALSE(sampleBuffer.isEmpty()); reset(&sampleBuffer); EXPECT_TRUE(sampleBuffer.isEmpty()); - SINT readCount = readAndVerify(&sampleBuffer, 10); + SINT readCount = readHeadAndVerify(&sampleBuffer, 10); EXPECT_EQ(readCount, 0); EXPECT_TRUE(sampleBuffer.isEmpty()); } From 4cc8c14293aadcfe104efff22763f8f44a5ac04d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 11 Mar 2015 18:32:27 +0100 Subject: [PATCH 235/481] Rename FifoSampleBuffer to CircularSampleBuffer --- build/depends.py | 2 +- plugins/soundsourcem4a/SConscript | 2 +- plugins/soundsourcem4a/soundsourcem4a.cpp | 2 +- plugins/soundsourcem4a/soundsourcem4a.h | 4 +- src/circularsamplebuffer.cpp | 114 ++++++++++++++++++ ...osamplebuffer.h => circularsamplebuffer.h} | 56 ++++++--- src/fifosamplebuffer.cpp | 95 --------------- src/musicbrainz/chromaprinter.cpp | 2 +- src/sources/soundsourceflac.cpp | 2 +- src/sources/soundsourceflac.h | 5 +- ...test.cpp => circularsamplebuffer_test.cpp} | 54 +++++---- 11 files changed, 191 insertions(+), 147 deletions(-) create mode 100644 src/circularsamplebuffer.cpp rename src/{fifosamplebuffer.h => circularsamplebuffer.h} (51%) delete mode 100644 src/fifosamplebuffer.cpp rename src/test/{fifosamplebuffer_test.cpp => circularsamplebuffer_test.cpp} (74%) diff --git a/build/depends.py b/build/depends.py index 25d2438e39c..a7b616fb701 100644 --- a/build/depends.py +++ b/build/depends.py @@ -869,7 +869,7 @@ def sources(self, build): "sampleutil.cpp", "samplebuffer.cpp", - "fifosamplebuffer.cpp", + "circularsamplebuffer.cpp", "trackinfoobject.cpp", "track/beatgrid.cpp", diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index 4a0313251b8..ce869304c08 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -16,7 +16,7 @@ m4a_sources = [ "sources/soundsourceplugin.cpp", "sources/audiosource.cpp", "samplebuffer.cpp", - "fifosamplebuffer.cpp", + "circularsamplebuffer.cpp", "sampleutil.cpp", "metadata/trackmetadata.cpp", "metadata/trackmetadatataglib.cpp" diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index cc797c6d09e..553e2385db8 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -197,7 +197,7 @@ Result SoundSourceM4A::tryOpen(SINT channelCountHint) { // Resize temporary buffer for decoded sample data const SINT sampleBufferCapacity = frames2samples(kFramesPerSampleBlock); - FifoSampleBuffer(sampleBufferCapacity).swap(m_sampleBuffer); + CircularSampleBuffer(sampleBufferCapacity).swap(m_sampleBuffer); // Invalidate current position to enforce the following // seek operation diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 438bfe366c8..05768cdca00 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -1,9 +1,9 @@ #ifndef MIXXX_SOUNDSOURCEM4A_H #define MIXXX_SOUNDSOURCEM4A_H +#include "../../src/circularsamplebuffer.h" #include "sources/soundsourceplugin.h" -#include "fifosamplebuffer.h" #ifdef __MP4V2__ #include @@ -57,7 +57,7 @@ class SoundSourceM4A: public SoundSourcePlugin { NeAACDecHandle m_hDecoder; - FifoSampleBuffer m_sampleBuffer; + CircularSampleBuffer m_sampleBuffer; SINT m_curFrameIndex; }; diff --git a/src/circularsamplebuffer.cpp b/src/circularsamplebuffer.cpp new file mode 100644 index 00000000000..6533389fb66 --- /dev/null +++ b/src/circularsamplebuffer.cpp @@ -0,0 +1,114 @@ +#include +#include "sampleutil.h" + +#define DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer \ + DEBUG_ASSERT(m_primaryBuffer.size() == m_secondaryBuffer.size()); \ + DEBUG_ASSERT(0 <= m_headOffset); \ + DEBUG_ASSERT(m_headOffset <= m_tailOffset); \ + DEBUG_ASSERT(m_tailOffset <= m_primaryBuffer.size()); \ + DEBUG_ASSERT(!isEmpty() || (0 == m_headOffset)); \ + DEBUG_ASSERT(!isEmpty() || (0 == m_tailOffset)) + +CircularSampleBuffer::CircularSampleBuffer() + : m_headOffset(0), + m_tailOffset(0) { + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; +} + +CircularSampleBuffer::CircularSampleBuffer(SINT capacity) + : m_primaryBuffer(capacity), + m_secondaryBuffer(capacity), + m_headOffset(0), + m_tailOffset(0) { + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; +} + +void CircularSampleBuffer::swap(CircularSampleBuffer& other) { + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; + + std::swap(m_primaryBuffer, other.m_primaryBuffer); + std::swap(m_secondaryBuffer, other.m_secondaryBuffer); + std::swap(m_headOffset, other.m_headOffset); + std::swap(m_tailOffset, other.m_tailOffset); + + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; +} + +void CircularSampleBuffer::swapBuffers() { + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; + + // SampleUtil::copy() requires that the source and destination + // memory regions are disjunct. Double-buffering is necessary + // to satisfy this precondition. + SampleUtil::copy( + m_secondaryBuffer.data(), + m_primaryBuffer.data() + m_headOffset, + getSize()); + m_primaryBuffer.swap(m_secondaryBuffer); + // shift offsets + m_tailOffset -= m_headOffset; + m_headOffset = 0; + + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; +} + +void CircularSampleBuffer::reset() { + m_headOffset = 0; + m_tailOffset = 0; +} + +void CircularSampleBuffer::trim() { + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; + + if (0 < m_headOffset) { + if (isEmpty()) { + reset(); + } else { + swapBuffers(); + } + } + + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; +} + +std::pair CircularSampleBuffer::growTail(SINT size) { + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; + + CSAMPLE* const tailData = m_primaryBuffer.data() + m_tailOffset; + const SINT tailSize = math_min(size, getTailCapacity()); + m_tailOffset += tailSize; + + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; + return std::make_pair(tailData, tailSize); +} + +std::pair CircularSampleBuffer::shrinkTail(SINT size) { + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; + + const SINT tailSize = math_min(size, getSize()); + m_tailOffset -= tailSize; + const CSAMPLE* const tailData = m_primaryBuffer.data() + m_tailOffset; + if (tailSize == getSize()) { + DEBUG_ASSERT(isEmpty()); + reset(); + } + + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; + return std::make_pair(tailData, tailSize); +} + +std::pair CircularSampleBuffer::shrinkHead(SINT size) { + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; + + const CSAMPLE* const headData = m_primaryBuffer.data() + m_headOffset; + const SINT headSize = math_min(size, getSize()); + if (headSize == getSize()) { + // buffer becomes empty + reset(); + } else { + m_headOffset += headSize; + } + + DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; + return std::make_pair(headData, headSize); +} diff --git a/src/fifosamplebuffer.h b/src/circularsamplebuffer.h similarity index 51% rename from src/fifosamplebuffer.h rename to src/circularsamplebuffer.h index 4ecd99df74a..f8d6e2123b1 100644 --- a/src/fifosamplebuffer.h +++ b/src/circularsamplebuffer.h @@ -3,30 +3,45 @@ #include "samplebuffer.h" -// A FIFO/LIFO sample buffer with a fixed capacity, range checking, +// A circular FIFO/LIFO sample buffer with fixed capacity, range checking, // and double-buffering. // -// Maximum performance is achieved when consuming all buffered -// samples before the capacity is exhausted. This use case does -// not require any internal copying/moving of buffered samples. -class FifoSampleBuffer { - Q_DISABLE_COPY(FifoSampleBuffer); +// Maximum performance is achieved when consuming all buffered samples +// before the capacity is exhausted. This use case does not require any +// internal copying/moving of buffered samples. Otherwise the buffer +// needs to be trimmed when running out of tail capacity. +// +// This class is not thread-safe and not intended to be used from multiple +// threads! +class CircularSampleBuffer { + Q_DISABLE_COPY(CircularSampleBuffer); public: - FifoSampleBuffer(); - explicit FifoSampleBuffer(SINT capacity); + CircularSampleBuffer(); + explicit CircularSampleBuffer(SINT capacity); + + void swap(CircularSampleBuffer& other); - void swap(FifoSampleBuffer& other); + // The initial/total capacity of the buffer. + SINT getCapacity() const { + return m_primaryBuffer.size(); + } + + // The capacity at the tail that is immediately available + // without trimming the buffer. + SINT getTailCapacity() const { + return getCapacity() - m_tailOffset; + } bool isEmpty() const { - return m_writeOffset <= m_readOffset; + return m_tailOffset <= m_headOffset; } // Returns the current size of the buffer, i.e. the number of // buffered samples that have been written and are available // for reading. SINT getSize() const { - return m_writeOffset - m_readOffset; + return m_tailOffset - m_headOffset; } // Discards all buffered samples and resets the buffer to its @@ -35,15 +50,16 @@ class FifoSampleBuffer { // Moves all buffered samples to the beginning of the internal buffer. // - // This function will be called implicitly by growTail() when needed. + // This will increase the free capacity at the tail returned by + // getTailCapacity() to the maximum amount getCapacity() - getSize(). void trim(); // Reserves space at the buffer's tail for writing samples. The internal // buffer is trimmed if necessary to provide a continuous memory region. // // Returns a pointer to the continuous memory region and the actual number - // of samples that have been reserved. The pointer is valid for writing as - // long as no modifying member function is called! + // of samples that have been reserved. The maximum growth is limited by + // getTailCapacity() and might be increased by calling trim(). std::pair growTail(SINT size); // Shrinks the buffer from the tail for reading buffered samples. @@ -63,18 +79,18 @@ class FifoSampleBuffer { private: void swapBuffers(); - SampleBuffer m_sampleBuffer; - SampleBuffer m_shadowBuffer; - SINT m_readOffset; - SINT m_writeOffset; + SampleBuffer m_primaryBuffer; + SampleBuffer m_secondaryBuffer; + SINT m_headOffset; + SINT m_tailOffset; }; namespace std { -// Template specialization of std::swap for FifoSampleBuffer. +// Template specialization of std::swap for CircularSampleBuffer. template<> -inline void swap(FifoSampleBuffer& lhs, FifoSampleBuffer& rhs) { +inline void swap(CircularSampleBuffer& lhs, CircularSampleBuffer& rhs) { lhs.swap(rhs); } diff --git a/src/fifosamplebuffer.cpp b/src/fifosamplebuffer.cpp deleted file mode 100644 index 1dee51c0197..00000000000 --- a/src/fifosamplebuffer.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "fifosamplebuffer.h" - -#include "sampleutil.h" - -#define DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer() \ - DEBUG_ASSERT(0 <= m_readOffset); \ - DEBUG_ASSERT(m_readOffset <= m_writeOffset); \ - DEBUG_ASSERT(m_writeOffset <= m_sampleBuffer.size()); \ - DEBUG_ASSERT(m_sampleBuffer.size() == m_shadowBuffer.size()) - -FifoSampleBuffer::FifoSampleBuffer() - : m_readOffset(0), - m_writeOffset(0) { - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); -} - -FifoSampleBuffer::FifoSampleBuffer(SINT capacity) - : m_sampleBuffer(capacity), - m_shadowBuffer(capacity), - m_readOffset(0), - m_writeOffset(0) { - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); -} - -void FifoSampleBuffer::swap(FifoSampleBuffer& other) { - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - std::swap(m_sampleBuffer, other.m_sampleBuffer); - std::swap(m_shadowBuffer, other.m_shadowBuffer); - std::swap(m_readOffset, other.m_readOffset); - std::swap(m_writeOffset, other.m_writeOffset); - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); -} - -void FifoSampleBuffer::swapBuffers() { - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - SampleUtil::copy( - m_shadowBuffer.data(), - m_sampleBuffer.data() + m_readOffset, - m_writeOffset - m_readOffset); - m_sampleBuffer.swap(m_shadowBuffer); - m_writeOffset -= m_readOffset; - m_readOffset = 0; - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); -} - -void FifoSampleBuffer::reset() { - m_readOffset = 0; - m_writeOffset = 0; -} - -void FifoSampleBuffer::trim() { - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - if (0 < m_readOffset) { - if (isEmpty()) { - reset(); - } else { - swapBuffers(); - } - } - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); -} - -std::pair FifoSampleBuffer::growTail(SINT size) { - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - if (isEmpty()) { - reset(); - } else { - if ((m_sampleBuffer.size() - m_writeOffset) < size) { - trim(); - } - } - CSAMPLE* const resultData = m_sampleBuffer.data() + m_writeOffset; - const SINT resultSize = math_min(size, m_sampleBuffer.size() - m_writeOffset); - m_writeOffset += resultSize; - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - return std::make_pair(resultData, resultSize); -} - -std::pair FifoSampleBuffer::shrinkTail(SINT size) { - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - const SINT resultSize = math_min(size, getSize()); - m_writeOffset -= resultSize; - const CSAMPLE* const resultData = m_sampleBuffer.data() + m_writeOffset; - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - return std::make_pair(resultData, resultSize); -} - -std::pair FifoSampleBuffer::shrinkHead(SINT size) { - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - const CSAMPLE* const resultData = m_sampleBuffer.data() + m_readOffset; - const SINT resultSize = math_min(size, getSize()); - m_readOffset += resultSize; - DEBUG_ASSERT_CLASS_INVARIANT_FifoSampleBuffer(); - return std::make_pair(resultData, resultSize); -} diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index e0fc928d081..f6b01653ebc 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -50,7 +50,7 @@ namespace // Convert floating-point to integer SampleUtil::convertFloat32ToS16( &fingerprintSamples[0], - &sampleBuffer[0], + sampleBuffer.data(), fingerprintSamples.size()); qDebug("reading file took: %d ms" , timerReadingFile.elapsed()); diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 9e877c893e3..a2843639a1f 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -384,7 +384,7 @@ void SoundSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { m_maxFramesize = metadata->data.stream_info.max_framesize; const SINT sampleBufferCapacity = m_maxBlocksize * getChannelCount(); - FifoSampleBuffer(sampleBufferCapacity).swap(m_sampleBuffer); + CircularSampleBuffer(sampleBufferCapacity).swap(m_sampleBuffer); break; } default: diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index 08771c03da2..01e45aec8a1 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -3,11 +3,10 @@ #include "sources/soundsource.h" -#include "fifosamplebuffer.h" - #include #include +#include "../circularsamplebuffer.h" namespace Mixxx { @@ -61,7 +60,7 @@ class SoundSourceFLAC: public SoundSource { CSAMPLE m_sampleScaleFactor; - FifoSampleBuffer m_sampleBuffer; + CircularSampleBuffer m_sampleBuffer; SINT m_curFrameIndex; }; diff --git a/src/test/fifosamplebuffer_test.cpp b/src/test/circularsamplebuffer_test.cpp similarity index 74% rename from src/test/fifosamplebuffer_test.cpp rename to src/test/circularsamplebuffer_test.cpp index 862526086ee..4c3dd764b37 100644 --- a/src/test/fifosamplebuffer_test.cpp +++ b/src/test/circularsamplebuffer_test.cpp @@ -1,14 +1,13 @@ +#include #include "test/mixxxtest.h" -#include "fifosamplebuffer.h" - #include #include -class FifoSampleBufferTest: public MixxxTest { +class CircularSampleBufferTest: public MixxxTest { public: - FifoSampleBufferTest() + CircularSampleBufferTest() : m_writeValue(CSAMPLE_ZERO), m_readValue(CSAMPLE_ZERO) { } @@ -16,7 +15,7 @@ class FifoSampleBufferTest: public MixxxTest { protected: static const SINT kCapacity; - SINT writeTail(FifoSampleBuffer* pSampleBuffer, SINT size) { + SINT writeTail(CircularSampleBuffer* pSampleBuffer, SINT size) { const std::pair writeBuffer( pSampleBuffer->growTail(size)); for (SINT i = 0; i < writeBuffer.second; ++i) { @@ -26,7 +25,7 @@ class FifoSampleBufferTest: public MixxxTest { return writeBuffer.second; } - SINT readHeadAndVerify(FifoSampleBuffer* pSampleBuffer, SINT size) { + SINT readHeadAndVerify(CircularSampleBuffer* pSampleBuffer, SINT size) { const std::pairreadBuffer( pSampleBuffer->shrinkHead(size)); for (SINT i = 0; i < readBuffer.second; ++i) { @@ -36,7 +35,7 @@ class FifoSampleBufferTest: public MixxxTest { return readBuffer.second; } - SINT readTailAndVerify(FifoSampleBuffer* pSampleBuffer, SINT size) { + SINT readTailAndVerify(CircularSampleBuffer* pSampleBuffer, SINT size) { const std::pairreadBuffer( pSampleBuffer->shrinkTail(size)); for (SINT i = readBuffer.second; i-- > 0; ) { @@ -46,7 +45,7 @@ class FifoSampleBufferTest: public MixxxTest { return readBuffer.second; } - void reset(FifoSampleBuffer* pSampleBuffer) { + void reset(CircularSampleBuffer* pSampleBuffer) { pSampleBuffer->reset(); m_writeValue = CSAMPLE_ZERO; m_readValue = CSAMPLE_ZERO; @@ -57,10 +56,10 @@ class FifoSampleBufferTest: public MixxxTest { CSAMPLE m_readValue; }; -const SINT FifoSampleBufferTest::kCapacity = 100; +const SINT CircularSampleBufferTest::kCapacity = 100; -TEST_F(FifoSampleBufferTest, emptyWithoutCapacity) { - FifoSampleBuffer sampleBuffer; +TEST_F(CircularSampleBufferTest, emptyWithoutCapacity) { + CircularSampleBuffer sampleBuffer; EXPECT_TRUE(sampleBuffer.isEmpty()); sampleBuffer.trim(); @@ -82,8 +81,8 @@ TEST_F(FifoSampleBufferTest, emptyWithoutCapacity) { EXPECT_TRUE(sampleBuffer.isEmpty()); } -TEST_F(FifoSampleBufferTest, emptyWithCapacity) { - FifoSampleBuffer sampleBuffer(kCapacity); +TEST_F(CircularSampleBufferTest, emptyWithCapacity) { + CircularSampleBuffer sampleBuffer(kCapacity); EXPECT_TRUE(sampleBuffer.isEmpty()); sampleBuffer.trim(); @@ -93,29 +92,40 @@ TEST_F(FifoSampleBufferTest, emptyWithCapacity) { EXPECT_TRUE(sampleBuffer.isEmpty()); } -TEST_F(FifoSampleBufferTest, readWriteTrim) { - FifoSampleBuffer sampleBuffer(kCapacity); +TEST_F(CircularSampleBufferTest, readWriteTrim) { + CircularSampleBuffer sampleBuffer(kCapacity); SINT writeCount1 = writeTail(&sampleBuffer, kCapacity + 10); EXPECT_EQ(writeCount1, kCapacity); // buffer is full EXPECT_FALSE(sampleBuffer.isEmpty()); + EXPECT_EQ(sampleBuffer.getTailCapacity(), 0); SINT readCount1 = readHeadAndVerify(&sampleBuffer, kCapacity - 10); EXPECT_EQ(readCount1, kCapacity - 10); EXPECT_FALSE(sampleBuffer.isEmpty()); + EXPECT_EQ(sampleBuffer.getTailCapacity(), 0); SINT writeCount2 = writeTail(&sampleBuffer, kCapacity); - EXPECT_EQ(writeCount2, readCount1); // buffer has been refilled + EXPECT_EQ(writeCount2, 0); // no tail capacity left + EXPECT_FALSE(sampleBuffer.isEmpty()); + + sampleBuffer.trim(); + EXPECT_EQ(sampleBuffer.getTailCapacity(), readCount1); + + SINT writeCount3 = writeTail(&sampleBuffer, kCapacity); + EXPECT_EQ(writeCount3, readCount1); // buffer has been refilled EXPECT_FALSE(sampleBuffer.isEmpty()); SINT readCount2 = readHeadAndVerify(&sampleBuffer, kCapacity - 10); EXPECT_EQ(readCount2, kCapacity - 10); EXPECT_FALSE(sampleBuffer.isEmpty()); + EXPECT_EQ(sampleBuffer.getTailCapacity(), 0); sampleBuffer.trim(); + EXPECT_EQ(sampleBuffer.getTailCapacity(), readCount2); - SINT writeCount3 = writeTail(&sampleBuffer, kCapacity); - EXPECT_EQ(writeCount3, readCount2); // buffer has been refilled + SINT writeCount4 = writeTail(&sampleBuffer, kCapacity); + EXPECT_EQ(writeCount4, readCount2); // buffer has been refilled EXPECT_FALSE(sampleBuffer.isEmpty()); SINT readCount3 = readHeadAndVerify(&sampleBuffer, kCapacity + 10); @@ -123,8 +133,8 @@ TEST_F(FifoSampleBufferTest, readWriteTrim) { EXPECT_TRUE(sampleBuffer.isEmpty()); } -TEST_F(FifoSampleBufferTest, shrink) { - FifoSampleBuffer sampleBuffer(kCapacity); +TEST_F(CircularSampleBufferTest, shrink) { + CircularSampleBuffer sampleBuffer(kCapacity); SINT writeCount1 = writeTail(&sampleBuffer, kCapacity - 10); EXPECT_EQ(writeCount1, kCapacity - 10); @@ -149,8 +159,8 @@ TEST_F(FifoSampleBufferTest, shrink) { EXPECT_TRUE(sampleBuffer.isEmpty()); } -TEST_F(FifoSampleBufferTest, reset) { - FifoSampleBuffer sampleBuffer(kCapacity); +TEST_F(CircularSampleBufferTest, reset) { + CircularSampleBuffer sampleBuffer(kCapacity); SINT writeCount = writeTail(&sampleBuffer, 10); EXPECT_EQ(writeCount, 10); From a3b9f455ecd6c50636cd7afcbae2ad2fb91bc6ab Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 12 Mar 2015 18:21:56 +0100 Subject: [PATCH 236/481] Split a simplified SingularSampleBuffer class from CircularSampleBuffer Double-buffering is not needed for temporary buffering of decoded audio data. --- build/depends.py | 1 + plugins/soundsourcem4a/SConscript | 2 +- plugins/soundsourcem4a/soundsourcem4a.cpp | 2 +- plugins/soundsourcem4a/soundsourcem4a.h | 4 +- src/circularsamplebuffer.cpp | 81 +++-------------- src/circularsamplebuffer.h | 76 ++-------------- src/singularsamplebuffer.cpp | 76 ++++++++++++++++ src/singularsamplebuffer.h | 87 +++++++++++++++++++ src/sources/soundsourceflac.cpp | 2 +- src/sources/soundsourceflac.h | 5 +- ...lebuffer_test.cpp => samplebuffertest.cpp} | 2 +- 11 files changed, 195 insertions(+), 143 deletions(-) create mode 100644 src/singularsamplebuffer.cpp create mode 100644 src/singularsamplebuffer.h rename src/test/{circularsamplebuffer_test.cpp => samplebuffertest.cpp} (99%) diff --git a/build/depends.py b/build/depends.py index a7b616fb701..29e40e0d08b 100644 --- a/build/depends.py +++ b/build/depends.py @@ -869,6 +869,7 @@ def sources(self, build): "sampleutil.cpp", "samplebuffer.cpp", + "singularsamplebuffer.cpp", "circularsamplebuffer.cpp", "trackinfoobject.cpp", diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index ce869304c08..68ad241042d 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -16,7 +16,7 @@ m4a_sources = [ "sources/soundsourceplugin.cpp", "sources/audiosource.cpp", "samplebuffer.cpp", - "circularsamplebuffer.cpp", + "singularsamplebuffer.cpp", "sampleutil.cpp", "metadata/trackmetadata.cpp", "metadata/trackmetadatataglib.cpp" diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 553e2385db8..b66b31996ce 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -197,7 +197,7 @@ Result SoundSourceM4A::tryOpen(SINT channelCountHint) { // Resize temporary buffer for decoded sample data const SINT sampleBufferCapacity = frames2samples(kFramesPerSampleBlock); - CircularSampleBuffer(sampleBufferCapacity).swap(m_sampleBuffer); + m_sampleBuffer.resetCapacity(sampleBufferCapacity); // Invalidate current position to enforce the following // seek operation diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 05768cdca00..df614553fc3 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -1,9 +1,9 @@ #ifndef MIXXX_SOUNDSOURCEM4A_H #define MIXXX_SOUNDSOURCEM4A_H -#include "../../src/circularsamplebuffer.h" #include "sources/soundsourceplugin.h" +#include "singularsamplebuffer.h" #ifdef __MP4V2__ #include @@ -57,7 +57,7 @@ class SoundSourceM4A: public SoundSourcePlugin { NeAACDecHandle m_hDecoder; - CircularSampleBuffer m_sampleBuffer; + SingularSampleBuffer m_sampleBuffer; SINT m_curFrameIndex; }; diff --git a/src/circularsamplebuffer.cpp b/src/circularsamplebuffer.cpp index 6533389fb66..542f2274f3d 100644 --- a/src/circularsamplebuffer.cpp +++ b/src/circularsamplebuffer.cpp @@ -1,35 +1,27 @@ -#include -#include "sampleutil.h" +#include "circularsamplebuffer.h" -#define DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer \ - DEBUG_ASSERT(m_primaryBuffer.size() == m_secondaryBuffer.size()); \ - DEBUG_ASSERT(0 <= m_headOffset); \ - DEBUG_ASSERT(m_headOffset <= m_tailOffset); \ - DEBUG_ASSERT(m_tailOffset <= m_primaryBuffer.size()); \ - DEBUG_ASSERT(!isEmpty() || (0 == m_headOffset)); \ - DEBUG_ASSERT(!isEmpty() || (0 == m_tailOffset)) +#include "sampleutil.h" -CircularSampleBuffer::CircularSampleBuffer() - : m_headOffset(0), - m_tailOffset(0) { +CircularSampleBuffer::CircularSampleBuffer() { DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; } CircularSampleBuffer::CircularSampleBuffer(SINT capacity) - : m_primaryBuffer(capacity), - m_secondaryBuffer(capacity), - m_headOffset(0), - m_tailOffset(0) { + : SingularSampleBuffer(capacity), + m_secondaryBuffer(capacity) { DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; } -void CircularSampleBuffer::swap(CircularSampleBuffer& other) { +void CircularSampleBuffer::resetCapacity(SINT capacity) { DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - std::swap(m_primaryBuffer, other.m_primaryBuffer); - std::swap(m_secondaryBuffer, other.m_secondaryBuffer); - std::swap(m_headOffset, other.m_headOffset); - std::swap(m_tailOffset, other.m_tailOffset); + if (m_secondaryBuffer.size() != capacity) { + SampleBuffer secondaryBuffer(capacity); + SingularSampleBuffer::resetCapacity(capacity); + secondaryBuffer.swap(m_secondaryBuffer); + } else { + reset(); + } DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; } @@ -52,11 +44,6 @@ void CircularSampleBuffer::swapBuffers() { DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; } -void CircularSampleBuffer::reset() { - m_headOffset = 0; - m_tailOffset = 0; -} - void CircularSampleBuffer::trim() { DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; @@ -70,45 +57,3 @@ void CircularSampleBuffer::trim() { DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; } - -std::pair CircularSampleBuffer::growTail(SINT size) { - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - - CSAMPLE* const tailData = m_primaryBuffer.data() + m_tailOffset; - const SINT tailSize = math_min(size, getTailCapacity()); - m_tailOffset += tailSize; - - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - return std::make_pair(tailData, tailSize); -} - -std::pair CircularSampleBuffer::shrinkTail(SINT size) { - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - - const SINT tailSize = math_min(size, getSize()); - m_tailOffset -= tailSize; - const CSAMPLE* const tailData = m_primaryBuffer.data() + m_tailOffset; - if (tailSize == getSize()) { - DEBUG_ASSERT(isEmpty()); - reset(); - } - - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - return std::make_pair(tailData, tailSize); -} - -std::pair CircularSampleBuffer::shrinkHead(SINT size) { - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - - const CSAMPLE* const headData = m_primaryBuffer.data() + m_headOffset; - const SINT headSize = math_min(size, getSize()); - if (headSize == getSize()) { - // buffer becomes empty - reset(); - } else { - m_headOffset += headSize; - } - - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - return std::make_pair(headData, headSize); -} diff --git a/src/circularsamplebuffer.h b/src/circularsamplebuffer.h index f8d6e2123b1..3c05b4026a8 100644 --- a/src/circularsamplebuffer.h +++ b/src/circularsamplebuffer.h @@ -1,7 +1,7 @@ -#ifndef FIFOSAMPLEBUFFER_H -#define FIFOSAMPLEBUFFER_H +#ifndef CIRCULARSAMPLEBUFFER_H +#define CIRCULARSAMPLEBUFFER_H -#include "samplebuffer.h" +#include "singularsamplebuffer.h" // A circular FIFO/LIFO sample buffer with fixed capacity, range checking, // and double-buffering. @@ -13,40 +13,14 @@ // // This class is not thread-safe and not intended to be used from multiple // threads! -class CircularSampleBuffer { +class CircularSampleBuffer: public SingularSampleBuffer { Q_DISABLE_COPY(CircularSampleBuffer); public: CircularSampleBuffer(); explicit CircularSampleBuffer(SINT capacity); - void swap(CircularSampleBuffer& other); - - // The initial/total capacity of the buffer. - SINT getCapacity() const { - return m_primaryBuffer.size(); - } - - // The capacity at the tail that is immediately available - // without trimming the buffer. - SINT getTailCapacity() const { - return getCapacity() - m_tailOffset; - } - - bool isEmpty() const { - return m_tailOffset <= m_headOffset; - } - - // Returns the current size of the buffer, i.e. the number of - // buffered samples that have been written and are available - // for reading. - SINT getSize() const { - return m_tailOffset - m_headOffset; - } - - // Discards all buffered samples and resets the buffer to its - // initial state. - void reset(); + void resetCapacity(SINT capacity) /*override*/; // Moves all buffered samples to the beginning of the internal buffer. // @@ -54,46 +28,14 @@ class CircularSampleBuffer { // getTailCapacity() to the maximum amount getCapacity() - getSize(). void trim(); - // Reserves space at the buffer's tail for writing samples. The internal - // buffer is trimmed if necessary to provide a continuous memory region. - // - // Returns a pointer to the continuous memory region and the actual number - // of samples that have been reserved. The maximum growth is limited by - // getTailCapacity() and might be increased by calling trim(). - std::pair growTail(SINT size); - - // Shrinks the buffer from the tail for reading buffered samples. - // - // Returns a pointer to the continuous memory region and the actual - // number of buffered samples that have been dropped. The pointer is - // valid for reading as long as no modifying member function is called! - std::pair shrinkTail(SINT size); - - // Shrinks the buffer from the head for reading buffered samples. - // - // Returns a pointer to the continuous memory region and the actual - // number of buffered samples that have been dropped. The pointer is - // valid for reading as long as no modifying member function is called! - std::pair shrinkHead(SINT size); - private: void swapBuffers(); - SampleBuffer m_primaryBuffer; SampleBuffer m_secondaryBuffer; - SINT m_headOffset; - SINT m_tailOffset; }; -namespace std -{ - -// Template specialization of std::swap for CircularSampleBuffer. -template<> -inline void swap(CircularSampleBuffer& lhs, CircularSampleBuffer& rhs) { - lhs.swap(rhs); -} - -} +#define DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer \ + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; \ + DEBUG_ASSERT(m_primaryBuffer.size() == m_secondaryBuffer.size()); \ -#endif // FIFOSAMPLEBUFFER_H +#endif // CIRCULARSAMPLEBUFFER_H diff --git a/src/singularsamplebuffer.cpp b/src/singularsamplebuffer.cpp new file mode 100644 index 00000000000..8906b8a972f --- /dev/null +++ b/src/singularsamplebuffer.cpp @@ -0,0 +1,76 @@ +#include "singularsamplebuffer.h" + +SingularSampleBuffer::SingularSampleBuffer() + : m_headOffset(0), + m_tailOffset(0) { + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; +} + +SingularSampleBuffer::SingularSampleBuffer(SINT capacity) + : m_primaryBuffer(capacity), + m_headOffset(0), + m_tailOffset(0) { + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; +} + +void SingularSampleBuffer::reset() { + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; + + m_headOffset = 0; + m_tailOffset = 0; + + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; +} + +void SingularSampleBuffer::resetCapacity(SINT capacity) { + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; + + if (m_primaryBuffer.size() != capacity) { + SampleBuffer(capacity).swap(m_primaryBuffer); + } + reset(); + + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; +} + +std::pair SingularSampleBuffer::growTail(SINT size) { + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; + + CSAMPLE* const tailData = m_primaryBuffer.data() + m_tailOffset; + const SINT tailSize = math_min(size, getTailCapacity()); + m_tailOffset += tailSize; + + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; + return std::make_pair(tailData, tailSize); +} + +std::pair SingularSampleBuffer::shrinkTail(SINT size) { + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; + + const SINT tailSize = math_min(size, getSize()); + m_tailOffset -= tailSize; + const CSAMPLE* const tailData = m_primaryBuffer.data() + m_tailOffset; + if (tailSize == getSize()) { + DEBUG_ASSERT(isEmpty()); + reset(); + } + + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; + return std::make_pair(tailData, tailSize); +} + +std::pair SingularSampleBuffer::shrinkHead(SINT size) { + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; + + const CSAMPLE* const headData = m_primaryBuffer.data() + m_headOffset; + const SINT headSize = math_min(size, getSize()); + if (headSize == getSize()) { + // buffer becomes empty + reset(); + } else { + m_headOffset += headSize; + } + + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; + return std::make_pair(headData, headSize); +} diff --git a/src/singularsamplebuffer.h b/src/singularsamplebuffer.h new file mode 100644 index 00000000000..58e60090b95 --- /dev/null +++ b/src/singularsamplebuffer.h @@ -0,0 +1,87 @@ +#ifndef SINGULARSAMPLEBUFFER_H +#define SINGULARSAMPLEBUFFER_H + +#include "samplebuffer.h" + +// A singular FIFO/LIFO sample buffer with fixed capacity and range +// checking. +// +// Common use case: Consume all buffered samples before the capacity +// is exhausted. +// +// This class is not thread-safe and not intended to be used from multiple +// threads! +class SingularSampleBuffer { + Q_DISABLE_COPY(SingularSampleBuffer); + +public: + SingularSampleBuffer(); + explicit SingularSampleBuffer(SINT capacity); + virtual ~SingularSampleBuffer() {} + + // The initial/total capacity of the buffer. + SINT getCapacity() const { + return m_primaryBuffer.size(); + } + + // The capacity at the tail that is immediately available + // without trimming the buffer. + SINT getTailCapacity() const { + return getCapacity() - m_tailOffset; + } + + bool isEmpty() const { + return m_tailOffset <= m_headOffset; + } + + // Returns the current size of the buffer, i.e. the number of + // buffered samples that have been written and are available + // for reading. + SINT getSize() const { + return m_tailOffset - m_headOffset; + } + + // Discards all buffered samples and resets the buffer to its + // initial state. + void reset(); + + // Discards all buffered samples and resets the buffer to its + // initial state with a new capacity + virtual void resetCapacity(SINT capacity); + + // Reserves space at the buffer's tail for writing samples. The internal + // buffer is trimmed if necessary to provide a continuous memory region. + // + // Returns a pointer to the continuous memory region and the actual number + // of samples that have been reserved. The maximum growth is limited by + // getTailCapacity() and might be increased by calling trim(). + std::pair growTail(SINT size); + + // Shrinks the buffer from the tail for reading buffered samples. + // + // Returns a pointer to the continuous memory region and the actual + // number of buffered samples that have been dropped. The pointer is + // valid for reading as long as no modifying member function is called! + std::pair shrinkTail(SINT size); + + // Shrinks the buffer from the head for reading buffered samples. + // + // Returns a pointer to the continuous memory region and the actual + // number of buffered samples that have been dropped. The pointer is + // valid for reading as long as no modifying member function is called! + std::pair shrinkHead(SINT size); + +protected: + SampleBuffer m_primaryBuffer; + SINT m_headOffset; + SINT m_tailOffset; +}; + +#define DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer \ + DEBUG_ASSERT(0 <= m_headOffset); \ + DEBUG_ASSERT(m_headOffset <= m_tailOffset); \ + DEBUG_ASSERT(m_tailOffset <= m_primaryBuffer.size()); \ + DEBUG_ASSERT(!isEmpty() || (0 == m_headOffset)); \ + DEBUG_ASSERT(!isEmpty() || (0 == m_tailOffset)) + +#endif // SINGULARSAMPLEBUFFER_H diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index a2843639a1f..2a2848a473d 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -384,7 +384,7 @@ void SoundSourceFLAC::flacMetadata(const FLAC__StreamMetadata* metadata) { m_maxFramesize = metadata->data.stream_info.max_framesize; const SINT sampleBufferCapacity = m_maxBlocksize * getChannelCount(); - CircularSampleBuffer(sampleBufferCapacity).swap(m_sampleBuffer); + m_sampleBuffer.resetCapacity(sampleBufferCapacity); break; } default: diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index 01e45aec8a1..6c7a84aa039 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -3,10 +3,11 @@ #include "sources/soundsource.h" +#include "circularsamplebuffer.h" + #include #include -#include "../circularsamplebuffer.h" namespace Mixxx { @@ -60,7 +61,7 @@ class SoundSourceFLAC: public SoundSource { CSAMPLE m_sampleScaleFactor; - CircularSampleBuffer m_sampleBuffer; + SingularSampleBuffer m_sampleBuffer; SINT m_curFrameIndex; }; diff --git a/src/test/circularsamplebuffer_test.cpp b/src/test/samplebuffertest.cpp similarity index 99% rename from src/test/circularsamplebuffer_test.cpp rename to src/test/samplebuffertest.cpp index 4c3dd764b37..07a4bfcf396 100644 --- a/src/test/circularsamplebuffer_test.cpp +++ b/src/test/samplebuffertest.cpp @@ -1,4 +1,4 @@ -#include +#include "circularsamplebuffer.h" #include "test/mixxxtest.h" #include From 31e3c7212c0cba4674b6bf3a789fbf2cf47d0d91 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 12 Mar 2015 23:53:06 +0100 Subject: [PATCH 237/481] Decouple CircularSampleBuffer from SingularSampleBuffer --- src/circularsamplebuffer.cpp | 49 +++--------------------------------- src/circularsamplebuffer.h | 14 +++-------- src/singularsamplebuffer.cpp | 38 ++++++++++++++++++++++++++++ src/singularsamplebuffer.h | 12 ++++----- 4 files changed, 50 insertions(+), 63 deletions(-) diff --git a/src/circularsamplebuffer.cpp b/src/circularsamplebuffer.cpp index 542f2274f3d..03a1aec3ea2 100644 --- a/src/circularsamplebuffer.cpp +++ b/src/circularsamplebuffer.cpp @@ -1,59 +1,16 @@ #include "circularsamplebuffer.h" -#include "sampleutil.h" - -CircularSampleBuffer::CircularSampleBuffer() { - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; -} - CircularSampleBuffer::CircularSampleBuffer(SINT capacity) : SingularSampleBuffer(capacity), m_secondaryBuffer(capacity) { - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; } void CircularSampleBuffer::resetCapacity(SINT capacity) { - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - if (m_secondaryBuffer.size() != capacity) { - SampleBuffer secondaryBuffer(capacity); - SingularSampleBuffer::resetCapacity(capacity); - secondaryBuffer.swap(m_secondaryBuffer); + SampleBuffer secondaryBuffer(capacity); // may throw + SingularSampleBuffer::resetCapacity(capacity); // may throw + secondaryBuffer.swap(m_secondaryBuffer); // does not throw } else { reset(); } - - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; -} - -void CircularSampleBuffer::swapBuffers() { - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - - // SampleUtil::copy() requires that the source and destination - // memory regions are disjunct. Double-buffering is necessary - // to satisfy this precondition. - SampleUtil::copy( - m_secondaryBuffer.data(), - m_primaryBuffer.data() + m_headOffset, - getSize()); - m_primaryBuffer.swap(m_secondaryBuffer); - // shift offsets - m_tailOffset -= m_headOffset; - m_headOffset = 0; - - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; -} - -void CircularSampleBuffer::trim() { - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; - - if (0 < m_headOffset) { - if (isEmpty()) { - reset(); - } else { - swapBuffers(); - } - } - - DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer; } diff --git a/src/circularsamplebuffer.h b/src/circularsamplebuffer.h index 3c05b4026a8..72792933229 100644 --- a/src/circularsamplebuffer.h +++ b/src/circularsamplebuffer.h @@ -14,10 +14,8 @@ // This class is not thread-safe and not intended to be used from multiple // threads! class CircularSampleBuffer: public SingularSampleBuffer { - Q_DISABLE_COPY(CircularSampleBuffer); - public: - CircularSampleBuffer(); + CircularSampleBuffer() {} explicit CircularSampleBuffer(SINT capacity); void resetCapacity(SINT capacity) /*override*/; @@ -26,16 +24,12 @@ class CircularSampleBuffer: public SingularSampleBuffer { // // This will increase the free capacity at the tail returned by // getTailCapacity() to the maximum amount getCapacity() - getSize(). - void trim(); + void trim() { + SingularSampleBuffer::trim(m_secondaryBuffer); + } private: - void swapBuffers(); - SampleBuffer m_secondaryBuffer; }; -#define DEBUG_ASSERT_CLASS_INVARIANT_CircularSampleBuffer \ - DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; \ - DEBUG_ASSERT(m_primaryBuffer.size() == m_secondaryBuffer.size()); \ - #endif // CIRCULARSAMPLEBUFFER_H diff --git a/src/singularsamplebuffer.cpp b/src/singularsamplebuffer.cpp index 8906b8a972f..32bc3fa6bc0 100644 --- a/src/singularsamplebuffer.cpp +++ b/src/singularsamplebuffer.cpp @@ -1,5 +1,14 @@ #include "singularsamplebuffer.h" +#include "sampleutil.h" + +#define DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer \ + DEBUG_ASSERT(0 <= m_headOffset); \ + DEBUG_ASSERT(m_headOffset <= m_tailOffset); \ + DEBUG_ASSERT(m_tailOffset <= m_primaryBuffer.size()); \ + DEBUG_ASSERT(!isEmpty() || (0 == m_headOffset)); \ + DEBUG_ASSERT(!isEmpty() || (0 == m_tailOffset)) + SingularSampleBuffer::SingularSampleBuffer() : m_headOffset(0), m_tailOffset(0) { @@ -33,6 +42,35 @@ void SingularSampleBuffer::resetCapacity(SINT capacity) { DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; } +void SingularSampleBuffer::swapBuffers(SampleBuffer& secondaryBuffer) { + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; + DEBUG_ASSERT(m_primaryBuffer.size() == secondaryBuffer.size()); + + // SampleUtil::copy() requires that the source and destination + // memory regions are disjunct. Double-buffering is necessary + // to satisfy this precondition. + SampleUtil::copy( + secondaryBuffer.data(), + m_primaryBuffer.data() + m_headOffset, + getSize()); + m_primaryBuffer.swap(secondaryBuffer); + // shift offsets + m_tailOffset -= m_headOffset; + m_headOffset = 0; + + DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; +} + +void SingularSampleBuffer::trim(SampleBuffer& secondaryBuffer) { + if (0 < m_headOffset) { + if (isEmpty()) { + reset(); + } else { + swapBuffers(secondaryBuffer); + } + } +} + std::pair SingularSampleBuffer::growTail(SINT size) { DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; diff --git a/src/singularsamplebuffer.h b/src/singularsamplebuffer.h index 58e60090b95..cf167ff697d 100644 --- a/src/singularsamplebuffer.h +++ b/src/singularsamplebuffer.h @@ -72,16 +72,14 @@ class SingularSampleBuffer { std::pair shrinkHead(SINT size); protected: + void trim(SampleBuffer& secondaryBuffer); + +private: + void swapBuffers(SampleBuffer& secondaryBuffer); + SampleBuffer m_primaryBuffer; SINT m_headOffset; SINT m_tailOffset; }; -#define DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer \ - DEBUG_ASSERT(0 <= m_headOffset); \ - DEBUG_ASSERT(m_headOffset <= m_tailOffset); \ - DEBUG_ASSERT(m_tailOffset <= m_primaryBuffer.size()); \ - DEBUG_ASSERT(!isEmpty() || (0 == m_headOffset)); \ - DEBUG_ASSERT(!isEmpty() || (0 == m_tailOffset)) - #endif // SINGULARSAMPLEBUFFER_H From c8d03920f5cdf086fc25c42ccd8907306be366bb Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 19 Mar 2015 10:20:00 +0100 Subject: [PATCH 238/481] Fix release build --- plugins/soundsourcem4a/soundsourcem4a.cpp | 8 ++++---- .../soundsourcemediafoundation.cpp | 2 +- src/soundsourceproxy.cpp | 4 ++-- src/sources/soundsourceoggvorbis.cpp | 8 ++++---- src/sources/soundsourceoggvorbis.h | 4 ++-- src/sources/soundsourceopus.cpp | 10 +++++----- src/sources/soundsourcesndfile.cpp | 6 +++--- src/sources/urlresource.h | 3 +++ 8 files changed, 24 insertions(+), 21 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index b66b31996ce..a38c4dcd4a7 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -123,20 +123,20 @@ Result SoundSourceM4A::tryOpen(SINT channelCountHint) { m_hFile = MP4Read(getLocalFileNameBytes().constData()); #endif if (MP4_INVALID_FILE_HANDLE == m_hFile) { - qWarning() << "Failed to open file for reading:" << getUrl(); + qWarning() << "Failed to open file for reading:" << getUrlString(); return ERR; } m_trackId = findFirstAudioTrackId(m_hFile); if (MP4_INVALID_TRACK_ID == m_trackId) { - qWarning() << "No AAC track found:" << getUrl(); + qWarning() << "No AAC track found:" << getUrlString(); return ERR; } const MP4SampleId numberOfSamples = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); if (0 >= numberOfSamples) { - qWarning() << "Failed to read number of samples from file:" << getUrl(); + qWarning() << "Failed to read number of samples from file:" << getUrlString(); return ERR; } m_maxSampleBlockId = kSampleBlockIdMin + (numberOfSamples - 1); @@ -373,7 +373,7 @@ SINT SoundSourceM4A::readSampleFrames( qWarning() << "AAC decoding error:" << decFrameInfo.error << NeAACDecGetErrorMessage(decFrameInfo.error) - << getUrl(); + << getUrlString(); break; // abort } DEBUG_ASSERT(pDecodeResult == pDecodeBuffer); // verify the in/out parameter diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index b76b91b0763..6d1de827f1a 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -117,7 +117,7 @@ SoundSourceMediaFoundation::~SoundSourceMediaFoundation() { Result SoundSourceMediaFoundation::tryOpen(SINT channelCountHint) { if (SUCCEEDED(m_hrCoInitialize)) { - qWarning() << "Cannot reopen MediaFoundation file" << getUrl(); + qWarning() << "Cannot reopen MediaFoundation file" << getUrlString(); return ERR; } diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index 9785f3afb1a..33b41d91b22 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -329,13 +329,13 @@ Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource(SINT channelCountHin } if (!m_pSoundSource->isValid()) { - qWarning() << "Invalid file:" << m_pSoundSource->getUrl() + qWarning() << "Invalid file:" << m_pSoundSource->getUrlString() << "channels" << m_pSoundSource->getChannelCount() << "frame rate" << m_pSoundSource->getChannelCount(); return m_pAudioSource; } if (m_pSoundSource->isEmpty()) { - qWarning() << "Empty file:" << m_pSoundSource->getUrl(); + qWarning() << "Empty file:" << m_pSoundSource->getUrlString(); return m_pAudioSource; } diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index c3e46ce3766..463b45eb55c 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -33,19 +33,19 @@ SoundSourceOggVorbis::~SoundSourceOggVorbis() { Result SoundSourceOggVorbis::tryOpen(SINT /*channelCountHint*/) { const QByteArray qbaFilename(getLocalFileNameBytes()); if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { - qWarning() << "Failed to open OggVorbis file:" << getUrl(); + qWarning() << "Failed to open OggVorbis file:" << getUrlString(); return ERR; } if (!ov_seekable(&m_vf)) { - qWarning() << "OggVorbis file is not seekable:" << getUrl(); + qWarning() << "OggVorbis file is not seekable:" << getUrlString(); return ERR; } // lookup the ogg's channels and sample rate const vorbis_info* vi = ov_info(&m_vf, kCurrentBitstreamLink); if (!vi) { - qWarning() << "Failed to read OggVorbis file:" << getUrl(); + qWarning() << "Failed to read OggVorbis file:" << getUrlString(); return ERR; } setChannelCount(vi->channels); @@ -62,7 +62,7 @@ Result SoundSourceOggVorbis::tryOpen(SINT /*channelCountHint*/) { if (0 <= pcmTotal) { setFrameCount(pcmTotal); } else { - qWarning() << "Failed to read total length of OggVorbis file:" << getUrl(); + qWarning() << "Failed to read total length of OggVorbis file:" << getUrlString(); return ERR; } diff --git a/src/sources/soundsourceoggvorbis.h b/src/sources/soundsourceoggvorbis.h index 01d933907d6..b8acc2b23da 100644 --- a/src/sources/soundsourceoggvorbis.h +++ b/src/sources/soundsourceoggvorbis.h @@ -1,11 +1,11 @@ #ifndef MIXXX_SOUNDSOURCEOGGVORBIS_H #define MIXXX_SOUNDSOURCEOGGVORBIS_H +#include "sources/soundsource.h" + #define OV_EXCLUDE_STATIC_CALLBACKS #include -#include "sources/soundsource.h" - namespace Mixxx { class SoundSourceOggVorbis: public SoundSource { diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 8dfa4151f6a..dcdb8495fc9 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -126,13 +126,13 @@ Result SoundSourceOpus::tryOpen(SINT /*channelCountHint*/) { DEBUG_ASSERT(!m_pOggOpusFile); m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); if (!m_pOggOpusFile) { - qWarning() << "Failed to open OggOpus file:" << getUrl() << "errorCode" + qWarning() << "Failed to open OggOpus file:" << getUrlString() << "errorCode" << errorCode; return ERR; } if (!op_seekable(m_pOggOpusFile)) { - qWarning() << "OggOpus file is not seekable:" << getUrl(); + qWarning() << "OggOpus file is not seekable:" << getUrlString(); return ERR; } @@ -140,7 +140,7 @@ Result SoundSourceOpus::tryOpen(SINT /*channelCountHint*/) { if (0 < channelCount) { setChannelCount(channelCount); } else { - qWarning() << "Failed to read channel configuration of OggOpus file:" << getUrl(); + qWarning() << "Failed to read channel configuration of OggOpus file:" << getUrlString(); return ERR; } @@ -148,7 +148,7 @@ Result SoundSourceOpus::tryOpen(SINT /*channelCountHint*/) { if (0 <= pcmTotal) { setFrameCount(pcmTotal); } else { - qWarning() << "Failed to read total length of OggOpus file:" << getUrl(); + qWarning() << "Failed to read total length of OggOpus file:" << getUrlString(); return ERR; } @@ -156,7 +156,7 @@ Result SoundSourceOpus::tryOpen(SINT /*channelCountHint*/) { if (0 < bitrate) { setBitrate(bitrate / 1000); } else { - qWarning() << "Failed to determine bitrate of OggOpus file:" << getUrl(); + qWarning() << "Failed to determine bitrate of OggOpus file:" << getUrlString(); return ERR; } diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 796fc51b92b..fa1ad2ed773 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -34,13 +34,13 @@ Result SoundSourceSndFile::tryOpen(SINT /*channelCountHint*/) { #endif if (!m_pSndFile) { // sf_format_check is only for writes - qWarning() << "Error opening libsndfile file:" << getUrl() + qWarning() << "Error opening libsndfile file:" << getUrlString() << sf_strerror(m_pSndFile); return ERR; } if (sf_error(m_pSndFile) > 0) { - qWarning() << "Error opening libsndfile file:" << getUrl() + qWarning() << "Error opening libsndfile file:" << getUrlString() << sf_strerror(m_pSndFile); return ERR; } @@ -60,7 +60,7 @@ void SoundSourceSndFile::close() { } else { qWarning() << "Failed to close file:" << closeResult << sf_strerror(m_pSndFile) - << getUrl(); + << getUrlString(); } } } diff --git a/src/sources/urlresource.h b/src/sources/urlresource.h index 7cb2722ceee..b32a9cd5dc1 100644 --- a/src/sources/urlresource.h +++ b/src/sources/urlresource.h @@ -14,6 +14,9 @@ class UrlResource { const QUrl& getUrl() const { return m_url; } + QString getUrlString() const { + return m_url.toString(); + } protected: explicit UrlResource(QUrl url) From 4fdac1d842be89f898cb547b9c8c38af50129cb5 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 20 Mar 2015 08:01:49 +0100 Subject: [PATCH 239/481] Introduce ReadableChunk/WritableChunk for SampleBuffer --- plugins/soundsourcem4a/soundsourcem4a.cpp | 34 ++++++------ src/samplebuffer.h | 66 ++++++++++++++++++++--- src/singularsamplebuffer.cpp | 52 +++++++++--------- src/singularsamplebuffer.h | 11 ++-- src/sources/soundsourceflac.cpp | 30 +++++------ src/sources/soundsourcemodplug.cpp | 2 +- src/test/samplebuffertest.cpp | 46 ++++++++-------- 7 files changed, 152 insertions(+), 89 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index a38c4dcd4a7..3e0bb46a8bf 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -296,16 +296,16 @@ SINT SoundSourceM4A::readSampleFrames( if (!m_sampleBuffer.isEmpty()) { // Consume previously decoded sample data - const std::pair readBuffer( - m_sampleBuffer.shrinkHead(numberOfSamplesRemaining)); + const SampleBuffer::ReadableChunk readableChunk( + m_sampleBuffer.readFromHead(numberOfSamplesRemaining)); if (pSampleBuffer) { - SampleUtil::copy(pSampleBuffer, readBuffer.first, readBuffer.second); - pSampleBuffer += readBuffer.second; + SampleUtil::copy(pSampleBuffer, readableChunk.data(), readableChunk.size()); + pSampleBuffer += readableChunk.size(); } - m_curFrameIndex += samples2frames(readBuffer.second); + m_curFrameIndex += samples2frames(readableChunk.size()); DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); - DEBUG_ASSERT(numberOfSamplesRemaining >= readBuffer.second); - numberOfSamplesRemaining -= readBuffer.second; + DEBUG_ASSERT(numberOfSamplesRemaining >= readableChunk.size()); + numberOfSamplesRemaining -= readableChunk.size(); if (0 == numberOfSamplesRemaining) { break; // exit loop } @@ -352,12 +352,12 @@ SINT SoundSourceM4A::readSampleFrames( decodeBufferCapacity = numberOfSamplesRemaining; } else { // Decode next sample block into temporary buffer - const SINT growTailCount = math_max( + const SINT writeToTailCount = math_max( numberOfSamplesRemaining, decodeBufferCapacityMin); - const std::pair writeBuffer( - m_sampleBuffer.growTail(growTailCount)); - pDecodeBuffer = writeBuffer.first; - decodeBufferCapacity = writeBuffer.second; + const SampleBuffer::WritableChunk writableChunk( + m_sampleBuffer.writeToTail(writeToTailCount)); + pDecodeBuffer = writableChunk.data(); + decodeBufferCapacity = writableChunk.size(); } DEBUG_ASSERT(decodeBufferCapacityMin <= decodeBufferCapacity); @@ -404,12 +404,12 @@ SINT SoundSourceM4A::readSampleFrames( numberOfSamplesRead = math_min(numberOfSamplesDecoded, numberOfSamplesRemaining); pSampleBuffer += numberOfSamplesRead; } else { - m_sampleBuffer.shrinkTail(decodeBufferCapacity - numberOfSamplesDecoded); - const std::pair readBuffer( - m_sampleBuffer.shrinkHead(numberOfSamplesRemaining)); - numberOfSamplesRead = readBuffer.second; + m_sampleBuffer.readFromTail(decodeBufferCapacity - numberOfSamplesDecoded); + const SampleBuffer::ReadableChunk readableChunk( + m_sampleBuffer.readFromHead(numberOfSamplesRemaining)); + numberOfSamplesRead = readableChunk.size(); if (pSampleBuffer) { - SampleUtil::copy(pSampleBuffer, readBuffer.first, numberOfSamplesRead); + SampleUtil::copy(pSampleBuffer, readableChunk.data(), numberOfSamplesRead); pSampleBuffer += numberOfSamplesRead; } } diff --git a/src/samplebuffer.h b/src/samplebuffer.h index bc8bceb5e0e..c9f0e4160fe 100644 --- a/src/samplebuffer.h +++ b/src/samplebuffer.h @@ -42,18 +42,24 @@ class SampleBuffer { return m_size; } - CSAMPLE* data() { - return m_data; + CSAMPLE* data(SINT offset = 0) { + DEBUG_ASSERT(0 <= offset); + // >=: allow access to one element behind allocated memory + DEBUG_ASSERT(m_size >= offset); + return m_data + offset; } - const CSAMPLE* data() const { - return m_data; + const CSAMPLE* data(SINT offset = 0) const { + DEBUG_ASSERT(0 <= offset); + // >=: allow access to one element behind allocated memory + DEBUG_ASSERT(m_size >= offset); + return m_data + offset; } CSAMPLE& operator[](SINT index) { - return m_data[index]; + return *data(index); } const CSAMPLE& operator[](SINT index) const { - return m_data[index]; + return *data(index); } // Exchanges the members of two buffers in conformance with the @@ -71,6 +77,54 @@ class SampleBuffer { // Fills the whole buffer with the same value void fill(CSAMPLE value); + class ReadableChunk { + public: + ReadableChunk(const SampleBuffer& buffer, SINT offset, SINT length) + : m_data(buffer.data(offset)), + m_size(length) { + DEBUG_ASSERT((buffer.size() - offset) >= length); + } + const CSAMPLE* data(SINT offset = 0) const { + DEBUG_ASSERT(0 <= offset); + // >=: allow access to one element behind allocated memory + DEBUG_ASSERT(m_size >= offset); + return m_data + offset; + } + SINT size() const { + return m_size; + } + const CSAMPLE& operator[](SINT index) const { + return *data(index); + } + private: + const CSAMPLE* m_data; + SINT m_size; + }; + + class WritableChunk { + public: + WritableChunk(SampleBuffer& buffer, SINT offset, SINT length) + : m_data(buffer.data(offset)), + m_size(length) { + DEBUG_ASSERT((buffer.size() - offset) >= length); + } + CSAMPLE* data(SINT offset = 0) const { + DEBUG_ASSERT(0 <= offset); + // >=: allow access to one element behind allocated memory + DEBUG_ASSERT(m_size >= offset); + return m_data + offset; + } + SINT size() const { + return m_size; + } + CSAMPLE& operator[](SINT index) const { + return *data(index); + } + private: + CSAMPLE* m_data; + SINT m_size; + }; + private: CSAMPLE* m_data; SINT m_size; diff --git a/src/singularsamplebuffer.cpp b/src/singularsamplebuffer.cpp index 32bc3fa6bc0..af3b7967db9 100644 --- a/src/singularsamplebuffer.cpp +++ b/src/singularsamplebuffer.cpp @@ -37,7 +37,7 @@ void SingularSampleBuffer::resetCapacity(SINT capacity) { if (m_primaryBuffer.size() != capacity) { SampleBuffer(capacity).swap(m_primaryBuffer); } - reset(); + resetOffsets(); DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; } @@ -51,7 +51,7 @@ void SingularSampleBuffer::swapBuffers(SampleBuffer& secondaryBuffer) { // to satisfy this precondition. SampleUtil::copy( secondaryBuffer.data(), - m_primaryBuffer.data() + m_headOffset, + m_primaryBuffer.data(m_headOffset), getSize()); m_primaryBuffer.swap(secondaryBuffer); // shift offsets @@ -71,44 +71,48 @@ void SingularSampleBuffer::trim(SampleBuffer& secondaryBuffer) { } } -std::pair SingularSampleBuffer::growTail(SINT size) { +SampleBuffer::WritableChunk SingularSampleBuffer::writeToTail(SINT size) { DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; - CSAMPLE* const tailData = m_primaryBuffer.data() + m_tailOffset; - const SINT tailSize = math_min(size, getTailCapacity()); - m_tailOffset += tailSize; + const SINT tailLength = math_min(size, getTailCapacity()); + const SampleBuffer::WritableChunk tailChunk( + m_primaryBuffer, m_tailOffset, tailLength); + m_tailOffset += tailLength; DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; - return std::make_pair(tailData, tailSize); + return tailChunk; } -std::pair SingularSampleBuffer::shrinkTail(SINT size) { +SampleBuffer::ReadableChunk SingularSampleBuffer::readFromTail(SINT size) { DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; - const SINT tailSize = math_min(size, getSize()); - m_tailOffset -= tailSize; - const CSAMPLE* const tailData = m_primaryBuffer.data() + m_tailOffset; - if (tailSize == getSize()) { - DEBUG_ASSERT(isEmpty()); - reset(); + const SINT tailLength = math_min(size, getSize()); + m_tailOffset -= tailLength; + const SampleBuffer::ReadableChunk tailChunk( + m_primaryBuffer, m_tailOffset, tailLength); + if (isEmpty()) { + // Internal buffer becomes empty and can safely be reset + // to extend the tail capacity for future growth + resetOffsets(); } DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; - return std::make_pair(tailData, tailSize); + return tailChunk; } -std::pair SingularSampleBuffer::shrinkHead(SINT size) { +SampleBuffer::ReadableChunk SingularSampleBuffer::readFromHead(SINT size) { DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; - const CSAMPLE* const headData = m_primaryBuffer.data() + m_headOffset; - const SINT headSize = math_min(size, getSize()); - if (headSize == getSize()) { - // buffer becomes empty - reset(); - } else { - m_headOffset += headSize; + const SINT headLength = math_min(size, getSize()); + const SampleBuffer::ReadableChunk headChunk( + m_primaryBuffer, m_headOffset, headLength); + m_headOffset += headLength; + if (isEmpty()) { + // Internal buffer becomes empty and can safely be reset + // to extend the tail capacity for future growth + resetOffsets(); } DEBUG_ASSERT_CLASS_INVARIANT_SingularSampleBuffer; - return std::make_pair(headData, headSize); + return headChunk; } diff --git a/src/singularsamplebuffer.h b/src/singularsamplebuffer.h index cf167ff697d..bcfcb4b7d6c 100644 --- a/src/singularsamplebuffer.h +++ b/src/singularsamplebuffer.h @@ -55,21 +55,21 @@ class SingularSampleBuffer { // Returns a pointer to the continuous memory region and the actual number // of samples that have been reserved. The maximum growth is limited by // getTailCapacity() and might be increased by calling trim(). - std::pair growTail(SINT size); + SampleBuffer::WritableChunk writeToTail(SINT size); // Shrinks the buffer from the tail for reading buffered samples. // // Returns a pointer to the continuous memory region and the actual // number of buffered samples that have been dropped. The pointer is // valid for reading as long as no modifying member function is called! - std::pair shrinkTail(SINT size); + SampleBuffer::ReadableChunk readFromTail(SINT size); // Shrinks the buffer from the head for reading buffered samples. // // Returns a pointer to the continuous memory region and the actual // number of buffered samples that have been dropped. The pointer is // valid for reading as long as no modifying member function is called! - std::pair shrinkHead(SINT size); + SampleBuffer::ReadableChunk readFromHead(SINT size); protected: void trim(SampleBuffer& secondaryBuffer); @@ -77,6 +77,11 @@ class SingularSampleBuffer { private: void swapBuffers(SampleBuffer& secondaryBuffer); + void resetOffsets() { + m_headOffset = 0; + m_tailOffset = 0; + } + SampleBuffer m_primaryBuffer; SINT m_headOffset; SINT m_tailOffset; diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 2a2848a473d..6744dda7882 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -185,28 +185,28 @@ SINT SoundSourceFLAC::readSampleFrames( break; // EOF } - const std::pair readBuffer( - m_sampleBuffer.shrinkHead(numberOfSamplesRemaining)); - const SINT framesToCopy = samples2frames(readBuffer.second); + const SampleBuffer::ReadableChunk readableChunk( + m_sampleBuffer.readFromHead(numberOfSamplesRemaining)); + const SINT framesToCopy = samples2frames(readableChunk.size()); if (outBuffer) { if (readStereoSamples && !isChannelCountStereo()) { if (isChannelCountMono()) { SampleUtil::copyMonoToDualMono(outBuffer, - readBuffer.first, + readableChunk.data(), framesToCopy); } else { SampleUtil::copyMultiToStereo(outBuffer, - readBuffer.first, + readableChunk.data(), framesToCopy, getChannelCount()); } outBuffer += framesToCopy * 2; // copied 2 samples per frame } else { - SampleUtil::copy(outBuffer, readBuffer.first, readBuffer.second); - outBuffer += readBuffer.second; + SampleUtil::copy(outBuffer, readableChunk.data(), readableChunk.size()); + outBuffer += readableChunk.size(); } } m_curFrameIndex += framesToCopy; - numberOfSamplesRemaining -= readBuffer.second; + numberOfSamplesRemaining -= readableChunk.size(); } DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); @@ -284,15 +284,15 @@ FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( } DEBUG_ASSERT(m_sampleBuffer.isEmpty()); - const std::pair writeBuffer( - m_sampleBuffer.growTail( + const SampleBuffer::WritableChunk writableChunk( + m_sampleBuffer.writeToTail( frames2samples(frame->header.blocksize))); - CSAMPLE* pSampleBuffer = writeBuffer.first; + CSAMPLE* pSampleBuffer = writableChunk.data(); switch (getChannelCount()) { case 1: { // optimized code for 1 channel (mono) DEBUG_ASSERT(1 <= frame->header.channels); - for (SINT i = 0; i < writeBuffer.second; ++i) { + for (SINT i = 0; i < writableChunk.size(); ++i) { *pSampleBuffer++ = buffer[0][i] * m_sampleScaleFactor; } break; @@ -300,8 +300,8 @@ FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( case 2: { // optimized code for 2 channels (stereo) DEBUG_ASSERT(2 <= frame->header.channels); - DEBUG_ASSERT(0 == (writeBuffer.second % 2)); - for (SINT i = 0; i < (writeBuffer.second / 2); ++i) { + DEBUG_ASSERT(0 == (writableChunk.size() % 2)); + for (SINT i = 0; i < (writableChunk.size() / 2); ++i) { *pSampleBuffer++ = buffer[0][i] * m_sampleScaleFactor; *pSampleBuffer++ = buffer[1][i] * m_sampleScaleFactor; } @@ -310,7 +310,7 @@ FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( default: { // generic code for multiple channels DEBUG_ASSERT(getChannelCount() == frame->header.channels); - for (SINT i = 0; i < samples2frames(writeBuffer.second); ++i) { + for (SINT i = 0; i < samples2frames(writableChunk.size()); ++i) { for (unsigned j = 0; j < frame->header.channels; ++j) { *pSampleBuffer++ = buffer[j][i] * m_sampleScaleFactor; } diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index 13d85c23e57..5dffdbf89ba 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -157,7 +157,7 @@ Result SoundSourceModPlug::tryOpen(SINT /*channelCountHint*/) { // reserve enough space in sample buffer m_sampleBuf.resize(currentSize + CHUNKSIZE); samplesRead = ModPlug::ModPlug_Read(m_pModFile, - m_sampleBuf.data() + currentSize, + &m_sampleBuf[currentSize], CHUNKSIZE * 2) / 2; // adapt to actual size currentSize += samplesRead; diff --git a/src/test/samplebuffertest.cpp b/src/test/samplebuffertest.cpp index 07a4bfcf396..cf727802712 100644 --- a/src/test/samplebuffertest.cpp +++ b/src/test/samplebuffertest.cpp @@ -16,33 +16,33 @@ class CircularSampleBufferTest: public MixxxTest { static const SINT kCapacity; SINT writeTail(CircularSampleBuffer* pSampleBuffer, SINT size) { - const std::pair writeBuffer( - pSampleBuffer->growTail(size)); - for (SINT i = 0; i < writeBuffer.second; ++i) { - writeBuffer.first[i] = m_writeValue; + const SampleBuffer::WritableChunk writableChunk( + pSampleBuffer->writeToTail(size)); + for (SINT i = 0; i < writableChunk.size(); ++i) { + writableChunk[i] = m_writeValue; m_writeValue += CSAMPLE_ONE; } - return writeBuffer.second; + return writableChunk.size(); } SINT readHeadAndVerify(CircularSampleBuffer* pSampleBuffer, SINT size) { - const std::pairreadBuffer( - pSampleBuffer->shrinkHead(size)); - for (SINT i = 0; i < readBuffer.second; ++i) { - EXPECT_EQ(readBuffer.first[i], m_readValue); + const SampleBuffer::ReadableChunk readableChunk( + pSampleBuffer->readFromHead(size)); + for (SINT i = 0; i < readableChunk.size(); ++i) { + EXPECT_EQ(readableChunk[i], m_readValue); m_readValue += CSAMPLE_ONE; } - return readBuffer.second; + return readableChunk.size(); } SINT readTailAndVerify(CircularSampleBuffer* pSampleBuffer, SINT size) { - const std::pairreadBuffer( - pSampleBuffer->shrinkTail(size)); - for (SINT i = readBuffer.second; i-- > 0; ) { + const SampleBuffer::ReadableChunk readableChunk( + pSampleBuffer->readFromTail(size)); + for (SINT i = readableChunk.size(); i-- > 0; ) { m_writeValue -= CSAMPLE_ONE; - EXPECT_EQ(readBuffer.first[i], m_writeValue); + EXPECT_EQ(readableChunk[i], m_writeValue); } - return readBuffer.second; + return readableChunk.size(); } void reset(CircularSampleBuffer* pSampleBuffer) { @@ -68,16 +68,16 @@ TEST_F(CircularSampleBufferTest, emptyWithoutCapacity) { sampleBuffer.reset(); EXPECT_TRUE(sampleBuffer.isEmpty()); - const std::pair writeResult( - sampleBuffer.growTail(10)); - EXPECT_EQ(writeResult.first, static_cast(NULL)); - EXPECT_EQ(writeResult.second, 0); + const SampleBuffer::WritableChunk writableChunk( + sampleBuffer.writeToTail(10)); + EXPECT_EQ(writableChunk.data(), static_cast(NULL)); + EXPECT_EQ(writableChunk.size(), 0); EXPECT_TRUE(sampleBuffer.isEmpty()); - const std::pair readResult( - sampleBuffer.shrinkHead(10)); - EXPECT_EQ(readResult.first, static_cast(NULL)); - EXPECT_EQ(readResult.second, 0); + const SampleBuffer::ReadableChunk readableChunk( + sampleBuffer.readFromHead(10)); + EXPECT_EQ(readableChunk.data(), static_cast(NULL)); + EXPECT_EQ(readableChunk.size(), 0); EXPECT_TRUE(sampleBuffer.isEmpty()); } From 61e80dde08d16297d07acb11278dc823edced23a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 23 Mar 2015 07:52:45 +0100 Subject: [PATCH 240/481] SoundSourceModePlug: Delete obsolete enumeration type --- src/sources/soundsourcemodplug.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index 5dffdbf89ba..a2003c89479 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -36,18 +36,6 @@ QString getModPlugTypeFromUrl(QUrl url) { } } -// identification of modplug module type -enum ModuleTypes { - NONE = 0x00, - MOD = 0x01, - S3M = 0x02, - XM = 0x04, - MED = 0x08, - IT = 0x20, - STM = 0x100, - OKT = 0x8000 -}; - } // anonymous namespace const SINT SoundSourceModPlug::kChannelCount = 2; // always stereo From 3d7443f000b29ff142358749332e59142b275a9b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 23 Mar 2015 22:32:40 +0100 Subject: [PATCH 241/481] SoundSourceModePlug: Use SampleUtil for sample conversion --- src/sources/soundsourcemodplug.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index a2003c89479..d7b123f6dcd 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -2,6 +2,7 @@ #include "metadata/trackmetadata.h" #include "util/timer.h" +#include "sampleutil.h" #include @@ -177,22 +178,22 @@ void SoundSourceModPlug::close() { SINT SoundSourceModPlug::seekSampleFrame( SINT frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + return m_seekPos = frameIndex; } SINT SoundSourceModPlug::readSampleFrames( SINT numberOfFrames, CSAMPLE* sampleBuffer) { - const SINT maxFrames = samples2frames(m_sampleBuf.size()); - const SINT readFrames = math_min(maxFrames - m_seekPos, numberOfFrames); + DEBUG_ASSERT(0 <= numberOfFrames); + DEBUG_ASSERT(isValidFrameIndex(m_seekPos)); + const SINT readFrames = math_min(getFrameCount() - m_seekPos, numberOfFrames); const SINT readSamples = frames2samples(readFrames); const SINT readOffset = frames2samples(m_seekPos); - for (SINT i = 0; i < readSamples; ++i) { - sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) - / CSAMPLE(SAMPLE_MAX); - } - + SampleUtil::convertS16ToFloat32(sampleBuffer, &m_sampleBuf[readOffset], readSamples); m_seekPos += readFrames; + return readFrames; } From 25febb9277146e0cee61131718a3a7f52ea090fa Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 23 Mar 2015 23:25:48 +0100 Subject: [PATCH 242/481] SoundSourceModePlug: Define all constants here instead of in config dialog --- src/dlgprefmodplug.cpp | 6 +++--- src/sources/soundsourcemodplug.cpp | 5 +++-- src/sources/soundsourcemodplug.h | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/dlgprefmodplug.cpp b/src/dlgprefmodplug.cpp index 7dd73097b62..542cbeb0554 100644 --- a/src/dlgprefmodplug.cpp +++ b/src/dlgprefmodplug.cpp @@ -125,11 +125,11 @@ void DlgPrefModplug::applySettings() { // Currently this is fixed to 16bit 44.1kHz stereo // Number of channels - 1 for mono or 2 for stereo - settings.mChannels = 2; + settings.mChannels = Mixxx::SoundSourceModPlug::kChannelCount; // Bits per sample - 8, 16, or 32 - settings.mBits = 16; + settings.mBits = Mixxx::SoundSourceModPlug::kBitsPerSample; // Sampling rate - 11025, 22050, or 44100 - settings.mFrequency = 44100; + settings.mFrequency = Mixxx::SoundSourceModPlug::kFrameRate; // enabled features flags settings.mFlags = 0; diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index d7b123f6dcd..ee21e075fc9 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -39,8 +39,9 @@ QString getModPlugTypeFromUrl(QUrl url) { } // anonymous namespace -const SINT SoundSourceModPlug::kChannelCount = 2; // always stereo -const SINT SoundSourceModPlug::kFrameRate = 44100; // always 44.1 kHz +const SINT SoundSourceModPlug::kChannelCount = 2; // stereo +const SINT SoundSourceModPlug::kBitsPerSample = 16; +const SINT SoundSourceModPlug::kFrameRate = 44100; // 44.1 kHz QList SoundSourceModPlug::supportedFileExtensions() { QList list; diff --git a/src/sources/soundsourcemodplug.h b/src/sources/soundsourcemodplug.h index 7f818c9f8fe..5a656c645f0 100644 --- a/src/sources/soundsourcemodplug.h +++ b/src/sources/soundsourcemodplug.h @@ -19,6 +19,7 @@ class SoundSourceModPlug: public Mixxx::SoundSource { static QList supportedFileExtensions(); static const SINT kChannelCount; + static const SINT kBitsPerSample; static const SINT kFrameRate; // apply settings for decoding From 04c081f83653e6ce208c21285e61de2c181270b0 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 23 Mar 2015 23:29:44 +0100 Subject: [PATCH 243/481] SoundSourceModPlug: Fix length estimation and simplify read loop --- src/sources/soundsourcemodplug.cpp | 65 ++++++++++++++++-------------- src/sources/soundsourcemodplug.h | 10 +++-- 2 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index ee21e075fc9..cc77a5af04f 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -9,13 +9,13 @@ #include #include -/* read files in 512k chunks */ -#define CHUNKSIZE (1 << 18) - namespace Mixxx { namespace { +/* read files in 512k chunks */ +const SINT kChunkSizeInBytes = SINT(1) << 19; + QString getModPlugTypeFromUrl(QUrl url) { const QString type(SoundSource::getTypeFromUrl(url)); if (type == "mod") { @@ -69,7 +69,6 @@ void SoundSourceModPlug::configure(unsigned int bufferSizeLimit, SoundSourceModPlug::SoundSourceModPlug(QUrl url) : SoundSource(url, getModPlugTypeFromUrl(url)), m_pModFile(NULL), - m_fileLength(0), m_seekPos(0) { } @@ -113,10 +112,10 @@ Result SoundSourceModPlug::tryOpen(SINT /*channelCountHint*/) { modFile.open(QIODevice::ReadOnly); m_fileBuf = modFile.readAll(); modFile.close(); + // get ModPlugFile descriptor for later access m_pModFile = ModPlug::ModPlug_Load(m_fileBuf.constData(), m_fileBuf.length()); - if (m_pModFile == NULL) { // an error occured t.cancel(); @@ -124,36 +123,42 @@ Result SoundSourceModPlug::tryOpen(SINT /*channelCountHint*/) { return ERR; } - // estimate size of sample buffer (for better performance) - // beware: module length estimation is unreliable due to loops - // song milliseconds * 2 (bytes per sample) - // * 2 (channels) - // * 44.1 (samples per millisecond) - // + some more to accomodate short loops etc. - // approximate and align with CHUNKSIZE yields: - // (((milliseconds << 1) >> 10 /* to seconds */) - // div 11 /* samples to chunksize ratio */) - // << 19 /* align to chunksize */ - unsigned int estimate = ((ModPlug::ModPlug_GetLength(m_pModFile) >> 8) / 11) - << 18; - estimate = math_min(estimate, s_bufferSizeLimit); - m_sampleBuf.reserve(estimate); + DEBUG_ASSERT(0 == (kChunkSizeInBytes % sizeof(m_sampleBuf[0]))); + const SINT chunkSizeInSamples = kChunkSizeInBytes / sizeof(m_sampleBuf[0]); + + const SampleBuffer::size_type bufferSizeLimitInSamples = s_bufferSizeLimit / sizeof(m_sampleBuf[0]); + + // Estimate size of sample buffer (for better performance) aligned + // with the chunk size. Beware: Module length estimation is unreliable + // due to loops! + const SampleBuffer::size_type estimateMilliseconds = + ModPlug::ModPlug_GetLength(m_pModFile); + const SampleBuffer::size_type estimateSamples = + estimateMilliseconds * kChannelCount * kFrameRate; + const SampleBuffer::size_type estimateChunks = + (estimateSamples + (chunkSizeInSamples - 1)) / chunkSizeInSamples; + const SampleBuffer::size_type sampleBufferCapacity = math_min( + estimateChunks * chunkSizeInSamples, bufferSizeLimitInSamples); + m_sampleBuf.reserve(sampleBufferCapacity); qDebug() << "[ModPlug] Reserved " << m_sampleBuf.capacity() << " #samples"; - // decode samples to sample buffer - int samplesRead = -1; - int currentSize = 0; - while ((samplesRead != 0) && (m_sampleBuf.size() < s_bufferSizeLimit)) { + // decode samples into sample buffer + while (m_sampleBuf.size() < bufferSizeLimitInSamples) { // reserve enough space in sample buffer - m_sampleBuf.resize(currentSize + CHUNKSIZE); - samplesRead = ModPlug::ModPlug_Read(m_pModFile, + const SampleBuffer::size_type currentSize = m_sampleBuf.size(); + m_sampleBuf.resize(currentSize + chunkSizeInSamples); + const int bytesRead = ModPlug::ModPlug_Read(m_pModFile, &m_sampleBuf[currentSize], - CHUNKSIZE * 2) / 2; - // adapt to actual size - currentSize += samplesRead; - if (samplesRead != CHUNKSIZE) { + kChunkSizeInBytes); + // adjust size of sample buffer after reading + if (0 < bytesRead) { + DEBUG_ASSERT(0 == (bytesRead % sizeof(m_sampleBuf[0]))); + const SampleBuffer::size_type samplesRead = bytesRead / sizeof(m_sampleBuf[0]); + m_sampleBuf.resize(currentSize + samplesRead); + } else { + // nothing read -> EOF m_sampleBuf.resize(currentSize); - samplesRead = 0; // we reached the end of the file + break; // exit loop } } qDebug() << "[ModPlug] Filled Sample buffer with " << m_sampleBuf.size() diff --git a/src/sources/soundsourcemodplug.h b/src/sources/soundsourcemodplug.h index 5a656c645f0..0cb18ec991f 100644 --- a/src/sources/soundsourcemodplug.h +++ b/src/sources/soundsourcemodplug.h @@ -41,15 +41,17 @@ class SoundSourceModPlug: public Mixxx::SoundSource { CSAMPLE* sampleBuffer) /*override*/; private: - Result tryOpen(SINT channelCountHint = kChannelCountDefault) /*override*/; + Result tryOpen(SINT channelCountHint) /*override*/; static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor - SINT m_fileLength; // length of file in samples - SINT m_seekPos; // current read position QByteArray m_fileBuf; // original module file data - std::vector m_sampleBuf; // 16bit stereo samples, 44.1kHz + + typedef std::vector SampleBuffer; + SampleBuffer m_sampleBuf; + + SINT m_seekPos; // current read position }; } // namespace Mixxx From 077e427eb2db78fc22c8dc93ada7494432b9beee Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Tue, 7 Apr 2015 00:43:09 +0200 Subject: [PATCH 244/481] width parameter + not working tests with delay --- src/effects/native/autopaneffect.cpp | 47 +++++++++++++++++++++------- src/effects/native/autopaneffect.h | 1 + 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index 15e6b2d918e..d83ef23c81a 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -58,9 +58,23 @@ EffectManifest AutoPanEffect::getManifest() { delay->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); delay->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); delay->setMinimum(0.0); - delay->setMaximum(0.02); // 0.02 * sampleRate => 20ms +// delay->setMaximum(0.02); // 0.02 * sampleRate => 20ms + delay->setMaximum(0.1); // 0.02 * sampleRate => 20ms delay->setDefault(0.01); + // Width : applied on the channel with gain reducing. + EffectManifestParameter* width = manifest.addParameter(); + width->setId("width"); + width->setName(QObject::tr("width")); + width->setDescription("Controls length of the width"); + width->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + width->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + width->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + width->setMinimum(0.0); +// delay->setMaximum(0.02); // 0.02 * sampleRate => 20ms + width->setMaximum(1.0); // 0.02 * sampleRate => 20ms + width->setDefault(0.00); + return manifest; } @@ -68,7 +82,8 @@ AutoPanEffect::AutoPanEffect(EngineEffect* pEffect, const EffectManifest& manife : m_pSmoothingParameter(pEffect->getParameterById("smoothing")), m_pPeriodParameter(pEffect->getParameterById("period")), - m_pDelayParameter(pEffect->getParameterById("delay")) + m_pDelayParameter(pEffect->getParameterById("delay")), + m_pWidthParameter(pEffect->getParameterById("width")) { Q_UNUSED(manifest); } @@ -90,6 +105,7 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p return; } + CSAMPLE width = m_pWidthParameter->value(); CSAMPLE period = m_pPeriodParameter->value(); if (groupFeatures.has_beat_length) { // 1/8, 1/4, 1/2, 1, 2, 4, 8, 16, 32, 64 @@ -130,10 +146,9 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p // prepare the delay buffer float delay = round(m_pDelayParameter->value() * sampleRate); gs.delay->setDelay(delay); - gs.delay->process(pInput, gs.m_pDelayBuf, numSamples); - SampleUtil::copy1WithRampingGain(pOutput, - gs.m_pDelayBuf, pInput, pInput, - numSamples); +// gs.delay->process(pInput, pOutput, numSamples); +// gs.delay->process(pInput, gs.m_pDelayBuf, numSamples); +// SampleUtil::copyWithGain(pOutput, for (unsigned int i = 0; i + 1 < numSamples; i += 2) { @@ -159,7 +174,7 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p // transforms the angleFraction into a sinusoid (but between 0 and 1) gs.frac.setWithRampingApplied( - (sin(M_PI * 2.0f * angleFraction) + 1.0f) / 2.0f); + (sin(M_PI * 2.0f * angleFraction) * width + 1.0f) / 2.0f); // delay on the reducing channel // todo (jclaveau) : this produces clics, especially when the period @@ -179,14 +194,24 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p // pOutput[i+1] = gs.m_pDelayBuf[i+1] * (1.0f - gs.frac) * lawCoef; // pOutput[i] = gs.m_pDelayBuf[i] * gs.frac * lawCoef; - // pOutput[i] = pInput[i] * gs.frac * lawCoef; - // pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; - pOutput[i] = pOutput[i] * gs.frac * lawCoef; - pOutput[i+1] = pOutput[i+1] * (1.0f - gs.frac) * lawCoef; + pOutput[i] = pInput[i] * gs.frac * lawCoef; + pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; + +// pOutput[i] = pOutput[i] * gs.frac * lawCoef; +// pOutput[i+1] = pOutput[i+1] * (1.0f - gs.frac) * lawCoef; + +// pOutput[i] = gs.m_pDelayBuf[i] * gs.frac * lawCoef; +// pOutput[i+1] = gs.m_pDelayBuf[i+1] * (1.0f - gs.frac) * lawCoef; gs.time++; } + gs.delay->process(pOutput, pOutput, numSamples); + +// SampleUtil::addWithGain(pOutput, +// pOutput, 0.9, +// numSamples); + /**/ qDebug() // << "| ramped :" << gs.frac.ramped diff --git a/src/effects/native/autopaneffect.h b/src/effects/native/autopaneffect.h index 91441116aa2..7ef427400f6 100644 --- a/src/effects/native/autopaneffect.h +++ b/src/effects/native/autopaneffect.h @@ -103,6 +103,7 @@ class AutoPanEffect : public PerChannelEffectProcessor { EngineEffectParameter* m_pSmoothingParameter; EngineEffectParameter* m_pPeriodParameter; EngineEffectParameter* m_pDelayParameter; + EngineEffectParameter* m_pWidthParameter; DISALLOW_COPY_AND_ASSIGN(AutoPanEffect); }; From 26c24b4a5bea66ba33547467e313285986880f30 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Tue, 7 Apr 2015 01:19:58 +0200 Subject: [PATCH 245/481] cleaning + removing useless lawCoef --- src/effects/native/autopaneffect.cpp | 42 +++++----------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index d83ef23c81a..501f90d850d 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -132,11 +132,6 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p // 1 / ( 1 - 2 * stepfrac) float a = stepFrac != 0.5f ? 1.0f / (1.0f - stepFrac * 2.0f) : 1.0f; - // define the increasing of gain (the music sounds lower if only one - // channel is enabled). - // TODO(jclaveau) : Challenge this value - float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; - // size of a segment of slope (controled by the "strength" parameter) float u = (0.5f - stepFrac) / 2.0f; @@ -146,9 +141,6 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p // prepare the delay buffer float delay = round(m_pDelayParameter->value() * sampleRate); gs.delay->setDelay(delay); -// gs.delay->process(pInput, pOutput, numSamples); -// gs.delay->process(pInput, gs.m_pDelayBuf, numSamples); -// SampleUtil::copyWithGain(pOutput, for (unsigned int i = 0; i + 1 < numSamples; i += 2) { @@ -172,36 +164,16 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p angleFraction = (periodFraction - stepsFractionPart) * a; } - // transforms the angleFraction into a sinusoid (but between 0 and 1) + // transforms the angleFraction into a sinusoid (but between 0 and 1). + // The width parameter modulates the two limits. if width values 0.5, + // the limits will be 0.25 and 0.75. If it's 0, it will be 0.5 and 0.5 + // so the sound will be stuck at the center. If it values 1, the limits + // will be 0 and 1 (full left and full right). gs.frac.setWithRampingApplied( (sin(M_PI * 2.0f * angleFraction) * width + 1.0f) / 2.0f); - // delay on the reducing channel - // todo (jclaveau) : this produces clics, especially when the period - // is short. - /* - if (quarter == 0.0 || quarter == 3.0){ - // channel 1 increasing (left) - pOutput[i] = pInput[i] * gs.frac * lawCoef; - pOutput[i+1] = gs.m_pDelayBuf[i+1] * (1.0f - gs.frac) * lawCoef; - - } else { - // channel 2 increasing (right) - pOutput[i] = gs.m_pDelayBuf[i] * gs.frac * lawCoef; - pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; - } - */ - // pOutput[i+1] = gs.m_pDelayBuf[i+1] * (1.0f - gs.frac) * lawCoef; - // pOutput[i] = gs.m_pDelayBuf[i] * gs.frac * lawCoef; - - pOutput[i] = pInput[i] * gs.frac * lawCoef; - pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac) * lawCoef; - -// pOutput[i] = pOutput[i] * gs.frac * lawCoef; -// pOutput[i+1] = pOutput[i+1] * (1.0f - gs.frac) * lawCoef; - -// pOutput[i] = gs.m_pDelayBuf[i] * gs.frac * lawCoef; -// pOutput[i+1] = gs.m_pDelayBuf[i+1] * (1.0f - gs.frac) * lawCoef; + pOutput[i] = pInput[i] * gs.frac; + pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac); gs.time++; } From 311263d2cd3cc5df5a5e6e2e67e77d828d27a4f1 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Tue, 7 Apr 2015 09:50:22 +0200 Subject: [PATCH 246/481] cleaning --- src/sampleutil.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sampleutil.cpp b/src/sampleutil.cpp index 4c6cdaf7a67..05ead33c56f 100644 --- a/src/sampleutil.cpp +++ b/src/sampleutil.cpp @@ -281,7 +281,6 @@ void SampleUtil::copyWithRampingGain(CSAMPLE* _RESTRICT pDest, const CSAMPLE* _R // applyRampingGain(pDest, gain); } - // static void SampleUtil::convertS16ToFloat32(CSAMPLE* _RESTRICT pDest, const SAMPLE* _RESTRICT pSrc, int iNumSamples) { From 571528e7694f2c58361ed7244effbabb5796873f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Sch=C3=BCrmann?= Date: Sat, 11 Apr 2015 23:03:39 +0200 Subject: [PATCH 247/481] Introduced a EngineFilterPan class --- src/effects/native/autopaneffect.cpp | 17 ++-- src/effects/native/autopaneffect.h | 6 +- src/engine/enginefilterdelay.h | 2 +- src/engine/enginefilterpan.h | 124 +++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 10 deletions(-) create mode 100644 src/engine/enginefilterpan.h diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index 501f90d850d..d96a49ae29c 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -57,10 +57,13 @@ EffectManifest AutoPanEffect::getManifest() { delay->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); delay->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); delay->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); - delay->setMinimum(0.0); -// delay->setMaximum(0.02); // 0.02 * sampleRate => 20ms - delay->setMaximum(0.1); // 0.02 * sampleRate => 20ms - delay->setDefault(0.01); + // 10 ms seams to be more then enough here for test purpose + // it works for me up to ~5 ms using my laptop speakers. + // for 40 ms you have an echo + // http://en.wikipedia.org/wiki/Precedence_effect + delay->setMinimum(-0.01); + delay->setMaximum(0.01); + delay->setDefault(0.0); // Width : applied on the channel with gain reducing. EffectManifestParameter* width = manifest.addParameter(); @@ -140,8 +143,9 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p // prepare the delay buffer float delay = round(m_pDelayParameter->value() * sampleRate); - gs.delay->setDelay(delay); + gs.delay->setLeftDelay(delay); + /* for (unsigned int i = 0; i + 1 < numSamples; i += 2) { CSAMPLE periodFraction = CSAMPLE(gs.time) / period; @@ -177,8 +181,9 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p gs.time++; } + */ - gs.delay->process(pOutput, pOutput, numSamples); + gs.delay->process(pInput, pOutput, numSamples); // SampleUtil::addWithGain(pOutput, // pOutput, 0.9, diff --git a/src/effects/native/autopaneffect.h b/src/effects/native/autopaneffect.h index 7ef427400f6..2ca4f460e77 100644 --- a/src/effects/native/autopaneffect.h +++ b/src/effects/native/autopaneffect.h @@ -10,7 +10,7 @@ #include "engine/effects/engineeffectparameter.h" #include "effects/effectprocessor.h" #include "sampleutil.h" -#include "engine/enginefilterdelay.h" +#include "engine/enginefilterpan.h" // This class provides a float value that cannot be increased or decreased @@ -65,7 +65,7 @@ static const int panMaxDelay = 3300; // allows a 30 Hz filter at 97346; struct PanGroupState { PanGroupState() { time = 0; - delay = new EngineFilterDelay(); + delay = new EngineFilterPan(); m_pDelayBuf = SampleUtil::alloc(MAX_BUFFER_LEN); } ~PanGroupState() { @@ -73,7 +73,7 @@ struct PanGroupState { } unsigned int time; RampedSample frac; - EngineFilterDelay* delay; + EngineFilterPan* delay; CSAMPLE* m_pDelayBuf; }; diff --git a/src/engine/enginefilterdelay.h b/src/engine/enginefilterdelay.h index 77c24be7c1e..0f46154c1b6 100644 --- a/src/engine/enginefilterdelay.h +++ b/src/engine/enginefilterdelay.h @@ -115,4 +115,4 @@ class EngineFilterDelay : public EngineObjectConstIn { bool m_doStart; }; -#endif // ENGINEFILTERIIR_H +#endif // ENGINEFILTERDELAY_H diff --git a/src/engine/enginefilterpan.h b/src/engine/enginefilterpan.h new file mode 100644 index 00000000000..ba61c021427 --- /dev/null +++ b/src/engine/enginefilterpan.h @@ -0,0 +1,124 @@ +#ifndef ENGINEFILTERPAN_H +#define ENGINEFILTERPAN_H + +#include + +#include "engine/engineobject.h" +#include "util/assert.h" + +static const int numChannels = 2; + +template +class EngineFilterPan : public EngineObjectConstIn { + public: + EngineFilterPan() + : m_leftDelayFrames(0), + m_oldLeftDelayFrames(0), + m_delayFrame(0), + m_doRamping(false), + m_doStart(false) { + // Set the current buffers to 0 + memset(m_buf, 0, sizeof(m_buf)); + } + + virtual ~EngineFilterPan() {}; + + void pauseFilter() { + // Set the current buffers to 0 + if (!m_doStart) { + memset(m_buf, 0, sizeof(m_buf)); + m_doStart = true; + } + } + + void setLeftDelay(unsigned int leftDelaySamples) { + if (m_leftDelayFrames != leftDelaySamples) { + m_oldLeftDelayFrames = m_leftDelayFrames; + m_leftDelayFrames = leftDelaySamples; + m_doRamping = true; + } + } + + virtual void process(const CSAMPLE* pIn, CSAMPLE* pOutput, + const int iBufferSize) { + int delayLeftSourceFrame = m_delayFrame; + int delayRightSourceFrame = m_delayFrame; + if (m_leftDelayFrames > 0) { + delayLeftSourceFrame = (m_delayFrame + SIZE - m_leftDelayFrames) % SIZE; + } else { + delayRightSourceFrame = (m_delayFrame + SIZE + m_leftDelayFrames) % SIZE; + } + + DEBUG_ASSERT_AND_HANDLE(delayLeftSourceFrame >= 0 && + delayRightSourceFrame >= 0 && + delayLeftSourceFrame <= static_cast(SIZE) && + delayRightSourceFrame <= static_cast(SIZE) ) { + SampleUtil::copy(pOutput, pIn, iBufferSize); + return; + } + + if (!m_doRamping) { + for (int i = 0; i < iBufferSize / 2; ++i) { + // put sample into delay buffer: + m_buf[m_delayFrame * 2] = pIn[i * 2]; + m_buf[m_delayFrame * 2 + 1] = pIn[i * 2 + 1]; + m_delayFrame = (m_delayFrame + 1) % SIZE; + + // Take delayed sample from delay buffer and copy it to dest buffer: + pOutput[i * 2] = m_buf[delayLeftSourceFrame * 2]; + pOutput[i * 2 + 1] = m_buf[delayRightSourceFrame * 2 + 1]; + delayLeftSourceFrame = (delayLeftSourceFrame + 1) % SIZE; + delayRightSourceFrame = (delayRightSourceFrame + 1) % SIZE; + } + } else { + int delayOldLeftSourceFrame = m_delayFrame; + int delayOldRightSourceFrame = m_delayFrame; + if (m_oldLeftDelayFrames > 0) { + delayOldLeftSourceFrame = (m_delayFrame + SIZE - m_oldLeftDelayFrames) % SIZE; + } else { + delayOldRightSourceFrame = (m_delayFrame + SIZE + m_oldLeftDelayFrames) % SIZE; + } + + DEBUG_ASSERT_AND_HANDLE(delayOldLeftSourceFrame >= 0 && + delayOldRightSourceFrame >= 0 && + delayOldLeftSourceFrame <= static_cast(SIZE) && + delayOldRightSourceFrame <= static_cast(SIZE) ) { + SampleUtil::copy(pOutput, pIn, iBufferSize); + return; + } + + double cross_mix = 0.0; + double cross_inc = 2 / static_cast(iBufferSize); + + for (int i = 0; i < iBufferSize / 2; ++i) { + // put sample into delay buffer: + m_buf[m_delayFrame * 2] = pIn[i * 2]; + m_buf[m_delayFrame * 2 + 1] = pIn[i * 2 + 1]; + m_delayFrame = (m_delayFrame + 1) % SIZE; + + // Take delayed sample from delay buffer and copy it to dest buffer: + cross_mix += cross_inc; + pOutput[i * 2] = m_buf[delayLeftSourceFrame * 2] * cross_mix; + pOutput[i * 2 + 1] = m_buf[delayRightSourceFrame * 2 + 1] * cross_mix; + pOutput[i * 2] += m_buf[delayOldLeftSourceFrame * 2] * (1 - cross_mix); + pOutput[i * 2 + 1] += m_buf[delayOldRightSourceFrame * 2 + 1] * (1 - cross_mix); + delayLeftSourceFrame = (delayLeftSourceFrame + 1) % SIZE; + delayRightSourceFrame = (delayRightSourceFrame + 1) % SIZE; + delayOldLeftSourceFrame = (delayOldLeftSourceFrame + 1) % SIZE; + delayOldRightSourceFrame = (delayOldRightSourceFrame + 1) % SIZE; + } + m_doRamping = false; + } + m_doStart = false; + } + + protected: + int m_leftDelayFrames; + int m_oldLeftDelayFrames; + int m_delayFrame; + CSAMPLE m_buf[SIZE * numChannels]; + bool m_doRamping; + bool m_doStart; +}; + +#endif // ENGINEFILTERPAN_H From 0167ce2c0458f117aec7f1c3f5f38826feb8075f Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Tue, 7 Apr 2015 09:50:22 +0200 Subject: [PATCH 248/481] cleaning From 77ff751099408be5832edf7e3db51fbff85374cd Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Tue, 7 Apr 2015 09:50:22 +0200 Subject: [PATCH 249/481] cleaning From b1b0fb18e37be168d5df88268862ed4577860234 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Mon, 13 Apr 2015 11:46:24 +0200 Subject: [PATCH 250/481] reenabling pan curve --- src/effects/native/autopaneffect.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/effects/native/autopaneffect.cpp b/src/effects/native/autopaneffect.cpp index d96a49ae29c..86c66e5b5a2 100644 --- a/src/effects/native/autopaneffect.cpp +++ b/src/effects/native/autopaneffect.cpp @@ -145,7 +145,8 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p float delay = round(m_pDelayParameter->value() * sampleRate); gs.delay->setLeftDelay(delay); - /* + gs.delay->process(pInput, pOutput, numSamples); + for (unsigned int i = 0; i + 1 < numSamples; i += 2) { CSAMPLE periodFraction = CSAMPLE(gs.time) / period; @@ -176,14 +177,11 @@ void AutoPanEffect::processChannel(const ChannelHandle& handle, PanGroupState* p gs.frac.setWithRampingApplied( (sin(M_PI * 2.0f * angleFraction) * width + 1.0f) / 2.0f); - pOutput[i] = pInput[i] * gs.frac; - pOutput[i+1] = pInput[i+1] * (1.0f - gs.frac); + pOutput[i] = pOutput[i] * gs.frac; + pOutput[i+1] = pOutput[i+1] * (1.0f - gs.frac); gs.time++; } - */ - - gs.delay->process(pInput, pOutput, numSamples); // SampleUtil::addWithGain(pOutput, // pOutput, 0.9, From 963ec9cb691a224a570152464a38b128224c2c03 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 13 Apr 2015 16:53:22 +0200 Subject: [PATCH 251/481] Add log messages about unsupported MP3 files --- src/sources/soundsourcemp3.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index cfbdd62ead0..2d039894399 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -139,6 +139,7 @@ Result SoundSourceMp3::tryOpen(SINT /*channelCountHint*/) { qWarning() << "Differing number of channels in some headers:" << m_file.fileName() << getChannelCount() << "<>" << madChannelCount; + qWarning() << "MP3 files with varying channel configurations are not supported!"; // Abort mad_header_finish(&madHeader); return ERR; @@ -189,6 +190,7 @@ Result SoundSourceMp3::tryOpen(SINT /*channelCountHint*/) { qWarning() << "Differing sample rate in some headers:" << m_file.fileName() << getFrameRate() << "<>" << madSampleRate; + qWarning() << "MP3 files with varying sample rate are not supported!"; // Abort mad_header_finish(&madHeader); return ERR; From c43c3d29cc611432678cd6d6ac0ea4be6215db15 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 14 Apr 2015 17:23:05 +0200 Subject: [PATCH 252/481] Improve handling of decoding errors in MP3 frames --- src/sources/soundsourcemp3.cpp | 258 +++++++++++++++++++-------------- 1 file changed, 147 insertions(+), 111 deletions(-) diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 2d039894399..67a8648c0f1 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -28,33 +28,67 @@ const SINT kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 const SINT kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; -int decodeFrameHeader( +void logFrameHeader(QDebug logger, const mad_header& madHeader) { + logger << "MP3 frame header |" + << "layer:" << madHeader.layer + << "mode:" << madHeader.mode + << "#channels:" << MAD_NCHANNELS(&madHeader) + << "#samples:" << MAD_NSBSAMPLES(&madHeader) + << "bitrate:" << madHeader.bitrate + << "samplerate:" << madHeader.samplerate + << "flags:" << madHeader.flags; +} + +inline bool isRecoverableError(int madError) { + return MAD_RECOVERABLE(madError); +} + +inline bool isUnrecoverableError(int madError) { + return (MAD_ERROR_NONE != madError) && !isRecoverableError(madError); +} + +inline bool isStreamValid(const mad_stream& madStream) { + return !isUnrecoverableError(madStream.error); +} + +bool decodeFrameHeader( mad_header* pMadHeader, mad_stream* pMadStream, bool skipId3Tag) { - const int result = mad_header_decode(pMadHeader, pMadStream); - if (0 != result) { - if (MAD_RECOVERABLE(pMadStream->error)) { - if ((MAD_ERROR_LOSTSYNC == pMadStream->error) && skipId3Tag) { - long tagsize = id3_tag_query(pMadStream->this_frame, - pMadStream->bufend - pMadStream->this_frame); - if (0 < tagsize) { - // Skip ID3 tag data - mad_stream_skip(pMadStream, tagsize); - // Suppress lost synchronization warnings - return result; - } - } - qWarning() << "Recoverable MP3 header decoding error:" - << mad_stream_errorstr(pMadStream); - } else { - if (MAD_ERROR_BUFLEN != pMadStream->error) { // EOF - qWarning() << "Unrecoverable MP3 header decoding error:" - << mad_stream_errorstr(pMadStream); + DEBUG_ASSERT(isStreamValid(*pMadStream)); + const int decodeResult = mad_header_decode(pMadHeader, pMadStream); + if (MAD_ERROR_BUFLEN == decodeResult) { + // EOF + return false; + } + if (isUnrecoverableError(decodeResult)) { + DEBUG_ASSERT(!isStreamValid(*pMadStream)); + qWarning() << "Unrecoverable MP3 header decoding error:" + << mad_stream_errorstr(pMadStream); + return false; + } +#ifndef QT_NO_DEBUG_OUTPUT + logFrameHeader(qDebug(), *pMadHeader); +#endif + if (isRecoverableError(decodeResult)) { + if ((MAD_ERROR_LOSTSYNC == decodeResult) && skipId3Tag) { + long tagsize = id3_tag_query(pMadStream->this_frame, + pMadStream->bufend - pMadStream->this_frame); + if (0 < tagsize) { + // Skip ID3 tag data + mad_stream_skip(pMadStream, tagsize); + // Return immediately to suppress lost + // synchronization warnings + return false; } } + qWarning() << "Recoverable MP3 header decoding error:" + << mad_stream_errorstr(pMadStream); + logFrameHeader(qWarning(), *pMadHeader); + return false; } - return result; + DEBUG_ASSERT(isStreamValid(*pMadStream)); + return true; } } // anonymous namespace @@ -81,6 +115,9 @@ SoundSourceMp3::~SoundSourceMp3() { } Result SoundSourceMp3::tryOpen(SINT /*channelCountHint*/) { + DEBUG_ASSERT(!isChannelCountValid()); + DEBUG_ASSERT(!isFrameRateValid()); + DEBUG_ASSERT(!m_file.isOpen()); if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file:" << m_file.fileName(); @@ -105,96 +142,91 @@ Result SoundSourceMp3::tryOpen(SINT /*channelCountHint*/) { // Decode all the headers and calculate audio properties - mad_units madUnits = MAD_UNITS_44100_HZ; // default value mad_timer_t madDuration = mad_timer_zero; unsigned long sumBitrate = 0; mad_header madHeader; mad_header_init(&madHeader); + do { - if (0 != decodeFrameHeader(&madHeader, &m_madStream, true)) { - if (MAD_RECOVERABLE(m_madStream.error)) { + if (!decodeFrameHeader(&madHeader, &m_madStream, true)) { + if (isStreamValid(m_madStream)) { + // Skip frame continue; } else { - if (MAD_ERROR_BUFLEN == m_madStream.error) { - // EOF - break; - } else { - // Abort - mad_header_finish(&madHeader); - return ERR; - } + // Abort decoding + break; } } // Grab data from madHeader const SINT madChannelCount = MAD_NCHANNELS(&madHeader); - if (kChannelCountDefault == getChannelCount()) { - // initially set the number of channels - setChannelCount(madChannelCount); - } else { + if (isChannelCountValid()) { // check for consistent number of channels - if ((0 < madChannelCount) - && (getChannelCount() != madChannelCount)) { + if (getChannelCount() != madChannelCount) { qWarning() << "Differing number of channels in some headers:" - << m_file.fileName() << getChannelCount() << "<>" - << madChannelCount; + << m_file.fileName() + << madChannelCount << "<>" << getChannelCount(); qWarning() << "MP3 files with varying channel configurations are not supported!"; // Abort mad_header_finish(&madHeader); return ERR; } + } else { + // initially set the number of channels + setChannelCount(madChannelCount); } const SINT madSampleRate = madHeader.samplerate; - if (kFrameRateDefault == getFrameRate()) { - // initially set the frame/sample rate - setFrameRate(madSampleRate); - switch (madSampleRate) { - case 8000: - madUnits = MAD_UNITS_8000_HZ; - break; - case 11025: - madUnits = MAD_UNITS_11025_HZ; - break; - case 12000: - madUnits = MAD_UNITS_12000_HZ; - break; - case 16000: - madUnits = MAD_UNITS_16000_HZ; - break; - case 22050: - madUnits = MAD_UNITS_22050_HZ; - break; - case 24000: - madUnits = MAD_UNITS_24000_HZ; - break; - case 32000: - madUnits = MAD_UNITS_32000_HZ; - break; - case 44100: - madUnits = MAD_UNITS_44100_HZ; - break; - case 48000: - madUnits = MAD_UNITS_48000_HZ; - break; - default: - qWarning() << "Invalid sample rate:" << m_file.fileName() - << madSampleRate; - // Abort - mad_header_finish(&madHeader); - return ERR; - } - } else { + mad_units madUnits; + switch (madSampleRate) { + case 8000: + madUnits = MAD_UNITS_8000_HZ; + break; + case 11025: + madUnits = MAD_UNITS_11025_HZ; + break; + case 12000: + madUnits = MAD_UNITS_12000_HZ; + break; + case 16000: + madUnits = MAD_UNITS_16000_HZ; + break; + case 22050: + madUnits = MAD_UNITS_22050_HZ; + break; + case 24000: + madUnits = MAD_UNITS_24000_HZ; + break; + case 32000: + madUnits = MAD_UNITS_32000_HZ; + break; + case 44100: + madUnits = MAD_UNITS_44100_HZ; + break; + case 48000: + madUnits = MAD_UNITS_48000_HZ; + break; + default: + qWarning() << "Invalid sample rate:" << m_file.fileName() + << madSampleRate; + // Abort + mad_header_finish(&madHeader); + return ERR; + } + if (isFrameRateValid()) { // check for consistent frame/sample rate - if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { + if (getFrameRate() != madSampleRate) { qWarning() << "Differing sample rate in some headers:" << m_file.fileName() - << getFrameRate() << "<>" << madSampleRate; + << madSampleRate << "<>" << getFrameRate(); qWarning() << "MP3 files with varying sample rate are not supported!"; // Abort mad_header_finish(&madHeader); return ERR; } + } else { + // initially set the frame/sample rate + setFrameRate(madSampleRate); } addSeekFrame(m_curFrameIndex, m_madStream.this_frame); @@ -207,7 +239,7 @@ Result SoundSourceMp3::tryOpen(SINT /*channelCountHint*/) { m_curFrameIndex = kFrameIndexMin + mad_timer_count(madDuration, madUnits); - DEBUG_ASSERT(NULL != m_madStream.this_frame); + DEBUG_ASSERT(m_madStream.this_frame); DEBUG_ASSERT(0 < (m_madStream.this_frame - m_pFileData)); } while (quint64(m_madStream.this_frame - m_pFileData) < m_fileSize); @@ -303,8 +335,8 @@ SINT SoundSourceMp3::restartDecoding( mad_synth_mute(&m_madSynth); } - if (0 != decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) { - if (!MAD_RECOVERABLE(m_madStream.error)) { + if (!decodeFrameHeader(&m_madFrame.header, &m_madStream, false)) { + if (!isStreamValid(m_madStream)) { // Failure -> Seek to EOF return getFrameCount(); } @@ -451,40 +483,42 @@ SINT SoundSourceMp3::readSampleFrames( // When all decoded output data has been consumed... DEBUG_ASSERT(0 == m_madSynthCount); // ...decode the next MP3 frame - DEBUG_ASSERT(NULL != m_madStream.buffer); - DEBUG_ASSERT(NULL != m_madStream.this_frame); + DEBUG_ASSERT(m_madStream.buffer); + DEBUG_ASSERT(m_madStream.this_frame); // WARNING: Correctly evaluating and handling the result // of mad_frame_decode() has proven to be extremely tricky. // Don't change anything at the following lines of code // unless you know what you are doing!!! unsigned char const* pMadThisFrame = m_madStream.this_frame; - if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { - if (MAD_RECOVERABLE(m_madStream.error)) { - if (pMadThisFrame != m_madStream.this_frame) { - // Ignore all recoverable errors (and especially - // "lost synchronization" warnings) while skipping - // over prefetched frames after seeking. - if (NULL == pSampleBuffer) { - // Decoded samples will simply be discarded - qDebug() << "Recoverable MP3 frame decoding error while skipping:" - << mad_stream_errorstr(&m_madStream); - } else { - qWarning() << "Recoverable MP3 frame decoding error:" - << mad_stream_errorstr(&m_madStream); - } - } - // Acknowledge error... - m_madStream.error = MAD_ERROR_NONE; - // ...and continue - } else { - if (MAD_ERROR_BUFLEN != m_madStream.error) { - qWarning() << "Unrecoverable MP3 frame decoding error:" + const int decodeResult = mad_frame_decode(&m_madFrame, &m_madStream); + if (MAD_ERROR_BUFLEN == decodeResult) { + // Abort + break; + } + if (isUnrecoverableError(decodeResult)) { + qWarning() << "Unrecoverable MP3 frame decoding error:" + << mad_stream_errorstr(&m_madStream); + // Abort + break; + } + if (isRecoverableError(decodeResult)) { + if (pMadThisFrame != m_madStream.this_frame) { + // Ignore all recoverable errors (and especially + // "lost synchronization" warnings) while skipping + // over prefetched frames after seeking. + if (pSampleBuffer) { + qWarning() << "Recoverable MP3 frame decoding error:" << mad_stream_errorstr(&m_madStream); + } else { + // Decoded samples will simply be discarded + qDebug() << "Recoverable MP3 frame decoding error while skipping:" + << mad_stream_errorstr(&m_madStream); } - // Abort - break; } + // Acknowledge error... + m_madStream.error = MAD_ERROR_NONE; + // ...and continue } if (pMadThisFrame == m_madStream.this_frame) { qDebug() << "Retry decoding MP3 frame @" << m_curFrameIndex; @@ -492,18 +526,20 @@ SINT SoundSourceMp3::readSampleFrames( continue; } + DEBUG_ASSERT(isStreamValid(m_madStream)); DEBUG_ASSERT(getChannelCount() == MAD_NCHANNELS(&m_madFrame.header)); // Once decoded the frame is synthesized to PCM samples mad_synth_frame(&m_madSynth, &m_madFrame); - DEBUG_ASSERT(getFrameRate() == m_madSynth.pcm.samplerate); + // The following assumption seems to be violated by some files! + //DEBUG_ASSERT(getFrameRate() == m_madSynth.pcm.samplerate); m_madSynthCount = m_madSynth.pcm.length; DEBUG_ASSERT(0 < m_madSynthCount); } const SINT synthReadCount = math_min( m_madSynthCount, numberOfFramesRemaining); - if (NULL != pSampleBuffer) { + if (pSampleBuffer) { DEBUG_ASSERT(m_madSynthCount <= m_madSynth.pcm.length); const SINT madSynthOffset = m_madSynth.pcm.length - m_madSynthCount; From 3110a207e41f286fd940d54b4f6ba242442fde2f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 14 Apr 2015 17:38:02 +0200 Subject: [PATCH 253/481] Re-enable debug assertion --- src/sources/soundsourcemp3.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 67a8648c0f1..0518ef973a0 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -531,8 +531,7 @@ SINT SoundSourceMp3::readSampleFrames( // Once decoded the frame is synthesized to PCM samples mad_synth_frame(&m_madSynth, &m_madFrame); - // The following assumption seems to be violated by some files! - //DEBUG_ASSERT(getFrameRate() == m_madSynth.pcm.samplerate); + DEBUG_ASSERT(getFrameRate() == m_madSynth.pcm.samplerate); m_madSynthCount = m_madSynth.pcm.length; DEBUG_ASSERT(0 < m_madSynthCount); } From 71b9a1fd5f9589fbccb9b6548b620ac4857445fd Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 14 Apr 2015 18:41:39 +0200 Subject: [PATCH 254/481] Disable excessive debug logging of MP3 frame headers --- src/sources/soundsourcemp3.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 0518ef973a0..149824c7193 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -68,7 +68,9 @@ bool decodeFrameHeader( return false; } #ifndef QT_NO_DEBUG_OUTPUT - logFrameHeader(qDebug(), *pMadHeader); + // Logging of MP3 frame headers should only be enabled + // for debugging purposes. + //logFrameHeader(qDebug(), *pMadHeader); #endif if (isRecoverableError(decodeResult)) { if ((MAD_ERROR_LOSTSYNC == decodeResult) && skipId3Tag) { From d391b6f9bef983f63e959a20845daa39c4bfa0da Mon Sep 17 00:00:00 2001 From: Tuukka Pasanen Date: Wed, 15 Apr 2015 08:50:30 +0300 Subject: [PATCH 255/481] Fixed native FFmpeg playing with New Sound API whic uses Float point reading. Few FFmpeg/AVConv specific points: * FFmpeg decodes files to bytes. So old one used S16 (Streo 16 bit) which was as bytewise LLRR new system uses Float (Stereo 32 bit) which is LLLLRRRR. * Native system with Soundsource uses 'Frames' which is one Float number from channel. If you want to have FFmpeg filepoint converted to Frames (we always convert to Stereo) is calculated FrameIndex = bytes / (4 * 2) * FFmpeg soundsource should be done compatible with tests before Mixxx version 1.13 --- src/sources/soundsourceffmpeg.cpp | 66 ++++++++++++------------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index 2c6ab802c6f..c94954d70fe 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -65,7 +65,7 @@ Result SoundSourceFFmpeg::tryOpen(SINT channelCountHint) { AVDictionary *l_iFormatOpts = NULL; const QByteArray qBAFilename(getLocalFileNameBytes()); - qDebug() << "New SoundSourceFFmpeg :" << qBAFilename; + qDebug() << "New SoundSourceFFmpeg :" << qBAFilename << "(channelCountHint:" << channelCountHint << ")"; DEBUG_ASSERT(!m_pFormatCtx); m_pFormatCtx = avformat_alloc_context(); @@ -123,8 +123,7 @@ Result SoundSourceFFmpeg::tryOpen(SINT channelCountHint) { } m_pResample = new EncoderFfmpegResample(m_pCodecCtx); - // TODO(XXX): Use AV_SAMPLE_FMT_FLT instead of AV_SAMPLE_FMT_S16 - m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_S16); + m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_FLT); setChannelCount(m_pCodecCtx->channels); setFrameRate(m_pCodecCtx->sample_rate); @@ -266,9 +265,9 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { // Add to cache and store byte place to memory m_SCache.append(l_SObj); - l_SObj->startByte = m_lCacheBytePos / 2; - l_SObj->length = l_iRet / 2; - m_lCacheBytePos += l_iRet; + l_SObj->startByte = m_lCacheBytePos; + l_SObj->length = l_iRet; + m_lCacheBytePos += l_iRet / (sizeof(CSAMPLE) * 2); // Ogg/Opus have packages pos that have many // audio frames so seek next unique pos.. @@ -284,14 +283,14 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { struct ffmpegLocationObject *l_SJmp = (struct ffmpegLocationObject *)malloc( sizeof(struct ffmpegLocationObject)); m_lLastStoredPos = m_lCacheBytePos; - l_SJmp->startByte = m_lCacheBytePos / 2; + l_SJmp->startByte = m_lCacheBytePos; l_SJmp->pos = l_SPacket.pos; l_SJmp->pts = l_SPacket.pts; m_SJumpPoints.append(l_SJmp); m_bUnique = false; } - if (offset < 0 || (quint64) offset <= (m_lCacheBytePos / 2)) { + if (offset < 0 || (quint64) offset <= m_lCacheBytePos) { l_iCount --; } } else { @@ -377,7 +376,7 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, l_SObj = m_SCache[l_lPos]; - l_lLeft = (size * 2); + l_lLeft = (size * sizeof(CSAMPLE)) * 2; memset(buffer, 0x00, l_lLeft); while (l_lLeft > 0) { @@ -397,16 +396,16 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, } if (l_SObj->startByte <= offset) { - l_lOffset = (offset - l_SObj->startByte) * 2; + l_lOffset = (offset - l_SObj->startByte) * (sizeof(CSAMPLE) * 2); } - if (l_lOffset >= (l_SObj->length * 2)) { + if (l_lOffset >= l_SObj->length) { l_SObj = m_SCache[++ l_lPos]; continue; } - if (l_lLeft > (l_SObj->length * 2)) { - l_lBytesToCopy = ((l_SObj->length * 2) - l_lOffset); + if (l_lLeft > l_SObj->length) { + l_lBytesToCopy = l_SObj->length - l_lOffset; memcpy(buffer, (l_SObj->bytes + l_lOffset), l_lBytesToCopy); l_lOffset = 0; buffer += l_lBytesToCopy; @@ -429,12 +428,10 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - const SINT filepos = frames2samples(frameIndex); - int ret = 0; qint64 i = 0; - if (filepos < 0 || (unsigned long) filepos < m_lCacheStartByte) { + if (frameIndex < 0 || (unsigned long) frameIndex < m_lCacheStartByte) { ret = avformat_seek_file(m_pFormatCtx, m_iAudioStream, 0, @@ -459,9 +456,9 @@ SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { // Try to find some jump point near to // where we are located so we don't needed // to try guess it - if (filepos >= AUDIOSOURCEFFMPEG_POSDISTANCE) { + if (frameIndex >= AUDIOSOURCEFFMPEG_POSDISTANCE) { for (i = 0; i < m_SJumpPoints.size(); i ++) { - if (m_SJumpPoints[i]->startByte >= (unsigned long) filepos && i > 2) { + if (m_SJumpPoints[i]->startByte >= (unsigned long) frameIndex && i > 2) { m_lCacheBytePos = m_SJumpPoints[i - 2]->startByte * 2; m_lStoredSeekPoint = m_SJumpPoints[i - 2]->pos; break; @@ -469,26 +466,27 @@ SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { } } - if (filepos == 0) { + if (frameIndex == 0) { readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE - 50), -1); } else { - readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE / 2), filepos); + readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE / 2), frameIndex); } } - if (m_lCacheEndByte <= (unsigned long) filepos) { - readFramesToCache(100, filepos); + if (m_lCacheEndByte <= (unsigned long) frameIndex) { + readFramesToCache(100, frameIndex); } - m_iCurrentMixxTs = filepos; + m_iCurrentMixxTs = frameIndex; m_bIsSeeked = TRUE; return frameIndex; } -unsigned int SoundSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { +SINT SoundSourceFFmpeg::readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) { if (m_SCache.size() == 0) { // Make sure we allways start at begining and cache have some @@ -497,32 +495,18 @@ unsigned int SoundSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { m_bIsSeeked = FALSE; } - getBytesFromCache((char *)destination, m_iCurrentMixxTs, size); + getBytesFromCache((char *)sampleBuffer, m_iCurrentMixxTs, numberOfFrames); // As this is also Hack // If we don't seek like we don't on analyzer.. keep // place in mind.. if (m_bIsSeeked == FALSE) { - m_iCurrentMixxTs += size; + m_iCurrentMixxTs += numberOfFrames; } m_bIsSeeked = FALSE; - return size; -} -SINT SoundSourceFFmpeg::readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) { - // This is just a hack that simply reuses existing - // functionality. Sample data should be resampled - // directly into AV_SAMPLE_FMT_FLT instead of - // AV_SAMPLE_FMT_S16! - typedef std::vector TempBuffer; - TempBuffer tempBuffer(frames2samples(numberOfFrames)); - const SINT readSamples = read(tempBuffer.size(), &tempBuffer[0]); - for (SINT i = 0; i < readSamples; ++i) { - sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / CSAMPLE(SAMPLE_MAX); - } - return samples2frames(readSamples); + return numberOfFrames; } } // namespace Mixxx From 7bcd14b1be7f911e4f11bc7dc8743978904a9e3a Mon Sep 17 00:00:00 2001 From: Tuukka Pasanen Date: Wed, 15 Apr 2015 09:11:27 +0300 Subject: [PATCH 256/481] Added some comments how caching and FFmpeg/AVConv really works --- src/sources/soundsourceffmpeg.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index c94954d70fe..51bd0a7ecad 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -220,7 +220,12 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { l_pFrame = av_frame_alloc(); #endif + // Read one frame (which has nothing to do with Mixxx Frame) + // it's some packed audio data from container like MP3, Ogg or MP4 if (av_read_frame(m_pFormatCtx, &l_SPacket) >= 0) { + // Are we on correct audio stream. Currently we are always + // Using first audio stream but in future there should be + // possibility to choose which to use if (l_SPacket.stream_index == m_iAudioStream) { if (m_lStoredSeekPoint > 0) { // Seek for correct jump point @@ -234,6 +239,7 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { m_lStoredSeekPoint = -1; } + // Decode audio bytes (These can be S16P or FloatP [P is Planar]) l_iRet = avcodec_decode_audio4(m_pCodecCtx,l_pFrame,&l_iFrameFinished, &l_SPacket); @@ -252,6 +258,9 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { continue; } memset(l_SObj, 0x00, sizeof(struct ffmpegCacheObject)); + + // Try to convert it to Mixxx understand output format + // which is pure Stereo Float l_iRet = m_pResample->reSample(l_pFrame, &l_SObj->bytes); if (l_iRet > 0) { @@ -363,10 +372,14 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, quint32 l_lOffset = 0; quint32 l_lBytesToCopy = 0; + // Is offset bigger than start of cache if (offset >= m_lCacheStartByte) { + // If last pos is (which is shouldn't) use caches end if (m_lCacheLastPos == 0) { m_lCacheLastPos = m_SCache.size() - 1; } + + // Seek to correct FrameIndex for (l_lPos = m_lCacheLastPos; l_lPos > 0; l_lPos --) { l_SObj = m_SCache[l_lPos]; if ((l_SObj->startByte + l_SObj->length) < offset) { @@ -374,17 +387,21 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, } } + // Use this Cache object as starting point l_SObj = m_SCache[l_lPos]; + // Calculate in other words get bytes how much we must copy to + // buffer (CSAMPLE = 4 and we have 2 channels which is 8 times) l_lLeft = (size * sizeof(CSAMPLE)) * 2; memset(buffer, 0x00, l_lLeft); while (l_lLeft > 0) { - + // If Cache is running low read more if (l_SObj == NULL || (l_lPos + 5) > (unsigned int)m_SCache.size()) { offset = l_SObj->startByte; if (readFramesToCache(50, -1) == false) { return false; } + // Seek back to correct place for (l_lPos = (m_SCache.size() - 50); l_lPos > 0; l_lPos --) { l_SObj = m_SCache[l_lPos]; if ((l_SObj->startByte + l_SObj->length) < offset) { @@ -395,16 +412,20 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, continue; } + // If Cache object ain't correct then calculate offset if (l_SObj->startByte <= offset) { + // We have to convert again it to bytes l_lOffset = (offset - l_SObj->startByte) * (sizeof(CSAMPLE) * 2); } + // Okay somehow offset is bigger than our Cache object have bytes if (l_lOffset >= l_SObj->length) { l_SObj = m_SCache[++ l_lPos]; continue; } if (l_lLeft > l_SObj->length) { + // calculate start point of copy l_lBytesToCopy = l_SObj->length - l_lOffset; memcpy(buffer, (l_SObj->bytes + l_lOffset), l_lBytesToCopy); l_lOffset = 0; From 8ebca216e4c58f673c99ffd2bafc9fa3e9772f4f Mon Sep 17 00:00:00 2001 From: Tuukka Pasanen Date: Wed, 15 Apr 2015 08:50:30 +0300 Subject: [PATCH 257/481] Fixed native FFmpeg playing with New Sound API whic uses Float point reading. Few FFmpeg/AVConv specific points: * FFmpeg decodes files to bytes. So old one used S16 (Streo 16 bit) which was as bytewise LLRR new system uses Float (Stereo 32 bit) which is LLLLRRRR. * Native system with Soundsource uses 'Frames' which is one Float number from channel. If you want to have FFmpeg filepoint converted to Frames (we always convert to Stereo) is calculated FrameIndex = bytes / (4 * 2) * FFmpeg soundsource should be done compatible with tests before Mixxx version 1.13 --- src/sources/soundsourceffmpeg.cpp | 66 ++++++++++++------------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index 2c6ab802c6f..c94954d70fe 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -65,7 +65,7 @@ Result SoundSourceFFmpeg::tryOpen(SINT channelCountHint) { AVDictionary *l_iFormatOpts = NULL; const QByteArray qBAFilename(getLocalFileNameBytes()); - qDebug() << "New SoundSourceFFmpeg :" << qBAFilename; + qDebug() << "New SoundSourceFFmpeg :" << qBAFilename << "(channelCountHint:" << channelCountHint << ")"; DEBUG_ASSERT(!m_pFormatCtx); m_pFormatCtx = avformat_alloc_context(); @@ -123,8 +123,7 @@ Result SoundSourceFFmpeg::tryOpen(SINT channelCountHint) { } m_pResample = new EncoderFfmpegResample(m_pCodecCtx); - // TODO(XXX): Use AV_SAMPLE_FMT_FLT instead of AV_SAMPLE_FMT_S16 - m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_S16); + m_pResample->open(m_pCodecCtx->sample_fmt, AV_SAMPLE_FMT_FLT); setChannelCount(m_pCodecCtx->channels); setFrameRate(m_pCodecCtx->sample_rate); @@ -266,9 +265,9 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { // Add to cache and store byte place to memory m_SCache.append(l_SObj); - l_SObj->startByte = m_lCacheBytePos / 2; - l_SObj->length = l_iRet / 2; - m_lCacheBytePos += l_iRet; + l_SObj->startByte = m_lCacheBytePos; + l_SObj->length = l_iRet; + m_lCacheBytePos += l_iRet / (sizeof(CSAMPLE) * 2); // Ogg/Opus have packages pos that have many // audio frames so seek next unique pos.. @@ -284,14 +283,14 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { struct ffmpegLocationObject *l_SJmp = (struct ffmpegLocationObject *)malloc( sizeof(struct ffmpegLocationObject)); m_lLastStoredPos = m_lCacheBytePos; - l_SJmp->startByte = m_lCacheBytePos / 2; + l_SJmp->startByte = m_lCacheBytePos; l_SJmp->pos = l_SPacket.pos; l_SJmp->pts = l_SPacket.pts; m_SJumpPoints.append(l_SJmp); m_bUnique = false; } - if (offset < 0 || (quint64) offset <= (m_lCacheBytePos / 2)) { + if (offset < 0 || (quint64) offset <= m_lCacheBytePos) { l_iCount --; } } else { @@ -377,7 +376,7 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, l_SObj = m_SCache[l_lPos]; - l_lLeft = (size * 2); + l_lLeft = (size * sizeof(CSAMPLE)) * 2; memset(buffer, 0x00, l_lLeft); while (l_lLeft > 0) { @@ -397,16 +396,16 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, } if (l_SObj->startByte <= offset) { - l_lOffset = (offset - l_SObj->startByte) * 2; + l_lOffset = (offset - l_SObj->startByte) * (sizeof(CSAMPLE) * 2); } - if (l_lOffset >= (l_SObj->length * 2)) { + if (l_lOffset >= l_SObj->length) { l_SObj = m_SCache[++ l_lPos]; continue; } - if (l_lLeft > (l_SObj->length * 2)) { - l_lBytesToCopy = ((l_SObj->length * 2) - l_lOffset); + if (l_lLeft > l_SObj->length) { + l_lBytesToCopy = l_SObj->length - l_lOffset; memcpy(buffer, (l_SObj->bytes + l_lOffset), l_lBytesToCopy); l_lOffset = 0; buffer += l_lBytesToCopy; @@ -429,12 +428,10 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); - const SINT filepos = frames2samples(frameIndex); - int ret = 0; qint64 i = 0; - if (filepos < 0 || (unsigned long) filepos < m_lCacheStartByte) { + if (frameIndex < 0 || (unsigned long) frameIndex < m_lCacheStartByte) { ret = avformat_seek_file(m_pFormatCtx, m_iAudioStream, 0, @@ -459,9 +456,9 @@ SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { // Try to find some jump point near to // where we are located so we don't needed // to try guess it - if (filepos >= AUDIOSOURCEFFMPEG_POSDISTANCE) { + if (frameIndex >= AUDIOSOURCEFFMPEG_POSDISTANCE) { for (i = 0; i < m_SJumpPoints.size(); i ++) { - if (m_SJumpPoints[i]->startByte >= (unsigned long) filepos && i > 2) { + if (m_SJumpPoints[i]->startByte >= (unsigned long) frameIndex && i > 2) { m_lCacheBytePos = m_SJumpPoints[i - 2]->startByte * 2; m_lStoredSeekPoint = m_SJumpPoints[i - 2]->pos; break; @@ -469,26 +466,27 @@ SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { } } - if (filepos == 0) { + if (frameIndex == 0) { readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE - 50), -1); } else { - readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE / 2), filepos); + readFramesToCache((AUDIOSOURCEFFMPEG_CACHESIZE / 2), frameIndex); } } - if (m_lCacheEndByte <= (unsigned long) filepos) { - readFramesToCache(100, filepos); + if (m_lCacheEndByte <= (unsigned long) frameIndex) { + readFramesToCache(100, frameIndex); } - m_iCurrentMixxTs = filepos; + m_iCurrentMixxTs = frameIndex; m_bIsSeeked = TRUE; return frameIndex; } -unsigned int SoundSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { +SINT SoundSourceFFmpeg::readSampleFrames(SINT numberOfFrames, + CSAMPLE* sampleBuffer) { if (m_SCache.size() == 0) { // Make sure we allways start at begining and cache have some @@ -497,32 +495,18 @@ unsigned int SoundSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { m_bIsSeeked = FALSE; } - getBytesFromCache((char *)destination, m_iCurrentMixxTs, size); + getBytesFromCache((char *)sampleBuffer, m_iCurrentMixxTs, numberOfFrames); // As this is also Hack // If we don't seek like we don't on analyzer.. keep // place in mind.. if (m_bIsSeeked == FALSE) { - m_iCurrentMixxTs += size; + m_iCurrentMixxTs += numberOfFrames; } m_bIsSeeked = FALSE; - return size; -} -SINT SoundSourceFFmpeg::readSampleFrames(SINT numberOfFrames, - CSAMPLE* sampleBuffer) { - // This is just a hack that simply reuses existing - // functionality. Sample data should be resampled - // directly into AV_SAMPLE_FMT_FLT instead of - // AV_SAMPLE_FMT_S16! - typedef std::vector TempBuffer; - TempBuffer tempBuffer(frames2samples(numberOfFrames)); - const SINT readSamples = read(tempBuffer.size(), &tempBuffer[0]); - for (SINT i = 0; i < readSamples; ++i) { - sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / CSAMPLE(SAMPLE_MAX); - } - return samples2frames(readSamples); + return numberOfFrames; } } // namespace Mixxx From 8b9ce49e1d214a9fa29a6d0628053ef39b6b23dd Mon Sep 17 00:00:00 2001 From: Tuukka Pasanen Date: Wed, 15 Apr 2015 09:11:27 +0300 Subject: [PATCH 258/481] Added some comments how caching and FFmpeg/AVConv really works --- src/sources/soundsourceffmpeg.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index c94954d70fe..51bd0a7ecad 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -220,7 +220,12 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { l_pFrame = av_frame_alloc(); #endif + // Read one frame (which has nothing to do with Mixxx Frame) + // it's some packed audio data from container like MP3, Ogg or MP4 if (av_read_frame(m_pFormatCtx, &l_SPacket) >= 0) { + // Are we on correct audio stream. Currently we are always + // Using first audio stream but in future there should be + // possibility to choose which to use if (l_SPacket.stream_index == m_iAudioStream) { if (m_lStoredSeekPoint > 0) { // Seek for correct jump point @@ -234,6 +239,7 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { m_lStoredSeekPoint = -1; } + // Decode audio bytes (These can be S16P or FloatP [P is Planar]) l_iRet = avcodec_decode_audio4(m_pCodecCtx,l_pFrame,&l_iFrameFinished, &l_SPacket); @@ -252,6 +258,9 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { continue; } memset(l_SObj, 0x00, sizeof(struct ffmpegCacheObject)); + + // Try to convert it to Mixxx understand output format + // which is pure Stereo Float l_iRet = m_pResample->reSample(l_pFrame, &l_SObj->bytes); if (l_iRet > 0) { @@ -363,10 +372,14 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, quint32 l_lOffset = 0; quint32 l_lBytesToCopy = 0; + // Is offset bigger than start of cache if (offset >= m_lCacheStartByte) { + // If last pos is (which is shouldn't) use caches end if (m_lCacheLastPos == 0) { m_lCacheLastPos = m_SCache.size() - 1; } + + // Seek to correct FrameIndex for (l_lPos = m_lCacheLastPos; l_lPos > 0; l_lPos --) { l_SObj = m_SCache[l_lPos]; if ((l_SObj->startByte + l_SObj->length) < offset) { @@ -374,17 +387,21 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, } } + // Use this Cache object as starting point l_SObj = m_SCache[l_lPos]; + // Calculate in other words get bytes how much we must copy to + // buffer (CSAMPLE = 4 and we have 2 channels which is 8 times) l_lLeft = (size * sizeof(CSAMPLE)) * 2; memset(buffer, 0x00, l_lLeft); while (l_lLeft > 0) { - + // If Cache is running low read more if (l_SObj == NULL || (l_lPos + 5) > (unsigned int)m_SCache.size()) { offset = l_SObj->startByte; if (readFramesToCache(50, -1) == false) { return false; } + // Seek back to correct place for (l_lPos = (m_SCache.size() - 50); l_lPos > 0; l_lPos --) { l_SObj = m_SCache[l_lPos]; if ((l_SObj->startByte + l_SObj->length) < offset) { @@ -395,16 +412,20 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, continue; } + // If Cache object ain't correct then calculate offset if (l_SObj->startByte <= offset) { + // We have to convert again it to bytes l_lOffset = (offset - l_SObj->startByte) * (sizeof(CSAMPLE) * 2); } + // Okay somehow offset is bigger than our Cache object have bytes if (l_lOffset >= l_SObj->length) { l_SObj = m_SCache[++ l_lPos]; continue; } if (l_lLeft > l_SObj->length) { + // calculate start point of copy l_lBytesToCopy = l_SObj->length - l_lOffset; memcpy(buffer, (l_SObj->bytes + l_lOffset), l_lBytesToCopy); l_lOffset = 0; From 4404cb7870cb64bec66a70280be0e9a933c4b3b4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 15 Apr 2015 12:44:29 +0200 Subject: [PATCH 259/481] Fix conversion from QString to TagLib::String Always convert strings through UTF-8. TagLib might apply additional conversions internally, but we do not need to take care of that. --- src/metadata/trackmetadatataglib.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 57f82e854c8..6410bee108f 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -162,9 +162,9 @@ inline QString toQString(const TagLib::APE::Item& apeItem) { return toQString(apeItem.toString()); } -inline TagLib::String toTagLibString(const QString& str, TagLib::String::Type stringType = TagLib::String::UTF8) { +inline TagLib::String toTagLibString(const QString& str) { const QByteArray qba(str.toUtf8()); - return TagLib::String(qba.constData(), stringType); + return TagLib::String(qba.constData(), TagLib::String::UTF8); } inline bool parseBpm(TrackMetadata* pTrackMetadata, QString sBpm) { @@ -375,7 +375,7 @@ void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, getID3v2StringType(*pTag, isNumericOrURL); QScopedPointer pTextFrame( new TagLib::ID3v2::TextIdentificationFrame(id, stringType)); - pTextFrame->setText(toTagLibString(text, stringType)); + pTextFrame->setText(toTagLibString(text)); replaceID3v2Frame(pTag, pTextFrame.data()); // Now the plain pointer in pTextFrame is owned and // managed by pTag. We need to release the ownership @@ -389,18 +389,16 @@ void writeID3v2UserTextIdentificationFrame(TagLib::ID3v2::Tag* pTag, findUserTextIdentificationFrame(*pTag, description); if (pTextFrame) { // Modify existing frame - const TagLib::String::Type stringType = - pTextFrame->textEncoding(); - pTextFrame->setDescription(toTagLibString(description, stringType)); - pTextFrame->setText(toTagLibString(text, stringType)); + pTextFrame->setDescription(toTagLibString(description)); + pTextFrame->setText(toTagLibString(text)); } else { // Add a new frame const TagLib::String::Type stringType = getID3v2StringType(*pTag, isNumericOrURL); QScopedPointer pTextFrame( new TagLib::ID3v2::UserTextIdentificationFrame(stringType)); - pTextFrame->setDescription(toTagLibString(description, stringType)); - pTextFrame->setText(toTagLibString(text, stringType)); + pTextFrame->setDescription(toTagLibString(description)); + pTextFrame->setText(toTagLibString(text)); pTag->addFrame(pTextFrame.data()); // Now the plain pointer in pTextFrame is owned and // managed by pTag. We need to release the ownership From 0e1221ddf0a38b5d6057de7a43f53742c51a3e1b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 15 Apr 2015 15:57:45 +0200 Subject: [PATCH 260/481] Improve readability of MP3 debug output --- src/sources/soundsourcemp3.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 149824c7193..ee63b248e40 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -28,6 +28,10 @@ const SINT kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 const SINT kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; +inline QString formatHeaderFlags(int headerFlags) { + return QString("0x%1").arg(headerFlags, 4, 16, QLatin1Char('0')); +} + void logFrameHeader(QDebug logger, const mad_header& madHeader) { logger << "MP3 frame header |" << "layer:" << madHeader.layer @@ -36,7 +40,7 @@ void logFrameHeader(QDebug logger, const mad_header& madHeader) { << "#samples:" << MAD_NSBSAMPLES(&madHeader) << "bitrate:" << madHeader.bitrate << "samplerate:" << madHeader.samplerate - << "flags:" << madHeader.flags; + << "flags:" << formatHeaderFlags(madHeader.flags); } inline bool isRecoverableError(int madError) { From 8aa26a6a83cb39a460ddb2348501ec14e47e7820 Mon Sep 17 00:00:00 2001 From: Tuukka Pasanen Date: Thu, 16 Apr 2015 09:20:23 +0300 Subject: [PATCH 261/481] Get rid of Byte word in variables (use Frame) in SoundSourceFFMPEG and change everything to SINT which seems to be preferred type --- src/sources/soundsourceffmpeg.cpp | 64 +++++++++++++++---------------- src/sources/soundsourceffmpeg.h | 30 +++++++-------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index 51bd0a7ecad..89af2ad0632 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -46,11 +46,11 @@ SoundSourceFFmpeg::SoundSourceFFmpeg(QUrl url) m_pCodecCtx(NULL), m_pCodec(NULL), m_pResample(NULL), - m_iCurrentMixxTs(0), + m_currentMixxxFrameIndex(0), m_bIsSeeked(false), - m_lCacheBytePos(0), - m_lCacheStartByte(0), - m_lCacheEndByte(0), + m_lCacheFramePos(0), + m_lCacheStartFrame(0), + m_lCacheEndFrame(0), m_lCacheLastPos(0), m_lLastStoredPos(0), m_lStoredSeekPoint(-1) { @@ -172,7 +172,7 @@ void SoundSourceFFmpeg::clearCache() { } } -bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { +bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, SINT offset) { unsigned int l_iCount = 0; qint32 l_iRet = 0; AVPacket l_SPacket; @@ -274,9 +274,9 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { // Add to cache and store byte place to memory m_SCache.append(l_SObj); - l_SObj->startByte = m_lCacheBytePos; + l_SObj->startFrame = m_lCacheFramePos; l_SObj->length = l_iRet; - m_lCacheBytePos += l_iRet / (sizeof(CSAMPLE) * 2); + m_lCacheFramePos += l_iRet / (sizeof(CSAMPLE) * 2); // Ogg/Opus have packages pos that have many // audio frames so seek next unique pos.. @@ -286,20 +286,20 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { } // If we are over last storepos and we have read more than jump point need is and pos is unique we store it to memory - if (m_lCacheBytePos > m_lLastStoredPos && - m_lCacheBytePos > (AUDIOSOURCEFFMPEG_POSDISTANCE + m_lLastStoredPos) && + if (m_lCacheFramePos > m_lLastStoredPos && + m_lCacheFramePos > (AUDIOSOURCEFFMPEG_POSDISTANCE + m_lLastStoredPos) && m_bUnique == true) { struct ffmpegLocationObject *l_SJmp = (struct ffmpegLocationObject *)malloc( sizeof(struct ffmpegLocationObject)); - m_lLastStoredPos = m_lCacheBytePos; - l_SJmp->startByte = m_lCacheBytePos; + m_lLastStoredPos = m_lCacheFramePos; + l_SJmp->startFrame = m_lCacheFramePos; l_SJmp->pos = l_SPacket.pos; l_SJmp->pts = l_SPacket.pts; m_SJumpPoints.append(l_SJmp); m_bUnique = false; } - if (offset < 0 || (quint64) offset <= m_lCacheBytePos) { + if (offset < 0 || offset <= m_lCacheFramePos) { l_iCount --; } } else { @@ -353,9 +353,9 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { } l_SObj = m_SCache.first(); - m_lCacheStartByte = l_SObj->startByte; + m_lCacheStartFrame = l_SObj->startFrame; l_SObj = m_SCache.last(); - m_lCacheEndByte = (l_SObj->startByte + l_SObj->length); + m_lCacheEndFrame = (l_SObj->startFrame + l_SObj->length); if (!l_iCount) { return true; @@ -364,8 +364,8 @@ bool SoundSourceFFmpeg::readFramesToCache(unsigned int count, qint64 offset) { } } -bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, - quint64 size) { +bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, SINT offset, + SINT size) { struct ffmpegCacheObject *l_SObj = NULL; quint32 l_lPos = 0; quint32 l_lLeft = 0; @@ -373,7 +373,7 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, quint32 l_lBytesToCopy = 0; // Is offset bigger than start of cache - if (offset >= m_lCacheStartByte) { + if (offset >= m_lCacheStartFrame) { // If last pos is (which is shouldn't) use caches end if (m_lCacheLastPos == 0) { m_lCacheLastPos = m_SCache.size() - 1; @@ -382,7 +382,7 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, // Seek to correct FrameIndex for (l_lPos = m_lCacheLastPos; l_lPos > 0; l_lPos --) { l_SObj = m_SCache[l_lPos]; - if ((l_SObj->startByte + l_SObj->length) < offset) { + if ((l_SObj->startFrame + l_SObj->length) < offset) { break; } } @@ -397,14 +397,14 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, while (l_lLeft > 0) { // If Cache is running low read more if (l_SObj == NULL || (l_lPos + 5) > (unsigned int)m_SCache.size()) { - offset = l_SObj->startByte; + offset = l_SObj->startFrame; if (readFramesToCache(50, -1) == false) { return false; } // Seek back to correct place for (l_lPos = (m_SCache.size() - 50); l_lPos > 0; l_lPos --) { l_SObj = m_SCache[l_lPos]; - if ((l_SObj->startByte + l_SObj->length) < offset) { + if ((l_SObj->startFrame + l_SObj->length) < offset) { break; } } @@ -413,9 +413,9 @@ bool SoundSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, } // If Cache object ain't correct then calculate offset - if (l_SObj->startByte <= offset) { + if (l_SObj->startFrame <= offset) { // We have to convert again it to bytes - l_lOffset = (offset - l_SObj->startByte) * (sizeof(CSAMPLE) * 2); + l_lOffset = (offset - l_SObj->startFrame) * (sizeof(CSAMPLE) * 2); } // Okay somehow offset is bigger than our Cache object have bytes @@ -452,7 +452,7 @@ SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { int ret = 0; qint64 i = 0; - if (frameIndex < 0 || (unsigned long) frameIndex < m_lCacheStartByte) { + if (frameIndex < 0 || frameIndex < m_lCacheStartFrame) { ret = avformat_seek_file(m_pFormatCtx, m_iAudioStream, 0, @@ -467,10 +467,10 @@ SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { } clearCache(); - m_lCacheStartByte = 0; - m_lCacheEndByte = 0; + m_lCacheStartFrame = 0; + m_lCacheEndFrame = 0; m_lCacheLastPos = 0; - m_lCacheBytePos = 0; + m_lCacheFramePos = 0; m_lStoredSeekPoint = -1; @@ -479,8 +479,8 @@ SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { // to try guess it if (frameIndex >= AUDIOSOURCEFFMPEG_POSDISTANCE) { for (i = 0; i < m_SJumpPoints.size(); i ++) { - if (m_SJumpPoints[i]->startByte >= (unsigned long) frameIndex && i > 2) { - m_lCacheBytePos = m_SJumpPoints[i - 2]->startByte * 2; + if (m_SJumpPoints[i]->startFrame >= frameIndex && i > 2) { + m_lCacheFramePos = m_SJumpPoints[i - 2]->startFrame * 2; m_lStoredSeekPoint = m_SJumpPoints[i - 2]->pos; break; } @@ -495,11 +495,11 @@ SINT SoundSourceFFmpeg::seekSampleFrame(SINT frameIndex) { } - if (m_lCacheEndByte <= (unsigned long) frameIndex) { + if (m_lCacheEndFrame <= frameIndex) { readFramesToCache(100, frameIndex); } - m_iCurrentMixxTs = frameIndex; + m_currentMixxxFrameIndex = frameIndex; m_bIsSeeked = TRUE; @@ -516,13 +516,13 @@ SINT SoundSourceFFmpeg::readSampleFrames(SINT numberOfFrames, m_bIsSeeked = FALSE; } - getBytesFromCache((char *)sampleBuffer, m_iCurrentMixxTs, numberOfFrames); + getBytesFromCache((char *)sampleBuffer, m_currentMixxxFrameIndex, numberOfFrames); // As this is also Hack // If we don't seek like we don't on analyzer.. keep // place in mind.. if (m_bIsSeeked == FALSE) { - m_iCurrentMixxTs += numberOfFrames; + m_currentMixxxFrameIndex += numberOfFrames; } m_bIsSeeked = FALSE; diff --git a/src/sources/soundsourceffmpeg.h b/src/sources/soundsourceffmpeg.h index 085130bc9e6..a5008da71d3 100644 --- a/src/sources/soundsourceffmpeg.h +++ b/src/sources/soundsourceffmpeg.h @@ -29,14 +29,14 @@ namespace Mixxx { struct ffmpegLocationObject { - quint64 pos; - qint64 pts; - quint64 startByte; + SINT pos; + SINT pts; + SINT startFrame; }; struct ffmpegCacheObject { - quint64 startByte; - quint32 length; + SINT startFrame; + SINT length; quint8 *bytes; }; @@ -56,9 +56,9 @@ class SoundSourceFFmpeg : public SoundSource { private: Result tryOpen(SINT channelCountHint) /*override*/; - bool readFramesToCache(unsigned int count, qint64 offset); - bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); - quint64 getSizeofCache(); + bool readFramesToCache(unsigned int count, SINT offset); + bool getBytesFromCache(char *buffer, SINT offset, SINT size); + SINT getSizeofCache(); void clearCache(); unsigned int read(unsigned long size, SAMPLE*); @@ -70,18 +70,18 @@ class SoundSourceFFmpeg : public SoundSource { EncoderFfmpegResample *m_pResample; - qint64 m_iCurrentMixxTs; + SINT m_currentMixxxFrameIndex; bool m_bIsSeeked; - quint64 m_lCacheBytePos; - quint64 m_lCacheStartByte; - quint64 m_lCacheEndByte; - quint32 m_lCacheLastPos; + SINT m_lCacheFramePos; + SINT m_lCacheStartFrame; + SINT m_lCacheEndFrame; + SINT m_lCacheLastPos; QVector m_SCache; QVector m_SJumpPoints; - quint64 m_lLastStoredPos; - qint64 m_lStoredSeekPoint; + SINT m_lLastStoredPos; + SINT m_lStoredSeekPoint; }; } // namespace Mixxx From 4b710b69c6c583dcc2bee35bb15de5275c48803b Mon Sep 17 00:00:00 2001 From: Tuukka Pasanen Date: Thu, 16 Apr 2015 09:21:43 +0300 Subject: [PATCH 262/481] Minor code cleaning changed one '==' to ' == ' --- src/sources/soundsourceffmpeg.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index 89af2ad0632..87c365bc238 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -102,7 +102,7 @@ Result SoundSourceFFmpeg::tryOpen(SINT channelCountHint) { m_iAudioStream=i; break; } - if (m_iAudioStream==-1) { + if (m_iAudioStream == -1) { qDebug() << "ffmpeg: cannot find an audio stream: cannot open" << qBAFilename; return ERR; From 0372ee2e60957e91f048a27a4142c0cffb580f0c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 17 Apr 2015 08:12:11 +0200 Subject: [PATCH 263/481] SoundSourceMediaFoundation: Fix compile errors on Windows --- .../soundsourcemediafoundation.cpp | 10 +++++----- .../soundsourcemediafoundation.h | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 6d1de827f1a..753e6eb6bd8 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -574,18 +574,18 @@ bool SoundSourceMediaFoundation::configureAudioStream(SINT channelCountHint) { * is moved to the beginning of m_leftoverBuffer, so empty it first (possibly * with this method). If src and dest overlap, I'll hurt you. */ -void SoundSourceMediaFoundation::copyFrames(CSAMPLE *dest, size_t *destFrames, - const CSAMPLE *src, size_t srcFrames) { +void SoundSourceMediaFoundation::copyFrames(CSAMPLE *dest, SINT *destFrames, + const CSAMPLE *src, SINT srcFrames) { if (srcFrames > *destFrames) { - int samplesToCopy(*destFrames * kNumChannels); + SINT samplesToCopy(frames2samples(*destFrames)); memcpy(dest, src, samplesToCopy * sizeof(*src)); srcFrames -= *destFrames; memmove(m_leftoverBuffer, src + samplesToCopy, - srcFrames * kNumChannels * sizeof(*src)); + frames2samples(srcFrames) * sizeof(*src)); *destFrames = 0; m_leftoverBufferLength = srcFrames; } else { - int samplesToCopy(srcFrames * kNumChannels); + SINT samplesToCopy(frames2samples(srcFrames)); memcpy(dest, src, samplesToCopy * sizeof(*src)); *destFrames -= srcFrames; if (src == m_leftoverBuffer) { diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 023b3f35b5a..7dd1e7015dc 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -52,8 +52,8 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { bool configureAudioStream(SINT channelCountHint); - void copyFrames(CSAMPLE *dest, size_t *destFrames, const CSAMPLE *src, - size_t srcFrames); + void copyFrames(CSAMPLE *dest, SINT *destFrames, const CSAMPLE *src, + SINT srcFrames); HRESULT m_hrCoInitialize; HRESULT m_hrMFStartup; @@ -62,8 +62,8 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { wchar_t *m_wcFilename; int m_nextFrame; CSAMPLE *m_leftoverBuffer; - size_t m_leftoverBufferSize; - size_t m_leftoverBufferLength; + SINT m_leftoverBufferSize; + SINT m_leftoverBufferLength; int m_leftoverBufferPosition; qint64 m_mfDuration; long m_iCurrentPosition; From 3c6f641f6906d8c02f15b3c967577d6da436f303 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 17 Apr 2015 08:22:28 +0200 Subject: [PATCH 264/481] SoundSourceMediaFoundation: Fix #frames to #samples conversion Mathematically equivalent, but now the intention should be clearer. --- .../soundsourcemediafoundation/soundsourcemediafoundation.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 753e6eb6bd8..4d6dc6d704a 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -342,9 +342,9 @@ SINT SoundSourceMediaFoundation::readSampleFrames( qWarning() << __FILE__ << __LINE__ << "Working around inaccurate seeking. Writing silence for" << offshootFrames << "frames"; - // Set offshootFrames * kNumChannels samples to zero. + // Set offshootFrames samples to zero. memset(pBufferCurpos, 0, - frames2samples(sizeof(*pBufferCurpos) * offshootFrames)); + sizeof(*pBufferCurpos) * frames2samples(offshootFrames)); // Now m_nextFrame == bufferPosition m_nextFrame += offshootFrames; framesNeeded -= offshootFrames; From ca1f432c71185f548a745c2aa988da20aa607436 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 17 Apr 2015 08:23:08 +0200 Subject: [PATCH 265/481] SoundSourceMediaFoundation: Improve readability of bitrate calculation --- .../soundsourcemediafoundation/soundsourcemediafoundation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 4d6dc6d704a..a9fe4264efb 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -108,7 +108,7 @@ SoundSourceMediaFoundation::SoundSourceMediaFoundation(QUrl url) // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for // presentation descriptors, one of which MFSourceReader is not. // Therefore, we calculate it ourselves, assuming 16 bits per sample - setBitrate(frames2samples(kBitsPerSampleForBitrate * kSampleRate) / 1000); + setBitrate((frames2samples(getFrameRate()) * kBitsPerSampleForBitrate) / 1000); } SoundSourceMediaFoundation::~SoundSourceMediaFoundation() { From da50c9ec2ab00f5112511ffa8c61b1170fb95a46 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 17 Apr 2015 08:29:12 +0200 Subject: [PATCH 266/481] SoundSourceMediaFoundation: Use SINT instead of int/long --- .../soundsourcemediafoundation.cpp | 10 +++++----- .../soundsourcemediafoundation.h | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index a9fe4264efb..a385ea777f5 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -33,9 +33,9 @@ namespace const bool sDebug = false; -const int kSampleRate = 44100; -const int kLeftoverSize = 4096; // in CSAMPLE's, this seems to be the size MF AAC -const int kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give +const SINT kSampleRate = 44100; +const SINT kLeftoverSize = 4096; // in CSAMPLE's, this seems to be the size MF AAC +const SINT kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give /** * Convert a 100ns Media Foundation value to a number of seconds. @@ -200,7 +200,7 @@ SINT SoundSourceMediaFoundation::seekSampleFrame( // enough for our calculatedFrameFromMF <= nextFrame assertion in ::read). // Has something to do with 100ns MF units being much smaller than most // frame offsets (in seconds) -bkgood - long result = m_iCurrentPosition; + SINT result = m_iCurrentPosition; if (m_dead) { return result; } @@ -375,7 +375,7 @@ SINT SoundSourceMediaFoundation::readSampleFrames( // If the bufferLength is larger than the leftover buffer, re-allocate // it with 2x the space. if (frames2samples(bufferLength) > m_leftoverBufferSize) { - int newSize = m_leftoverBufferSize; + SINT newSize = m_leftoverBufferSize; while (newSize < frames2samples(bufferLength)) { newSize *= 2; diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 7dd1e7015dc..c0857318e42 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -60,13 +60,13 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { IMFSourceReader *m_pReader; IMFMediaType *m_pAudioType; wchar_t *m_wcFilename; - int m_nextFrame; + SINT m_nextFrame; CSAMPLE *m_leftoverBuffer; SINT m_leftoverBufferSize; SINT m_leftoverBufferLength; - int m_leftoverBufferPosition; + SINT m_leftoverBufferPosition; qint64 m_mfDuration; - long m_iCurrentPosition; + SINT m_iCurrentPosition; bool m_dead; bool m_seeking; }; From 1cd3233646fb6b584bcc549f3d1459e4dd1c5e8a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 19 Apr 2015 11:40:52 +0200 Subject: [PATCH 267/481] Add missing files to build script of SoundSourceMediaFoundation plugin --- plugins/soundsourcemediafoundation/SConscript | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/soundsourcemediafoundation/SConscript b/plugins/soundsourcemediafoundation/SConscript index d4d3bc41da4..997f1acdb65 100644 --- a/plugins/soundsourcemediafoundation/SConscript +++ b/plugins/soundsourcemediafoundation/SConscript @@ -18,7 +18,7 @@ if int(build.flags['mediafoundation']): else: env["LINKFLAGS"].remove("/subsystem:windows,5.01") ssmediafoundation_bin = env.SharedLibrary('soundsourcemediafoundation', - ['soundsourcemediafoundation.cpp', 'sources/soundsource.cpp', 'sources/audiosource.cpp', 'metadata/trackmetadata.cpp', 'metadata/trackmetadatataglib.cpp'], + ['soundsourcemediafoundation.cpp', 'sources/soundsourceplugin.cpp', 'sources/soundsource.cpp', 'sources/audiosource.cpp', 'metadata/trackmetadata.cpp', 'metadata/trackmetadatataglib.cpp', 'samplebuffer.cpp', 'sampleutil.cpp'], LINKCOM = [env['LINKCOM'], 'mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;1']) Return("ssmediafoundation_bin") From 99b8ca376ae4045bea784620a48d8b630f729688 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 21 Apr 2015 01:26:26 +0200 Subject: [PATCH 268/481] Fix decoding of some "flaky" FLAC files Thanks to Stephan Balmer for the detailed report and the files! --- src/sources/soundsourceflac.cpp | 41 +++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 6744dda7882..0519b06e9cd 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -129,13 +129,24 @@ SINT SoundSourceFLAC::seekSampleFrame(SINT frameIndex) { // Discard decoded sample data before seeking m_sampleBuffer.reset(); + // Seek to the new position... if (!FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex)) { - qWarning() << "SSFLAC: Seeking error at file" << m_file.fileName(); + qWarning() << "SSFLAC: Seek error at" << frameIndex << "in" << m_file.fileName(); } - if ((FLAC__STREAM_DECODER_SEEK_ERROR == FLAC__stream_decoder_get_state(m_decoder)) && - !FLAC__stream_decoder_flush(m_decoder)) { - qWarning() << "SSFLAC: Failed to flush the decoder's input buffer after seeking" << m_file.fileName(); + // ...and handle seek errors + if (FLAC__STREAM_DECODER_SEEK_ERROR == FLAC__stream_decoder_get_state(m_decoder)) { + // Flush the input stream of the decoder according to the + // documentation of FLAC__stream_decoder_seek_absolute() + if (!FLAC__stream_decoder_flush(m_decoder)) { + qWarning() << "SSFLAC: Failed to flush the decoder's input buffer" << m_file.fileName(); + // Invalidate current position + m_curFrameIndex = getFrameIndexMax(); + return m_curFrameIndex; + } } + + // Pretend to continue decoding at the new position + // (might be adjusted during the next write callback) m_curFrameIndex = frameIndex; DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); @@ -171,6 +182,9 @@ SINT SoundSourceFLAC::readSampleFrames( // If our buffer from libflac is empty (either because we explicitly cleared // it or because we've simply used all the samples), ask for a new buffer if (m_sampleBuffer.isEmpty()) { + // Save the current frame index (might be modified during decoding, + // see below) + const SINT curFrameIndex = m_curFrameIndex; // Documentation of FLAC__stream_decoder_process_single(): // "Depending on what was decoded, the metadata or write callback // will be called with the decoded metadata block or audio frame." @@ -180,6 +194,17 @@ SINT SoundSourceFLAC::readSampleFrames( << m_file.fileName() << ")"; break; // abort } + // After seeking we might need to skip some samples if the decoder + // complained that it has lost sync!?! + if (curFrameIndex != m_curFrameIndex) { + if (curFrameIndex > m_curFrameIndex) { + skipSampleFrames(curFrameIndex - m_curFrameIndex); + } else { + qWarning() << "SSFLAC: Continue decoding at" << m_curFrameIndex << ">" << curFrameIndex; + return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, readStereoSamples); + } + } + DEBUG_ASSERT(curFrameIndex == m_curFrameIndex); } if (m_sampleBuffer.isEmpty()) { break; // EOF @@ -263,7 +288,6 @@ FLAC__bool SoundSourceFLAC::flacEOF() { FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( const FLAC__Frame* frame, const FLAC__int32* const buffer[]) { - // decode buffer must be empty before decoding the next frame if (frame->header.channels != getChannelCount()) { qWarning() << "Corrupt or unsupported FLAC file:" << "Invalid number of channels in FLAC frame header" @@ -283,10 +307,17 @@ FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } + // According to the API docs the decoder will always report + // "FLAC samples" (= "Mixxx frames") for convenience + DEBUG_ASSERT(frame->header.number_type == FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER); + m_curFrameIndex = frame->header.number.sample_number; + + // Decode buffer must be empty before decoding the next frame DEBUG_ASSERT(m_sampleBuffer.isEmpty()); const SampleBuffer::WritableChunk writableChunk( m_sampleBuffer.writeToTail( frames2samples(frame->header.blocksize))); + CSAMPLE* pSampleBuffer = writableChunk.data(); switch (getChannelCount()) { case 1: { From f72f2b25d21b0d623ae69c4333d082919c4f320c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 21 Apr 2015 01:26:59 +0200 Subject: [PATCH 269/481] Avoid unnecessary seeking in FLAC files --- src/sources/soundsourceflac.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 0519b06e9cd..7d076a179aa 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -126,6 +126,13 @@ SINT SoundSourceFLAC::seekSampleFrame(SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + // Avoid unnecessary seeking + // NOTE(uklotzde): Disabling this optimization might reveal rare + // seek errors on certain FLAC files were the decoder loses sync! + if (m_curFrameIndex == frameIndex) { + return m_curFrameIndex; + } + // Discard decoded sample data before seeking m_sampleBuffer.reset(); From 1040e6e76c6a932d8d23101a13dd62ce0c72b096 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 21 Apr 2015 18:14:24 +0200 Subject: [PATCH 270/481] Improve decoding of corrupt/malformed FLAC files --- src/sources/soundsourceflac.cpp | 143 +++++++++++++++++++++----------- src/sources/soundsourceflac.h | 10 +-- 2 files changed, 98 insertions(+), 55 deletions(-) diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 7d076a179aa..85f4018d4b6 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -136,26 +136,46 @@ SINT SoundSourceFLAC::seekSampleFrame(SINT frameIndex) { // Discard decoded sample data before seeking m_sampleBuffer.reset(); - // Seek to the new position... - if (!FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex)) { - qWarning() << "SSFLAC: Seek error at" << frameIndex << "in" << m_file.fileName(); - } - // ...and handle seek errors - if (FLAC__STREAM_DECODER_SEEK_ERROR == FLAC__stream_decoder_get_state(m_decoder)) { - // Flush the input stream of the decoder according to the - // documentation of FLAC__stream_decoder_seek_absolute() - if (!FLAC__stream_decoder_flush(m_decoder)) { - qWarning() << "SSFLAC: Failed to flush the decoder's input buffer" << m_file.fileName(); - // Invalidate current position - m_curFrameIndex = getFrameIndexMax(); - return m_curFrameIndex; + // Seek to the new position + if (FLAC__stream_decoder_seek_absolute(m_decoder, frameIndex)) { + // Set the new position + m_curFrameIndex = frameIndex; + DEBUG_ASSERT(FLAC__STREAM_DECODER_SEEK_ERROR != FLAC__stream_decoder_get_state(m_decoder)); + } else { + qWarning() << "Seek error at" << frameIndex << "in" << m_file.fileName(); + // Invalidate the current position + m_curFrameIndex = getFrameIndexMax(); + if (FLAC__STREAM_DECODER_SEEK_ERROR == FLAC__stream_decoder_get_state(m_decoder)) { + // Flush the input stream of the decoder according to the + // documentation of FLAC__stream_decoder_seek_absolute() + if (!FLAC__stream_decoder_flush(m_decoder)) { + qWarning() << "Failed to flush input buffer of the FLAC decoder after seeking in" + << m_file.fileName(); + // Invalidate the current position... + m_curFrameIndex = getFrameIndexMax(); + // ...and abort + return m_curFrameIndex; + } + // Discard previously decoded sample data before decoding + // the next block of samples + m_sampleBuffer.reset(); + // Trigger decoding of the next block to update the current position + if (!FLAC__stream_decoder_process_single(m_decoder)) { + qWarning() << "Failed to resync FLAC decoder after seeking in" + << m_file.fileName(); + // Invalidate the current position... + m_curFrameIndex = getFrameIndexMax(); + // ...and abort + return m_curFrameIndex; + } + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + if (m_curFrameIndex < frameIndex) { + // Adjust the current position + skipSampleFrames(frameIndex - m_curFrameIndex); + } } } - // Pretend to continue decoding at the new position - // (might be adjusted during the next write callback) - m_curFrameIndex = frameIndex; - DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); return m_curFrameIndex; } @@ -189,29 +209,35 @@ SINT SoundSourceFLAC::readSampleFrames( // If our buffer from libflac is empty (either because we explicitly cleared // it or because we've simply used all the samples), ask for a new buffer if (m_sampleBuffer.isEmpty()) { - // Save the current frame index (might be modified during decoding, - // see below) - const SINT curFrameIndex = m_curFrameIndex; + // Save the current frame index + const SINT curFrameIndexBeforeProcessing = m_curFrameIndex; // Documentation of FLAC__stream_decoder_process_single(): // "Depending on what was decoded, the metadata or write callback // will be called with the decoded metadata block or audio frame." // See also: https://xiph.org/flac/api/group__flac__stream__decoder.html#ga9d6df4a39892c05955122cf7f987f856 if (!FLAC__stream_decoder_process_single(m_decoder)) { - qWarning() << "SSFLAC: decoder_process_single returned false (" - << m_file.fileName() << ")"; + qWarning() << "Failed to decode FLAC file" + << m_file.fileName(); break; // abort } // After seeking we might need to skip some samples if the decoder - // complained that it has lost sync!?! - if (curFrameIndex != m_curFrameIndex) { - if (curFrameIndex > m_curFrameIndex) { - skipSampleFrames(curFrameIndex - m_curFrameIndex); + // complained that it has lost sync for some malformed(?) files + if (curFrameIndexBeforeProcessing != m_curFrameIndex) { + if (curFrameIndexBeforeProcessing > m_curFrameIndex) { + qWarning() << "Trying to adjust frame index" + << m_curFrameIndex << "<>" << curFrameIndexBeforeProcessing + << "while decoding FLAC file" + << m_file.fileName(); + skipSampleFrames(curFrameIndexBeforeProcessing - m_curFrameIndex); } else { - qWarning() << "SSFLAC: Continue decoding at" << m_curFrameIndex << ">" << curFrameIndex; - return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, readStereoSamples); + qWarning() << "Unexpected frame index" + << m_curFrameIndex << "<>" << curFrameIndexBeforeProcessing + << "while decoding FLAC file" + << m_file.fileName(); + break; // abort } } - DEBUG_ASSERT(curFrameIndex == m_curFrameIndex); + DEBUG_ASSERT(curFrameIndexBeforeProcessing == m_curFrameIndex); } if (m_sampleBuffer.isEmpty()) { break; // EOF @@ -249,13 +275,24 @@ SINT SoundSourceFLAC::readSampleFrames( // flac callback methods FLAC__StreamDecoderReadStatus SoundSourceFLAC::flacRead(FLAC__byte buffer[], size_t* bytes) { - *bytes = m_file.read((char*) buffer, *bytes); - if (*bytes > 0) { + const qint64 maxlen = *bytes; + if (0 >= maxlen) { + *bytes = 0; + return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; + } + + const qint64 readlen = m_file.read((char*) buffer, maxlen); + + if (0 < readlen) { + *bytes = readlen; return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; - } else if (*bytes == 0) { - return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; } else { - return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + *bytes = 0; + if (0 == readlen) { + return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; + } else { + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } } } @@ -295,51 +332,57 @@ FLAC__bool SoundSourceFLAC::flacEOF() { FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( const FLAC__Frame* frame, const FLAC__int32* const buffer[]) { - if (frame->header.channels != getChannelCount()) { + const SINT numChannels = frame->header.channels; + if (getChannelCount() > numChannels) { qWarning() << "Corrupt or unsupported FLAC file:" << "Invalid number of channels in FLAC frame header" << frame->header.channels << "<>" << getChannelCount(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - if (frame->header.sample_rate != getFrameRate()) { + if (getFrameRate() != SINT(frame->header.sample_rate)) { qWarning() << "Corrupt or unsupported FLAC file:" << "Invalid sample rate in FLAC frame header" << frame->header.sample_rate << "<>" << getFrameRate(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - if (frame->header.blocksize > m_maxBlocksize) { + const SINT numReadableFrames = frame->header.blocksize; + if (numReadableFrames > m_maxBlocksize) { qWarning() << "Corrupt or unsupported FLAC file:" << "Block size in FLAC frame header exceeds the maximum block size" << frame->header.blocksize << ">" << m_maxBlocksize; return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - // According to the API docs the decoder will always report - // "FLAC samples" (= "Mixxx frames") for convenience + // According to the API docs the decoder will always report the current + // position in "FLAC samples" (= "Mixxx frames") for convenience DEBUG_ASSERT(frame->header.number_type == FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER); m_curFrameIndex = frame->header.number.sample_number; - // Decode buffer must be empty before decoding the next frame + // Decode buffer should be empty before decoding the next frame DEBUG_ASSERT(m_sampleBuffer.isEmpty()); const SampleBuffer::WritableChunk writableChunk( - m_sampleBuffer.writeToTail( - frames2samples(frame->header.blocksize))); + m_sampleBuffer.writeToTail(frames2samples(numReadableFrames))); + + const SINT numWritableFrames = samples2frames(writableChunk.size()); + DEBUG_ASSERT(numWritableFrames <= numReadableFrames); + if (numWritableFrames < numReadableFrames) { + qWarning() << "Sample buffer has not enough free space for all decoded FLAC samples:" + << numWritableFrames << "<" << numReadableFrames; + } CSAMPLE* pSampleBuffer = writableChunk.data(); + DEBUG_ASSERT(getChannelCount() <= numChannels); switch (getChannelCount()) { case 1: { // optimized code for 1 channel (mono) - DEBUG_ASSERT(1 <= frame->header.channels); - for (SINT i = 0; i < writableChunk.size(); ++i) { + for (SINT i = 0; i < numWritableFrames; ++i) { *pSampleBuffer++ = buffer[0][i] * m_sampleScaleFactor; } break; } case 2: { // optimized code for 2 channels (stereo) - DEBUG_ASSERT(2 <= frame->header.channels); - DEBUG_ASSERT(0 == (writableChunk.size() % 2)); - for (SINT i = 0; i < (writableChunk.size() / 2); ++i) { + for (SINT i = 0; i < numWritableFrames; ++i) { *pSampleBuffer++ = buffer[0][i] * m_sampleScaleFactor; *pSampleBuffer++ = buffer[1][i] * m_sampleScaleFactor; } @@ -347,14 +390,14 @@ FLAC__StreamDecoderWriteStatus SoundSourceFLAC::flacWrite( } default: { // generic code for multiple channels - DEBUG_ASSERT(getChannelCount() == frame->header.channels); - for (SINT i = 0; i < samples2frames(writableChunk.size()); ++i) { - for (unsigned j = 0; j < frame->header.channels; ++j) { + for (SINT i = 0; i < numWritableFrames; ++i) { + for (SINT j = 0; j < getChannelCount(); ++j) { *pSampleBuffer++ = buffer[j][i] * m_sampleScaleFactor; } } } } + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; } diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index 6c7a84aa039..ec66b1f6f57 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -53,11 +53,11 @@ class SoundSourceFLAC: public SoundSource { // subblocks (one for each chan) // flac stores in 'frames', each of which has a header and a certain number // of subframes (one for each channel) - unsigned m_minBlocksize; // in time samples (audio samples = time samples * chanCount) - unsigned m_maxBlocksize; - unsigned m_minFramesize; - unsigned m_maxFramesize; - unsigned m_bitsPerSample; + SINT m_minBlocksize; // in time samples (audio samples = time samples * chanCount) + SINT m_maxBlocksize; + SINT m_minFramesize; + SINT m_maxFramesize; + SINT m_bitsPerSample; CSAMPLE m_sampleScaleFactor; From b703f85dec0c474fe6925fa58d954ef652c34fc2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 22 Apr 2015 08:42:50 +0200 Subject: [PATCH 271/481] Change initialization order in SoundSourceMediaFoundation plugin ...and fix the "bits per sample" attribute --- .../soundsourcemediafoundation.cpp | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index a385ea777f5..a85d88e31cc 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -35,7 +35,7 @@ const bool sDebug = false; const SINT kSampleRate = 44100; const SINT kLeftoverSize = 4096; // in CSAMPLE's, this seems to be the size MF AAC -const SINT kBitsPerSampleForBitrate = 16; // for bitrate calculation decoder likes to give +const SINT kBitsPerSample = 16; // for bitrate calculation decoder likes to give /** * Convert a 100ns Media Foundation value to a number of seconds. @@ -108,7 +108,7 @@ SoundSourceMediaFoundation::SoundSourceMediaFoundation(QUrl url) // presentation attribute MF_PD_AUDIO_ENCODING_BITRATE only exists for // presentation descriptors, one of which MFSourceReader is not. // Therefore, we calculate it ourselves, assuming 16 bits per sample - setBitrate((frames2samples(getFrameRate()) * kBitsPerSampleForBitrate) / 1000); + setBitrate((frames2samples(getFrameRate()) * kBitsPerSample) / 1000); } SoundSourceMediaFoundation::~SoundSourceMediaFoundation() { @@ -487,19 +487,16 @@ bool SoundSourceMediaFoundation::configureAudioStream(SINT channelCountHint) { return false; } - // MSDN for this attribute says that if bps is 8, samples are unsigned. - // Otherwise, they're signed (so they're signed for us as 16 bps). Why - // chose to hide this rather useful tidbit here is beyond me -bkgood - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, sizeof(m_leftoverBuffer[0]) * 8); + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, kSampleRate); if (FAILED(hr)) { - qWarning() << "SSMF: failed to set bits per sample"; + qWarning() << "SSMF: failed to set sample rate"; return false; } - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, - frames2samples(sizeof(m_leftoverBuffer[0]))); + // "Number of bits per audio sample in an audio media type." + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, kBitsPerSample); if (FAILED(hr)) { - qWarning() << "SSMF: failed to set block alignment"; + qWarning() << "SSMF: failed to set bits per sample"; return false; } @@ -520,9 +517,12 @@ bool SoundSourceMediaFoundation::configureAudioStream(SINT channelCountHint) { setChannelCount(numChannels); } - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, kSampleRate); + // "...the block alignment is equal to the number of audio channels + // multiplied by the number of bytes per audio sample." + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_BLOCK_ALIGNMENT, + frames2samples(sizeof(m_leftoverBuffer[0]))); if (FAILED(hr)) { - qWarning() << "SSMF: failed to set sample rate"; + qWarning() << "SSMF: failed to set block alignment"; return false; } From df73263c0871308b24bad77c380730a2394440da Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 22 Apr 2015 18:27:22 +0200 Subject: [PATCH 272/481] Provide hints for opening audio sources via AudioSourceConfig --- plugins/soundsourcem4a/soundsourcem4a.cpp | 9 +++++---- plugins/soundsourcem4a/soundsourcem4a.h | 2 +- .../soundsourcemediafoundation.cpp | 10 +++++----- .../soundsourcemediafoundation.h | 4 ++-- plugins/soundsourcewv/soundsourcewv.cpp | 6 +++--- plugins/soundsourcewv/soundsourcewv.h | 2 +- src/analyserqueue.cpp | 4 +++- src/cachingreaderworker.cpp | 8 +++++--- src/musicbrainz/chromaprinter.cpp | 4 +++- src/soundsourceproxy.cpp | 4 ++-- src/soundsourceproxy.h | 2 +- src/sources/audiosource.h | 11 +++++++++++ src/sources/soundsource.cpp | 4 ++-- src/sources/soundsource.h | 4 ++-- src/sources/soundsourcecoreaudio.cpp | 4 ++-- src/sources/soundsourcecoreaudio.h | 2 +- src/sources/soundsourceffmpeg.cpp | 4 ++-- src/sources/soundsourceffmpeg.h | 2 +- src/sources/soundsourceflac.cpp | 2 +- src/sources/soundsourceflac.h | 2 +- src/sources/soundsourcemodplug.cpp | 2 +- src/sources/soundsourcemodplug.h | 2 +- src/sources/soundsourcemp3.cpp | 2 +- src/sources/soundsourcemp3.h | 2 +- src/sources/soundsourceoggvorbis.cpp | 2 +- src/sources/soundsourceoggvorbis.h | 2 +- src/sources/soundsourceopus.cpp | 2 +- src/sources/soundsourceopus.h | 2 +- src/sources/soundsourcesndfile.cpp | 2 +- src/sources/soundsourcesndfile.h | 2 +- 30 files changed, 64 insertions(+), 46 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 3e0bb46a8bf..7871b2934e3 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -114,7 +114,7 @@ SoundSourceM4A::~SoundSourceM4A() { close(); } -Result SoundSourceM4A::tryOpen(SINT channelCountHint) { +Result SoundSourceM4A::tryOpen(const AudioSourceConfig& audioSrcCfg) { DEBUG_ASSERT(MP4_INVALID_FILE_HANDLE == m_hFile); /* open MP4 file, check for >= ver 1.9.1 */ #if MP4V2_PROJECT_version_hex <= 0x00010901 @@ -155,9 +155,10 @@ Result SoundSourceM4A::tryOpen(SINT channelCountHint) { } NeAACDecConfigurationPtr pDecoderConfig = NeAACDecGetCurrentConfiguration( m_hDecoder); - pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ - if ((kChannelCountZero < channelCountHint) && (2 >= channelCountHint)) { - pDecoderConfig->downMatrix = 1; /* multi -> stereo */ + pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; + if ((1 == audioSrcCfg.channelCountHint) || (2 == audioSrcCfg.channelCountHint)) { + // mono or stereo requested + pDecoderConfig->downMatrix = 1; } else { pDecoderConfig->downMatrix = 0; } diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index df614553fc3..31f0a5bdd6a 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -39,7 +39,7 @@ class SoundSourceM4A: public SoundSourcePlugin { CSAMPLE* sampleBuffer) /*override*/; private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; bool isValidSampleBlockId(MP4SampleId sampleBlockId) const; diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index a85d88e31cc..bde34ee56f3 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -115,7 +115,7 @@ SoundSourceMediaFoundation::~SoundSourceMediaFoundation() { close(); } -Result SoundSourceMediaFoundation::tryOpen(SINT channelCountHint) { +Result SoundSourceMediaFoundation::tryOpen(const Mixxx::AudioSourceConfig& audioSrcCfg) { if (SUCCEEDED(m_hrCoInitialize)) { qWarning() << "Cannot reopen MediaFoundation file" << getUrlString(); return ERR; @@ -156,7 +156,7 @@ Result SoundSourceMediaFoundation::tryOpen(SINT channelCountHint) { return ERR; } - if (!configureAudioStream(channelCountHint)) { + if (!configureAudioStream(audioSrcCfg)) { qWarning() << "SSMF: Error configuring audio stream."; return ERR; } @@ -434,7 +434,7 @@ SINT SoundSourceMediaFoundation::readSampleFrames( If anything in here fails, just bail. I'm not going to decode HRESULTS. -- Bill */ -bool SoundSourceMediaFoundation::configureAudioStream(SINT channelCountHint) { +bool SoundSourceMediaFoundation::configureAudioStream(const Mixxx::AudioSourceConfig& audioSrcCfg) { HRESULT hr(S_OK); // deselect all streams, we only want the first @@ -500,8 +500,8 @@ bool SoundSourceMediaFoundation::configureAudioStream(SINT channelCountHint) { return false; } - if (kChannelCountZero < channelCountHint) { - hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, channelCountHint); + if (kChannelCountZero < audioSrcCfg.channelCountHint) { + hr = m_pAudioType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, audioSrcCfg.channelCountHint); if (FAILED(hr)) { qWarning() << "SSMF: failed to set number of channels"; return false; diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index c0857318e42..afb84e9733d 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -48,9 +48,9 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const Mixxx::AudioSourceConfig& audioSrcCfg) /*override*/; - bool configureAudioStream(SINT channelCountHint); + bool configureAudioStream(const Mixxx::AudioSourceConfig& audioSrcCfg); void copyFrames(CSAMPLE *dest, SINT *destFrames, const CSAMPLE *src, SINT srcFrames); diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 18f5f894a41..020e136fab3 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -18,12 +18,12 @@ SoundSourceWV::~SoundSourceWV() { close(); } -Result SoundSourceWV::tryOpen(SINT channelCountHint) { +Result SoundSourceWV::tryOpen(const AudioSourceConfig& audioSrcCfg) { DEBUG_ASSERT(!m_wpc); char msg[80]; // hold possible error message int openFlags = OPEN_WVC | OPEN_NORMALIZE; - if ((kChannelCountZero < channelCountHint) && (2 >= channelCountHint)) { - // mono or stereo + if ((1 == audioSrcCfg.channelCountHint) || (2 == audioSrcCfg.channelCountHint)) { + // mono or stereo requested openFlags |= OPEN_2CH_MAX; } m_wpc = WavpackOpenFileInput( diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index b501d0e4ac0..e3c6bf2c5df 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -28,7 +28,7 @@ class SoundSourceWV: public SoundSourcePlugin { CSAMPLE* sampleBuffer) /*override*/; private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; WavpackContext* m_wpc; diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 35ab4d9e613..c4d32b8e23c 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -309,7 +309,9 @@ void AnalyserQueue::run() { // Get the audio SoundSourceProxy soundSourceProxy(nextTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource(kAnalysisChannels)); + Mixxx::AudioSourceConfig audioSrcCfg; + audioSrcCfg.channelCountHint = kAnalysisChannels; + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource(audioSrcCfg)); if (!pAudioSource) { qWarning() << "Failed to open file for analyzing:" << nextTrack->getLocation(); continue; diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 1e5a9b0c890..9b11396f969 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -134,9 +134,9 @@ void CachingReaderWorker::run() { namespace { - Mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack, SINT channelCountHint) { + Mixxx::AudioSourcePointer openAudioSourceForReading(const TrackPointer& pTrack, const Mixxx::AudioSourceConfig& audioSrcCfg) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource(channelCountHint)); + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource(audioSrcCfg)); if (pAudioSource.isNull()) { qWarning() << "Failed to open file:" << pTrack->getLocation(); return Mixxx::AudioSourcePointer(); @@ -169,7 +169,9 @@ void CachingReaderWorker::loadTrack(const TrackPointer& pTrack) { return; } - m_pAudioSource = openAudioSourceForReading(pTrack, kChunkChannels); + Mixxx::AudioSourceConfig audioSrcCfg; + audioSrcCfg.channelCountHint = kChunkChannels; + m_pAudioSource = openAudioSourceForReading(pTrack, audioSrcCfg); if (m_pAudioSource.isNull()) { // Must unlock before emitting to avoid deadlock qDebug() << m_group << "CachingReaderWorker::loadTrack() load failed for\"" diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index f6b01653ebc..ba37a780bac 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -99,7 +99,9 @@ ChromaPrinter::ChromaPrinter(QObject* parent) QString ChromaPrinter::getFingerprint(TrackPointer pTrack) { SoundSourceProxy soundSourceProxy(pTrack); - Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource(kFingerprintChannels)); + Mixxx::AudioSourceConfig audioSrcCfg; + audioSrcCfg.channelCountHint = kFingerprintChannels; + Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource(audioSrcCfg)); if (pAudioSource.isNull()) { qDebug() << "Skipping invalid file:" << pTrack->getLocation(); return QString(); diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index 33b41d91b22..8ba87fcf32c 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -312,7 +312,7 @@ QLibrary* SoundSourceProxy::getPlugin(QString lib_filename) return pPlugin; } -Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource(SINT channelCountHint) { +Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource(const Mixxx::AudioSourceConfig& audioSrcCfg) { if (m_pAudioSource) { qDebug() << "AudioSource is already open"; return m_pAudioSource; @@ -323,7 +323,7 @@ Mixxx::AudioSourcePointer SoundSourceProxy::openAudioSource(SINT channelCountHin return m_pAudioSource; } - if (OK != m_pSoundSource->open(channelCountHint)) { + if (OK != m_pSoundSource->open(audioSrcCfg)) { qWarning() << "Failed to open SoundSource"; return m_pAudioSource; } diff --git a/src/soundsourceproxy.h b/src/soundsourceproxy.h index f10bb020c63..1c50a48d861 100644 --- a/src/soundsourceproxy.h +++ b/src/soundsourceproxy.h @@ -59,7 +59,7 @@ class SoundSourceProxy: public Mixxx::MetadataSource { // Opening the audio data through the proxy will // update the some metadata of the track object. // Returns a null pointer on failure. - Mixxx::AudioSourcePointer openAudioSource(SINT channelCountHint = Mixxx::AudioSource::kChannelCountDefault); + Mixxx::AudioSourcePointer openAudioSource(const Mixxx::AudioSourceConfig& audioSrcCfg = Mixxx::AudioSourceConfig()); void closeAudioSource(); diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 8b959d9a3a1..641746378f0 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -256,6 +256,17 @@ class AudioSource: public UrlResource { SINT m_bitrate; }; +// Parameters for configuring audio sources +struct AudioSourceConfig { + AudioSourceConfig() + : channelCountHint(AudioSource::kChannelCountDefault), + frameRateHint(AudioSource::kFrameRateDefault){ + } + + SINT channelCountHint; + SINT frameRateHint; +}; + typedef QSharedPointer AudioSourcePointer; } // namespace Mixxx diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index ff0b658d678..9f166868e2f 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -20,11 +20,11 @@ SoundSource::SoundSource(QUrl url, QString type) DEBUG_ASSERT(getUrl().isValid()); } -Result SoundSource::open(SINT channelCountHint) { +Result SoundSource::open(const AudioSourceConfig& audioSrcCfg) { close(); // reopening is not supported Result result; try { - result = tryOpen(channelCountHint); + result = tryOpen(audioSrcCfg); } catch (...) { close(); throw; diff --git a/src/sources/soundsource.h b/src/sources/soundsource.h index 6f94d7cf227..46a3e4bf110 100644 --- a/src/sources/soundsource.h +++ b/src/sources/soundsource.h @@ -34,7 +34,7 @@ class SoundSource: public MetadataSource, public AudioSource { // // Since reopening is not supported close() will be called // implicitly before the AudioSource is actually opened. - Result open(SINT channelCountHint = kChannelCountDefault); + Result open(const AudioSourceConfig& audioSrcCfg = AudioSourceConfig()); // Closes the AudioSource and frees all resources. // @@ -54,7 +54,7 @@ class SoundSource: public MetadataSource, public AudioSource { // need to free resources in tryOpen() themselves, but // should instead be prepared for the following invocation // of close(). - virtual Result tryOpen(SINT channelCountHint) = 0; + virtual Result tryOpen(const AudioSourceConfig& audioSrcCfg) = 0; const QString m_type; }; diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index 19b2dd54dbd..a6a331d0f79 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -25,7 +25,7 @@ SoundSourceCoreAudio::~SoundSourceCoreAudio() { } // soundsource overrides -Result AudioSourceCoreAudio::tryOpen(SINT channelCountHint) { +Result AudioSourceCoreAudio::tryOpen(const AudioSourceConfig& audioSrcCfg) { const QString fileName(getLocalFileName()); //Open the audio file. @@ -66,7 +66,7 @@ Result AudioSourceCoreAudio::tryOpen(SINT channelCountHint) { // create the output format const UInt32 numChannels = - (channelCountZero < channelCountHint) ? channelCountHint : 2; + (channelCountZero < audioSrcCfg.channelCountHint) ? audioSrcCfg.channelCountHint : 2; m_outputFormat = CAStreamBasicDescription(m_inputFormat.mSampleRate, numChannels, CAStreamBasicDescription::kPCMFormatFloat32, true); diff --git a/src/sources/soundsourcecoreaudio.h b/src/sources/soundsourcecoreaudio.h index 7dbb2d4afaa..afe858bcbfd 100644 --- a/src/sources/soundsourcecoreaudio.h +++ b/src/sources/soundsourcecoreaudio.h @@ -33,7 +33,7 @@ class SoundSourceCoreAudio : public Mixxx::SoundSource { CSAMPLE* sampleBuffer) /*override*/; private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; ExtAudioFileRef m_audioFile; CAStreamBasicDescription m_inputFormat; diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index 87c365bc238..dccc6c1d8df 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -60,12 +60,12 @@ SoundSourceFFmpeg::~SoundSourceFFmpeg() { close(); } -Result SoundSourceFFmpeg::tryOpen(SINT channelCountHint) { +Result SoundSourceFFmpeg::tryOpen(const AudioSourceConfig& /*audioSrcCfg*/) { unsigned int i; AVDictionary *l_iFormatOpts = NULL; const QByteArray qBAFilename(getLocalFileNameBytes()); - qDebug() << "New SoundSourceFFmpeg :" << qBAFilename << "(channelCountHint:" << channelCountHint << ")"; + qDebug() << "New SoundSourceFFmpeg :" << qBAFilename; DEBUG_ASSERT(!m_pFormatCtx); m_pFormatCtx = avformat_alloc_context(); diff --git a/src/sources/soundsourceffmpeg.h b/src/sources/soundsourceffmpeg.h index a5008da71d3..3f1866950a7 100644 --- a/src/sources/soundsourceffmpeg.h +++ b/src/sources/soundsourceffmpeg.h @@ -54,7 +54,7 @@ class SoundSourceFFmpeg : public SoundSource { SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer) /*override*/; private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; bool readFramesToCache(unsigned int count, SINT offset); bool getBytesFromCache(char *buffer, SINT offset, SINT size); diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 85f4018d4b6..598b146f4e3 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -80,7 +80,7 @@ SoundSourceFLAC::~SoundSourceFLAC() { close(); } -Result SoundSourceFLAC::tryOpen(SINT /*channelCountHint*/) { +Result SoundSourceFLAC::tryOpen(const AudioSourceConfig& /*audioSrcCfg*/) { DEBUG_ASSERT(!m_file.isOpen()); if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open FLAC file:" << m_file.fileName(); diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index ec66b1f6f57..fb46f387a7e 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -39,7 +39,7 @@ class SoundSourceFLAC: public SoundSource { void flacError(FLAC__StreamDecoderErrorStatus status); private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer, SINT sampleBufferSize, diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index cc77a5af04f..3f5df31d694 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -102,7 +102,7 @@ QImage SoundSourceModPlug::parseCoverArt() const { return QImage(); } -Result SoundSourceModPlug::tryOpen(SINT /*channelCountHint*/) { +Result SoundSourceModPlug::tryOpen(const AudioSourceConfig& /*audioSrcCfg*/) { ScopedTimer t("SoundSourceModPlug::open()"); // read module file to byte array diff --git a/src/sources/soundsourcemodplug.h b/src/sources/soundsourcemodplug.h index 0cb18ec991f..d81970650e6 100644 --- a/src/sources/soundsourcemodplug.h +++ b/src/sources/soundsourcemodplug.h @@ -41,7 +41,7 @@ class SoundSourceModPlug: public Mixxx::SoundSource { CSAMPLE* sampleBuffer) /*override*/; private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index ee63b248e40..2dc11bb1df4 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -120,7 +120,7 @@ SoundSourceMp3::~SoundSourceMp3() { close(); } -Result SoundSourceMp3::tryOpen(SINT /*channelCountHint*/) { +Result SoundSourceMp3::tryOpen(const AudioSourceConfig& /*audioSrcCfg*/) { DEBUG_ASSERT(!isChannelCountValid()); DEBUG_ASSERT(!isFrameRateValid()); diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h index 2bd8ef32316..d29616f8981 100644 --- a/src/sources/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -38,7 +38,7 @@ class SoundSourceMp3: public SoundSource { bool readStereoSamples); private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; QFile m_file; quint64 m_fileSize; diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index 463b45eb55c..78ae0494b2e 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -30,7 +30,7 @@ SoundSourceOggVorbis::~SoundSourceOggVorbis() { close(); } -Result SoundSourceOggVorbis::tryOpen(SINT /*channelCountHint*/) { +Result SoundSourceOggVorbis::tryOpen(const AudioSourceConfig& /*audioSrcCfg*/) { const QByteArray qbaFilename(getLocalFileNameBytes()); if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { qWarning() << "Failed to open OggVorbis file:" << getUrlString(); diff --git a/src/sources/soundsourceoggvorbis.h b/src/sources/soundsourceoggvorbis.h index b8acc2b23da..e79336dd695 100644 --- a/src/sources/soundsourceoggvorbis.h +++ b/src/sources/soundsourceoggvorbis.h @@ -25,7 +25,7 @@ class SoundSourceOggVorbis: public SoundSource { CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; SINT readSampleFrames(SINT numberOfFrames, CSAMPLE* sampleBuffer, SINT sampleBufferSize, diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index dcdb8495fc9..e76c61bc852 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -119,7 +119,7 @@ Result SoundSourceOpus::parseTrackMetadata(Mixxx::TrackMetadata* pMetadata) cons return OK; } -Result SoundSourceOpus::tryOpen(SINT /*channelCountHint*/) { +Result SoundSourceOpus::tryOpen(const AudioSourceConfig& /*audioSrcCfg*/) { const QByteArray qbaFilename(getLocalFileNameBytes()); int errorCode = 0; diff --git a/src/sources/soundsourceopus.h b/src/sources/soundsourceopus.h index a879b44320e..6c0ae382cb3 100644 --- a/src/sources/soundsourceopus.h +++ b/src/sources/soundsourceopus.h @@ -29,7 +29,7 @@ class SoundSourceOpus: public Mixxx::SoundSource { CSAMPLE* sampleBuffer, SINT sampleBufferSize) /*override*/; private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; OggOpusFile *m_pOggOpusFile; diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index fa1ad2ed773..efa04cdfb2e 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -20,7 +20,7 @@ SoundSourceSndFile::~SoundSourceSndFile() { close(); } -Result SoundSourceSndFile::tryOpen(SINT /*channelCountHint*/) { +Result SoundSourceSndFile::tryOpen(const AudioSourceConfig& /*audioSrcCfg*/) { DEBUG_ASSERT(!m_pSndFile); memset(&m_sfInfo, 0, sizeof(m_sfInfo)); #ifdef __WINDOWS__ diff --git a/src/sources/soundsourcesndfile.h b/src/sources/soundsourcesndfile.h index eda5f78377b..81e8824fe78 100644 --- a/src/sources/soundsourcesndfile.h +++ b/src/sources/soundsourcesndfile.h @@ -28,7 +28,7 @@ class SoundSourceSndFile: public Mixxx::SoundSource { CSAMPLE* sampleBuffer) /*override*/; private: - Result tryOpen(SINT channelCountHint) /*override*/; + Result tryOpen(const AudioSourceConfig& audioSrcCfg) /*override*/; SNDFILE* m_pSndFile; SF_INFO m_sfInfo; From 55ff4459d2879816d1e800d5a7c0f9b708172467 Mon Sep 17 00:00:00 2001 From: Alexandru Jercaianu Date: Wed, 22 Apr 2015 22:35:38 +0300 Subject: [PATCH 273/481] [Bug #1299029] phaser effect --- build/depends.py | 1 + src/effects/native/nativebackend.cpp | 2 + src/effects/native/phasereffect.cpp | 140 +++++++++++++++++++++++++++ src/effects/native/phasereffect.h | 60 ++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 src/effects/native/phasereffect.cpp create mode 100644 src/effects/native/phasereffect.h diff --git a/build/depends.py b/build/depends.py index 72f7909ebd3..51d17de5e22 100644 --- a/build/depends.py +++ b/build/depends.py @@ -554,6 +554,7 @@ def sources(self, build): "effects/native/moogladder4filtereffect.cpp", "effects/native/reverbeffect.cpp", "effects/native/echoeffect.cpp", + "effects/native/phasereffect.cpp", "effects/native/reverb/Reverb.cc", "engine/effects/engineeffectsmanager.cpp", diff --git a/src/effects/native/nativebackend.cpp b/src/effects/native/nativebackend.cpp index 2dcc3482f09..0084eca76f5 100644 --- a/src/effects/native/nativebackend.cpp +++ b/src/effects/native/nativebackend.cpp @@ -13,6 +13,7 @@ #include "effects/native/reverbeffect.h" #endif #include "effects/native/echoeffect.h" +#include "effects/native/phasereffect.h" NativeBackend::NativeBackend(QObject* pParent) : EffectsBackend(pParent, tr("Native")) { @@ -33,6 +34,7 @@ NativeBackend::NativeBackend(QObject* pParent) #ifndef __MACAPPSTORE__ registerEffect(); #endif + registerEffect(); } NativeBackend::~NativeBackend() { diff --git a/src/effects/native/phasereffect.cpp b/src/effects/native/phasereffect.cpp new file mode 100644 index 00000000000..781565baeae --- /dev/null +++ b/src/effects/native/phasereffect.cpp @@ -0,0 +1,140 @@ +#include +#include "util/math.h" +#include "effects/native/phasereffect.h" +#include + +// static +QString PhaserEffect::getId() { + return "org.mixxx.effects.phaser"; +} + +// static +EffectManifest PhaserEffect::getManifest() { + EffectManifest manifest; + manifest.setId(getId()); + manifest.setName(QObject::tr("Phaser")); + manifest.setAuthor("The Mixxx Team"); + manifest.setVersion("1.0"); + manifest.setDescription(QObject::tr("Phaser filter")); + + EffectManifestParameter* stages = manifest.addParameter(); + stages->setId("stages"); + stages->setName(QObject::tr("Stages")); + stages->setDescription("Sets number of stages."); + stages->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + stages->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + stages->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + stages->setDefault(2.0); + stages->setMinimum(2.0); + stages->setMaximum(24.0); + + EffectManifestParameter* lfo_frequency = manifest.addParameter(); + lfo_frequency->setId("lfo_frequency"); + lfo_frequency->setName(QObject::tr("LFO Frequency")); + lfo_frequency->setDescription("Controls LFO Frequency."); + lfo_frequency->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + lfo_frequency->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + lfo_frequency->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + lfo_frequency->setDefault(0.4); + lfo_frequency->setMinimum(0.0); + lfo_frequency->setMaximum(4.0); + + EffectManifestParameter* lfo_startphase = manifest.addParameter(); + lfo_startphase->setId("lfo_startphase"); + lfo_startphase->setName(QObject::tr("LFO Start Phase")); + lfo_startphase->setDescription("Sets starting phase."); + lfo_startphase->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + lfo_startphase->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + lfo_startphase->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + lfo_startphase->setDefault(0.0); + lfo_startphase->setMinimum(0.0); + lfo_startphase->setMaximum(5.0); + + EffectManifestParameter* depth = manifest.addParameter(); + depth->setId("depth"); + depth->setName(QObject::tr("Depth")); + depth->setDescription("Controls depth."); + depth->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); + depth->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); + depth->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); + depth->setDefault(100.0); + depth->setMinimum(0.0); + depth->setMaximum(255); + + return manifest; +} + +PhaserEffect::PhaserEffect(EngineEffect* pEffect, + const EffectManifest& manifest) + : m_pStagesParameter(pEffect->getParameterById("stages")), + m_pLFOFrequencyParameter(pEffect->getParameterById("lfo_frequency")), + m_pLFOStartPhaseParameter(pEffect->getParameterById("lfo_startphase")), + m_pDepthParameter(pEffect->getParameterById("depth")) { + Q_UNUSED(manifest); +} + +PhaserEffect::~PhaserEffect() { + //qDebug() << debugString() << "destroyed"; +} + +void PhaserEffect::processChannel(const ChannelHandle& handle, + PhaserGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples, + const unsigned int sampleRate, + const EffectProcessor::EnableState enableState, + const GroupFeatureState& groupFeatures) { + + Q_UNUSED(handle); + Q_UNUSED(enableState); + Q_UNUSED(groupFeatures); + Q_UNUSED(sampleRate); + + CSAMPLE lfoFrequency = m_pLFOFrequencyParameter->value(); + CSAMPLE lfoStartPhase = m_pLFOStartPhaseParameter->value(); + int depth = m_pDepthParameter->value(); + int stages = m_pStagesParameter->value(); + + CSAMPLE* oldLeft = pState->oldLeft; + CSAMPLE* oldRight = pState->oldRight; + + CSAMPLE lfoShape = pState->lfoShape; + int lfoSkipSamples = pState->lfoSkipSamples; + CSAMPLE leftOut = 0, rightOut = 0; + CSAMPLE feedback = 0.5; + CSAMPLE gainLeft = 0, gainRight = 0; + CSAMPLE leftPhase = lfoStartPhase, rightPhase = leftPhase + M_PI; + CSAMPLE lfoSkip = lfoFrequency * 2 * M_PI/sampleRate; + + int skipCount = 0; + const int kChannels = 2; + for (unsigned int i = 0; i < numSamples; i += kChannels) { + leftOut = pInput[i] + leftOut * feedback / 100; + rightOut = pInput[i + 1] + rightOut * feedback / 100; + + if (((skipCount++) % lfoSkipSamples) == 0) { + gainLeft = (1.0 + cos(skipCount * lfoSkip + rightPhase)) / 2; + gainRight = (1.0 + cos(skipCount * lfoSkip + leftPhase)) / 2; + + gainLeft = (exp(gainLeft * lfoShape) - 1) / (exp(lfoShape) - 1); + gainRight = (exp(gainRight * lfoShape) - 1) / (exp(lfoShape) - 1); + + gainLeft = 1.0 - gainLeft / 255 * depth; + gainRight = 1.0 - gainRight / 255 * depth; + } + + for (int j = 0; j < stages; j++) { + CSAMPLE tmpLeft = oldLeft[j]; + CSAMPLE tmpRight = oldRight[j]; + + oldLeft[j] = gainLeft * tmpLeft + leftOut; + oldRight[j] = gainRight * tmpRight + rightOut; + + leftOut = tmpLeft - gainLeft * oldLeft[j]; + rightOut = tmpRight - gainRight * oldRight[j]; + } + + pOutput[i] = leftOut; + pOutput[i + 1] = rightOut; + } +} diff --git a/src/effects/native/phasereffect.h b/src/effects/native/phasereffect.h new file mode 100644 index 00000000000..1a1569982f1 --- /dev/null +++ b/src/effects/native/phasereffect.h @@ -0,0 +1,60 @@ +#ifndef PHASEREFFECT_H +#define PHASEREFFECT_H + +#include "util.h" +#include "util/defs.h" +#include "util/types.h" +#include "engine/effects/engineeffect.h" +#include "engine/effects/engineeffectparameter.h" +#include "effects/effectprocessor.h" +#include "sampleutil.h" + +#define MAXSTAGES 24 + +struct PhaserGroupState { + PhaserGroupState() : + lfoShape(4.0), + lfoSkipSamples(20) { + SampleUtil::applyGain(oldLeft, 0, MAXSTAGES); + SampleUtil::applyGain(oldRight, 0, MAXSTAGES); + } + CSAMPLE oldLeft[MAXSTAGES]; + CSAMPLE oldRight[MAXSTAGES]; + CSAMPLE lfoShape; + int lfoSkipSamples; +}; + +class PhaserEffect : public PerChannelEffectProcessor { + + public: + PhaserEffect(EngineEffect* pEffect, const EffectManifest& manifest); + virtual ~PhaserEffect(); + + static QString getId(); + static EffectManifest getManifest(); + + // See effectprocessor.h + void processChannel(const ChannelHandle& handle, + PhaserGroupState* pState, + const CSAMPLE* pInput, CSAMPLE* pOutput, + const unsigned int numSamples, + const unsigned int sampleRate, + const EffectProcessor::EnableState enableState, + const GroupFeatureState& groupFeatures); + + private: + QString debugString() const { + return getId(); + } + + EngineEffectParameter* m_pStagesParameter; + EngineEffectParameter* m_pLFOFrequencyParameter; + EngineEffectParameter* m_pLFOStartPhaseParameter; + EngineEffectParameter* m_pDepthParameter; + + + + DISALLOW_COPY_AND_ASSIGN(PhaserEffect); +}; + +#endif From 90a5e447289fb60bacb5e7c143152da1190e86a1 Mon Sep 17 00:00:00 2001 From: RJ Ryan Date: Wed, 22 Apr 2015 17:37:21 -0400 Subject: [PATCH 274/481] Fix the build on OS X. --- src/soundsourceproxy.cpp | 6 +++--- src/sources/soundsourcecoreaudio.cpp | 14 +++++++++----- src/sources/soundsourcecoreaudio.h | 4 ++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index 8ba87fcf32c..5f9241937d3 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -193,8 +193,8 @@ Mixxx::SoundSourcePointer SoundSourceProxy::initialize( } else if (Mixxx::SoundSourceFLAC::supportedFileExtensions().contains(type)) { return Mixxx::SoundSourcePointer(new Mixxx::SoundSourceFLAC(url)); #ifdef __COREAUDIO__ - } else if (SoundSourceCoreAudio::supportedFileExtensions().contains(type)) { - return Mixxx::SoundSourcePointer(new SoundSourceCoreAudio(url)); + } else if (Mixxx::SoundSourceCoreAudio::supportedFileExtensions().contains(type)) { + return Mixxx::SoundSourcePointer(new Mixxx::SoundSourceCoreAudio(url)); #endif #ifdef __MODPLUG__ } else if (Mixxx::SoundSourceModPlug::supportedFileExtensions().contains(type)) { @@ -386,7 +386,7 @@ QStringList SoundSourceProxy::supportedFileExtensions() Mixxx::SoundSourceSndFile::supportedFileExtensions()); #endif #ifdef __COREAUDIO__ - supportedFileExtensions.append(SoundSourceCoreAudio::supportedFileExtensions()); + supportedFileExtensions.append(Mixxx::SoundSourceCoreAudio::supportedFileExtensions()); #endif #ifdef __MODPLUG__ supportedFileExtensions.append( diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index a6a331d0f79..07711fcffbc 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -2,6 +2,8 @@ #include "util/math.h" +namespace Mixxx { + QList SoundSourceCoreAudio::supportedFileExtensions() { QList list; list.push_back("m4a"); @@ -25,7 +27,7 @@ SoundSourceCoreAudio::~SoundSourceCoreAudio() { } // soundsource overrides -Result AudioSourceCoreAudio::tryOpen(const AudioSourceConfig& audioSrcCfg) { +Result SoundSourceCoreAudio::tryOpen(const AudioSourceConfig& audioSrcCfg) { const QString fileName(getLocalFileName()); //Open the audio file. @@ -66,7 +68,7 @@ Result AudioSourceCoreAudio::tryOpen(const AudioSourceConfig& audioSrcCfg) { // create the output format const UInt32 numChannels = - (channelCountZero < audioSrcCfg.channelCountHint) ? audioSrcCfg.channelCountHint : 2; + (kChannelCountZero < audioSrcCfg.channelCountHint) ? audioSrcCfg.channelCountHint : 2; m_outputFormat = CAStreamBasicDescription(m_inputFormat.mSampleRate, numChannels, CAStreamBasicDescription::kPCMFormatFloat32, true); @@ -128,11 +130,11 @@ Result AudioSourceCoreAudio::tryOpen(const AudioSourceConfig& audioSrcCfg) { return OK; } -void AudioSourceCoreAudio::close() { +void SoundSourceCoreAudio::close() { ExtAudioFileDispose(m_audioFile); } -SINT AudioSourceCoreAudio::seekSampleFrame( +SINT SoundSourceCoreAudio::seekSampleFrame( SINT frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); @@ -144,7 +146,7 @@ SINT AudioSourceCoreAudio::seekSampleFrame( return frameIndex; } -SINT AudioSourceCoreAudio::readSampleFrames( +SINT SoundSourceCoreAudio::readSampleFrames( SINT numberOfFrames, CSAMPLE* sampleBuffer) { //if (!m_decoder) return 0; SINT numFramesRead = 0; @@ -171,3 +173,5 @@ SINT AudioSourceCoreAudio::readSampleFrames( } return numFramesRead; } + +} // namespace Mixxx diff --git a/src/sources/soundsourcecoreaudio.h b/src/sources/soundsourcecoreaudio.h index afe858bcbfd..73f0c36bdd3 100644 --- a/src/sources/soundsourcecoreaudio.h +++ b/src/sources/soundsourcecoreaudio.h @@ -18,6 +18,8 @@ #include "AudioFormat.h" #endif +namespace Mixxx { + class SoundSourceCoreAudio : public Mixxx::SoundSource { public: static QList supportedFileExtensions(); @@ -41,4 +43,6 @@ class SoundSourceCoreAudio : public Mixxx::SoundSource { SInt64 m_headerFrames; }; +} // namespace Mixxx + #endif // SOUNDSOURCECOREAUDIO_H From a95a67bdf2e485ea4630d19512443a5ef8d0482a Mon Sep 17 00:00:00 2001 From: Owen Williams Date: Wed, 22 Apr 2015 23:06:09 -0400 Subject: [PATCH 275/481] LateNight: Fix effect knobs cut off on Mac --- res/skins/LateNight/effect_parameter_knob.xml | 1 + res/skins/LateNight/effect_unit.xml | 2 +- res/skins/LateNight/style.qss | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/res/skins/LateNight/effect_parameter_knob.xml b/res/skins/LateNight/effect_parameter_knob.xml index 22766285e08..0bf29271428 100644 --- a/res/skins/LateNight/effect_parameter_knob.xml +++ b/res/skins/LateNight/effect_parameter_knob.xml @@ -16,6 +16,7 @@ horizontal min,min EffectKnob + 0,50 \ No newline at end of file + diff --git a/res/skins/LateNight/toolbar_small.xml b/res/skins/LateNight/toolbar_small.xml index 8a06ed7f9f8..f2a09e5b630 100644 --- a/res/skins/LateNight/toolbar_small.xml +++ b/res/skins/LateNight/toolbar_small.xml @@ -28,11 +28,11 @@ LIBRARY - [Library],show_library_tab_small + [Library],show_library LeftButton - [Library],show_library_tab_small + [Library],show_library @@ -156,4 +156,4 @@ - \ No newline at end of file + From 76a66274d7869ad4755e55003a93c3a54fbfbc15 Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Wed, 1 Jul 2015 14:54:35 +0200 Subject: [PATCH 456/481] Added Show Eqs check control to the view Menu. --- src/mixxx.cpp | 33 +++++++++++++++++++++++++++++++++ src/mixxx.h | 4 ++++ 2 files changed, 37 insertions(+) diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 2756e70212c..635b6a70e1a 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -109,6 +109,7 @@ MixxxMainWindow::MixxxMainWindow(QApplication* pApp, const CmdlineArgs& args) m_pShowEffects(NULL), m_pShowCoverArt(NULL), m_pShowLibrary(NULL), + m_pShowEqs(NULL), m_pPrefDlg(NULL), m_runtime_timer("MixxxMainWindow::runtime"), @@ -573,6 +574,7 @@ MixxxMainWindow::~MixxxMainWindow() { delete m_pShowEffects; delete m_pShowCoverArt; delete m_pShowLibrary; + delete m_pShowEqs; delete m_pNumAuxiliaries; delete m_pNumDecks; @@ -860,6 +862,10 @@ void MixxxMainWindow::slotViewShowLibrary(bool enable) { toggleVisibility(ConfigKey("[Library]", "show_library"), enable); } +void MixxxMainWindow::slotViewShowEqs(bool enable) { + toggleVisibility(ConfigKey("[Master]", "show_eqs"), enable); +} + void setVisibilityOptionState(QAction* pAction, ConfigKey key) { ControlObject* pVisibilityControl = ControlObject::getControl(key); pAction->setEnabled(pVisibilityControl != NULL); @@ -907,6 +913,11 @@ void MixxxMainWindow::slotToggleCheckedLibrary() { updateCheckedMenuAction(m_pViewShowLibrary, key); } +void MixxxMainWindow::slotToggleCheckedEqs() { + ConfigKey key("[Master]", "show_eqs"); + updateCheckedMenuAction(m_pViewShowEqs, key); +} + void MixxxMainWindow::linkSkinWidget(ControlObjectSlave** pCOS, ConfigKey key, const char* slot) { if (!*pCOS) { @@ -933,6 +944,8 @@ void MixxxMainWindow::onNewSkinLoaded() { ConfigKey("[Library]", "show_coverart")); setVisibilityOptionState(m_pViewShowLibrary, ConfigKey("[Library]", "show_library")); + setVisibilityOptionState(m_pViewShowEqs, + ConfigKey("[Master]", "show_eqs")); setVisibilityOptionState(m_pViewMaximizeLibrary, ConfigKey("[Master]", "maximize_library")); @@ -959,6 +972,9 @@ void MixxxMainWindow::onNewSkinLoaded() { linkSkinWidget(&m_pShowLibrary, ConfigKey("[Library]", "show_library"), SLOT(slotToggleCheckedLibrary())); + linkSkinWidget(&m_pShowEqs, + ConfigKey("[Master]", "show_eqs"), + SLOT(slotToggleCheckedEqs())); } int MixxxMainWindow::noSoundDlg(void) @@ -1461,6 +1477,20 @@ void MixxxMainWindow::initActions() connect(m_pViewShowLibrary, SIGNAL(toggled(bool)), this, SLOT(slotViewShowLibrary(bool))); + QString showEqsTitle = tr("Show Eqs"); + QString showEqsText = tr("Show the equalizers on the mixer section.") + + " " + mayNotBeSupported; + m_pViewShowEqs = new QAction(showEqsTitle, this); + m_pViewShowEqs->setCheckable(true); + m_pViewShowEqs->setShortcut( + QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", + "ViewMenu_ShowEqs"), + tr("Ctrl+1", "Menubar|View|Show Eqs")))); + m_pViewShowEqs->setStatusTip(showEqsText); + m_pViewShowEqs->setWhatsThis(buildWhatsThis(showEqsTitle, showEqsText)); + connect(m_pViewShowEqs, SIGNAL(toggled(bool)), + this, SLOT(slotViewShowEqs(bool))); + QString recordTitle = tr("&Record Mix"); QString recordText = tr("Record your mix to a file"); m_pOptionsRecord = new QAction(recordTitle, this); @@ -1647,6 +1677,7 @@ void MixxxMainWindow::initMenuBar() { #ifdef __VINYLCONTROL__ m_pViewMenu->addAction(m_pViewVinylControl); #endif + m_pViewMenu->addAction(m_pViewShowEqs); m_pViewMenu->addAction(m_pViewShowPreviewDeck); m_pViewMenu->addAction(m_pViewShowCoverArt); m_pViewMenu->addSeparator(); @@ -2095,12 +2126,14 @@ void MixxxMainWindow::rebootMixxxView() { delete m_pShowEffects; delete m_pShowCoverArt; delete m_pShowLibrary; + delete m_pShowEqs; m_pShowSamplers = NULL; m_pShowMicrophone = NULL; m_pShowPreviewDeck = NULL; m_pShowEffects = NULL; m_pShowCoverArt = NULL; m_pShowLibrary = NULL; + m_pShowEqs = NULL; if (m_pWidgetParent) { m_pWidgetParent->hide(); diff --git a/src/mixxx.h b/src/mixxx.h index d53bfb7405d..4abab2c206f 100644 --- a/src/mixxx.h +++ b/src/mixxx.h @@ -122,6 +122,7 @@ class MixxxMainWindow : public QMainWindow { void slotViewShowEffects(bool); void slotViewShowCoverArt(bool); void slotViewShowLibrary(bool); + void slotViewShowEqs(bool); void slotViewMaximizeLibrary(bool); // toogle full screen mode void slotViewFullScreen(bool toggle); @@ -155,6 +156,7 @@ class MixxxMainWindow : public QMainWindow { void slotToggleCheckedEffects(); void slotToggleCheckedCoverArt(); void slotToggleCheckedLibrary(); + void slotToggleCheckedEqs(); signals: void newSkinLoaded(); @@ -260,6 +262,7 @@ class MixxxMainWindow : public QMainWindow { QAction* m_pViewShowEffects; QAction* m_pViewShowCoverArt; QAction* m_pViewShowLibrary; + QAction* m_pViewShowEqs; QAction* m_pViewMaximizeLibrary; QAction* m_pViewFullScreen; QAction* m_pHelpAboutApp; @@ -283,6 +286,7 @@ class MixxxMainWindow : public QMainWindow { ControlObjectSlave* m_pShowEffects; ControlObjectSlave* m_pShowCoverArt; ControlObjectSlave* m_pShowLibrary; + ControlObjectSlave* m_pShowEqs; ControlObject* m_pNumAuxiliaries; int m_iNoPlaylists; From 48516214f326ee0287a0904947e530e78b6a5c95 Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Wed, 1 Jul 2015 15:08:05 +0200 Subject: [PATCH 457/481] Added Show Mixer check control in view menu. --- src/mixxx.cpp | 33 +++++++++++++++++++++++++++++++++ src/mixxx.h | 4 ++++ 2 files changed, 37 insertions(+) diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 635b6a70e1a..6b7a7145073 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -109,6 +109,7 @@ MixxxMainWindow::MixxxMainWindow(QApplication* pApp, const CmdlineArgs& args) m_pShowEffects(NULL), m_pShowCoverArt(NULL), m_pShowLibrary(NULL), + m_pShowMixer(NULL), m_pShowEqs(NULL), m_pPrefDlg(NULL), @@ -574,6 +575,7 @@ MixxxMainWindow::~MixxxMainWindow() { delete m_pShowEffects; delete m_pShowCoverArt; delete m_pShowLibrary; + delete m_pShowMixer; delete m_pShowEqs; delete m_pNumAuxiliaries; delete m_pNumDecks; @@ -862,6 +864,10 @@ void MixxxMainWindow::slotViewShowLibrary(bool enable) { toggleVisibility(ConfigKey("[Library]", "show_library"), enable); } +void MixxxMainWindow::slotViewShowMixer(bool enable) { + toggleVisibility(ConfigKey("[Master]", "show_mixer"), enable); +} + void MixxxMainWindow::slotViewShowEqs(bool enable) { toggleVisibility(ConfigKey("[Master]", "show_eqs"), enable); } @@ -913,6 +919,11 @@ void MixxxMainWindow::slotToggleCheckedLibrary() { updateCheckedMenuAction(m_pViewShowLibrary, key); } +void MixxxMainWindow::slotToggleCheckedMixer() { + ConfigKey key("[Master]", "show_mixer"); + updateCheckedMenuAction(m_pViewShowMixer, key); +} + void MixxxMainWindow::slotToggleCheckedEqs() { ConfigKey key("[Master]", "show_eqs"); updateCheckedMenuAction(m_pViewShowEqs, key); @@ -944,6 +955,8 @@ void MixxxMainWindow::onNewSkinLoaded() { ConfigKey("[Library]", "show_coverart")); setVisibilityOptionState(m_pViewShowLibrary, ConfigKey("[Library]", "show_library")); + setVisibilityOptionState(m_pViewShowMixer, + ConfigKey("[Master]", "show_mixer")); setVisibilityOptionState(m_pViewShowEqs, ConfigKey("[Master]", "show_eqs")); setVisibilityOptionState(m_pViewMaximizeLibrary, @@ -972,6 +985,9 @@ void MixxxMainWindow::onNewSkinLoaded() { linkSkinWidget(&m_pShowLibrary, ConfigKey("[Library]", "show_library"), SLOT(slotToggleCheckedLibrary())); + linkSkinWidget(&m_pShowMixer, + ConfigKey("[Master]", "show_mixer"), + SLOT(slotToggleCheckedMixer())); linkSkinWidget(&m_pShowEqs, ConfigKey("[Master]", "show_eqs"), SLOT(slotToggleCheckedEqs())); @@ -1477,6 +1493,20 @@ void MixxxMainWindow::initActions() connect(m_pViewShowLibrary, SIGNAL(toggled(bool)), this, SLOT(slotViewShowLibrary(bool))); + QString showMixerTitle = tr("Show Mixer"); + QString showMixerText = tr("Show the mixer section.") + + " " + mayNotBeSupported; + m_pViewShowMixer = new QAction(showMixerTitle, this); + m_pViewShowMixer->setCheckable(true); + m_pViewShowMixer->setShortcut( + QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", + "ViewMenu_ShowMixer"), + tr("Ctrl+1", "Menubar|View|Show Mixer")))); + m_pViewShowMixer->setStatusTip(showMixerText); + m_pViewShowMixer->setWhatsThis(buildWhatsThis(showMixerTitle, showMixerText)); + connect(m_pViewShowMixer, SIGNAL(toggled(bool)), + this, SLOT(slotViewShowMixer(bool))); + QString showEqsTitle = tr("Show Eqs"); QString showEqsText = tr("Show the equalizers on the mixer section.") + " " + mayNotBeSupported; @@ -1677,6 +1707,7 @@ void MixxxMainWindow::initMenuBar() { #ifdef __VINYLCONTROL__ m_pViewMenu->addAction(m_pViewVinylControl); #endif + m_pViewMenu->addAction(m_pViewShowMixer); m_pViewMenu->addAction(m_pViewShowEqs); m_pViewMenu->addAction(m_pViewShowPreviewDeck); m_pViewMenu->addAction(m_pViewShowCoverArt); @@ -2126,6 +2157,7 @@ void MixxxMainWindow::rebootMixxxView() { delete m_pShowEffects; delete m_pShowCoverArt; delete m_pShowLibrary; + delete m_pShowMixer; delete m_pShowEqs; m_pShowSamplers = NULL; m_pShowMicrophone = NULL; @@ -2133,6 +2165,7 @@ void MixxxMainWindow::rebootMixxxView() { m_pShowEffects = NULL; m_pShowCoverArt = NULL; m_pShowLibrary = NULL; + m_pShowMixer = NULL; m_pShowEqs = NULL; if (m_pWidgetParent) { diff --git a/src/mixxx.h b/src/mixxx.h index 4abab2c206f..d003cf51ab2 100644 --- a/src/mixxx.h +++ b/src/mixxx.h @@ -122,6 +122,7 @@ class MixxxMainWindow : public QMainWindow { void slotViewShowEffects(bool); void slotViewShowCoverArt(bool); void slotViewShowLibrary(bool); + void slotViewShowMixer(bool); void slotViewShowEqs(bool); void slotViewMaximizeLibrary(bool); // toogle full screen mode @@ -156,6 +157,7 @@ class MixxxMainWindow : public QMainWindow { void slotToggleCheckedEffects(); void slotToggleCheckedCoverArt(); void slotToggleCheckedLibrary(); + void slotToggleCheckedMixer(); void slotToggleCheckedEqs(); signals: @@ -262,6 +264,7 @@ class MixxxMainWindow : public QMainWindow { QAction* m_pViewShowEffects; QAction* m_pViewShowCoverArt; QAction* m_pViewShowLibrary; + QAction* m_pViewShowMixer; QAction* m_pViewShowEqs; QAction* m_pViewMaximizeLibrary; QAction* m_pViewFullScreen; @@ -286,6 +289,7 @@ class MixxxMainWindow : public QMainWindow { ControlObjectSlave* m_pShowEffects; ControlObjectSlave* m_pShowCoverArt; ControlObjectSlave* m_pShowLibrary; + ControlObjectSlave* m_pShowMixer; ControlObjectSlave* m_pShowEqs; ControlObject* m_pNumAuxiliaries; From 7a69e8aa5c1dc98e32fa1e91df6444b35853153b Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Wed, 1 Jul 2015 22:50:59 +0200 Subject: [PATCH 458/481] Added Show 4 decks check option to view menu. --- src/mixxx.cpp | 33 +++++++++++++++++++++++++++++++++ src/mixxx.h | 4 ++++ 2 files changed, 37 insertions(+) diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 6b7a7145073..6bfbd05f3c6 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -111,6 +111,7 @@ MixxxMainWindow::MixxxMainWindow(QApplication* pApp, const CmdlineArgs& args) m_pShowLibrary(NULL), m_pShowMixer(NULL), m_pShowEqs(NULL), + m_pShow4Decks(NULL), m_pPrefDlg(NULL), m_runtime_timer("MixxxMainWindow::runtime"), @@ -577,6 +578,7 @@ MixxxMainWindow::~MixxxMainWindow() { delete m_pShowLibrary; delete m_pShowMixer; delete m_pShowEqs; + delete m_pShow4Decks; delete m_pNumAuxiliaries; delete m_pNumDecks; @@ -872,6 +874,10 @@ void MixxxMainWindow::slotViewShowEqs(bool enable) { toggleVisibility(ConfigKey("[Master]", "show_eqs"), enable); } +void MixxxMainWindow::slotViewShow4Decks(bool enable) { + toggleVisibility(ConfigKey("[Master]", "show_4decks"), enable); +} + void setVisibilityOptionState(QAction* pAction, ConfigKey key) { ControlObject* pVisibilityControl = ControlObject::getControl(key); pAction->setEnabled(pVisibilityControl != NULL); @@ -929,6 +935,11 @@ void MixxxMainWindow::slotToggleCheckedEqs() { updateCheckedMenuAction(m_pViewShowEqs, key); } +void MixxxMainWindow::slotToggleChecked4Decks() { + ConfigKey key("[Master]", "show_4decks"); + updateCheckedMenuAction(m_pViewShow4Decks, key); +} + void MixxxMainWindow::linkSkinWidget(ControlObjectSlave** pCOS, ConfigKey key, const char* slot) { if (!*pCOS) { @@ -959,6 +970,8 @@ void MixxxMainWindow::onNewSkinLoaded() { ConfigKey("[Master]", "show_mixer")); setVisibilityOptionState(m_pViewShowEqs, ConfigKey("[Master]", "show_eqs")); + setVisibilityOptionState(m_pViewShow4Decks, + ConfigKey("[Master]", "show_4decks")); setVisibilityOptionState(m_pViewMaximizeLibrary, ConfigKey("[Master]", "maximize_library")); @@ -991,6 +1004,9 @@ void MixxxMainWindow::onNewSkinLoaded() { linkSkinWidget(&m_pShowEqs, ConfigKey("[Master]", "show_eqs"), SLOT(slotToggleCheckedEqs())); + linkSkinWidget(&m_pShow4Decks, + ConfigKey("[Master]", "show_4decks"), + SLOT(slotToggleChecked4Decks())); } int MixxxMainWindow::noSoundDlg(void) @@ -1521,6 +1537,20 @@ void MixxxMainWindow::initActions() connect(m_pViewShowEqs, SIGNAL(toggled(bool)), this, SLOT(slotViewShowEqs(bool))); + QString show4DecksTitle = tr("Show 4 Decks"); + QString show4DecksText = tr("Show 4 Decks on the Mixxx interface.") + + " " + mayNotBeSupported; + m_pViewShow4Decks = new QAction(show4DecksTitle, this); + m_pViewShow4Decks->setCheckable(true); + m_pViewShow4Decks->setShortcut( + QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", + "ViewMenu_Show4Decks"), + tr("Ctrl+1", "Menubar|View|Show 4 Decks")))); + m_pViewShow4Decks->setStatusTip(show4DecksText); + m_pViewShow4Decks->setWhatsThis(buildWhatsThis(show4DecksTitle, show4DecksText)); + connect(m_pViewShow4Decks, SIGNAL(toggled(bool)), + this, SLOT(slotViewShow4Decks(bool))); + QString recordTitle = tr("&Record Mix"); QString recordText = tr("Record your mix to a file"); m_pOptionsRecord = new QAction(recordTitle, this); @@ -1704,6 +1734,7 @@ void MixxxMainWindow::initMenuBar() { m_pViewMenu->addAction(m_pViewShowEffects); m_pViewMenu->addAction(m_pViewShowSamplers); m_pViewMenu->addSeparator(); + m_pViewMenu->addAction(m_pViewShow4Decks); #ifdef __VINYLCONTROL__ m_pViewMenu->addAction(m_pViewVinylControl); #endif @@ -2159,6 +2190,7 @@ void MixxxMainWindow::rebootMixxxView() { delete m_pShowLibrary; delete m_pShowMixer; delete m_pShowEqs; + delete m_pShow4Decks; m_pShowSamplers = NULL; m_pShowMicrophone = NULL; m_pShowPreviewDeck = NULL; @@ -2167,6 +2199,7 @@ void MixxxMainWindow::rebootMixxxView() { m_pShowLibrary = NULL; m_pShowMixer = NULL; m_pShowEqs = NULL; + m_pShow4Decks = NULL; if (m_pWidgetParent) { m_pWidgetParent->hide(); diff --git a/src/mixxx.h b/src/mixxx.h index d003cf51ab2..428dc738635 100644 --- a/src/mixxx.h +++ b/src/mixxx.h @@ -124,6 +124,7 @@ class MixxxMainWindow : public QMainWindow { void slotViewShowLibrary(bool); void slotViewShowMixer(bool); void slotViewShowEqs(bool); + void slotViewShow4Decks(bool); void slotViewMaximizeLibrary(bool); // toogle full screen mode void slotViewFullScreen(bool toggle); @@ -159,6 +160,7 @@ class MixxxMainWindow : public QMainWindow { void slotToggleCheckedLibrary(); void slotToggleCheckedMixer(); void slotToggleCheckedEqs(); + void slotToggleChecked4Decks(); signals: void newSkinLoaded(); @@ -266,6 +268,7 @@ class MixxxMainWindow : public QMainWindow { QAction* m_pViewShowLibrary; QAction* m_pViewShowMixer; QAction* m_pViewShowEqs; + QAction* m_pViewShow4Decks; QAction* m_pViewMaximizeLibrary; QAction* m_pViewFullScreen; QAction* m_pHelpAboutApp; @@ -291,6 +294,7 @@ class MixxxMainWindow : public QMainWindow { ControlObjectSlave* m_pShowLibrary; ControlObjectSlave* m_pShowMixer; ControlObjectSlave* m_pShowEqs; + ControlObjectSlave* m_pShow4Decks; ControlObject* m_pNumAuxiliaries; int m_iNoPlaylists; From e7a25852b2edf9541fa506ceb7f9fec6b93be609 Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Wed, 1 Jul 2015 23:10:05 +0200 Subject: [PATCH 459/481] Fixed Maximize Library skin button not updating the view menu check option. --- src/mixxx.cpp | 13 +++++++++++++ src/mixxx.h | 2 ++ 2 files changed, 15 insertions(+) diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 6bfbd05f3c6..1657ab5db79 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -112,6 +112,7 @@ MixxxMainWindow::MixxxMainWindow(QApplication* pApp, const CmdlineArgs& args) m_pShowMixer(NULL), m_pShowEqs(NULL), m_pShow4Decks(NULL), + m_pMaximizeLibrary(NULL), m_pPrefDlg(NULL), m_runtime_timer("MixxxMainWindow::runtime"), @@ -579,6 +580,7 @@ MixxxMainWindow::~MixxxMainWindow() { delete m_pShowMixer; delete m_pShowEqs; delete m_pShow4Decks; + delete m_pMaximizeLibrary; delete m_pNumAuxiliaries; delete m_pNumDecks; @@ -940,6 +942,11 @@ void MixxxMainWindow::slotToggleChecked4Decks() { updateCheckedMenuAction(m_pViewShow4Decks, key); } +void MixxxMainWindow::slotToggleCheckedMaximizeLibrary() { + ConfigKey key("[Master]", "maximize_library"); + updateCheckedMenuAction(m_pViewMaximizeLibrary, key); +} + void MixxxMainWindow::linkSkinWidget(ControlObjectSlave** pCOS, ConfigKey key, const char* slot) { if (!*pCOS) { @@ -1007,6 +1014,9 @@ void MixxxMainWindow::onNewSkinLoaded() { linkSkinWidget(&m_pShow4Decks, ConfigKey("[Master]", "show_4decks"), SLOT(slotToggleChecked4Decks())); + linkSkinWidget(&m_pMaximizeLibrary, + ConfigKey("[Master]", "maximize_library"), + SLOT(slotToggleCheckedMaximizeLibrary())); } int MixxxMainWindow::noSoundDlg(void) @@ -2191,6 +2201,8 @@ void MixxxMainWindow::rebootMixxxView() { delete m_pShowMixer; delete m_pShowEqs; delete m_pShow4Decks; + delete m_pMaximizeLibrary; + m_pShowSamplers = NULL; m_pShowMicrophone = NULL; m_pShowPreviewDeck = NULL; @@ -2200,6 +2212,7 @@ void MixxxMainWindow::rebootMixxxView() { m_pShowMixer = NULL; m_pShowEqs = NULL; m_pShow4Decks = NULL; + m_pMaximizeLibrary = NULL; if (m_pWidgetParent) { m_pWidgetParent->hide(); diff --git a/src/mixxx.h b/src/mixxx.h index 428dc738635..3022cb8d5e0 100644 --- a/src/mixxx.h +++ b/src/mixxx.h @@ -161,6 +161,7 @@ class MixxxMainWindow : public QMainWindow { void slotToggleCheckedMixer(); void slotToggleCheckedEqs(); void slotToggleChecked4Decks(); + void slotToggleCheckedMaximizeLibrary(); signals: void newSkinLoaded(); @@ -295,6 +296,7 @@ class MixxxMainWindow : public QMainWindow { ControlObjectSlave* m_pShowMixer; ControlObjectSlave* m_pShowEqs; ControlObjectSlave* m_pShow4Decks; + ControlObjectSlave* m_pMaximizeLibrary; ControlObject* m_pNumAuxiliaries; int m_iNoPlaylists; From 5985074b0d22599c0896590d5746fb3a887202fd Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Wed, 24 Jun 2015 23:22:57 +0200 Subject: [PATCH 460/481] LateNight: crossfader can now be hidden. Added "Show crossfader" check option to view menu. --- res/skins/LateNight/mixer.xml | 4 ++++ res/skins/LateNight/skin.xml | 3 ++- src/controllers/controlpickermenu.cpp | 3 +++ src/mixxx.cpp | 33 +++++++++++++++++++++++++++ src/mixxx.h | 4 ++++ src/skin/tooltips.cpp | 4 ++++ 6 files changed, 50 insertions(+), 1 deletion(-) diff --git a/res/skins/LateNight/mixer.xml b/res/skins/LateNight/mixer.xml index 3e0dbf7d671..287c4e5a419 100644 --- a/res/skins/LateNight/mixer.xml +++ b/res/skins/LateNight/mixer.xml @@ -147,6 +147,10 @@ + + [Master],show_xfader + visible + diff --git a/res/skins/LateNight/skin.xml b/res/skins/LateNight/skin.xml index 3472d31f7ca..cefa0185d3d 100644 --- a/res/skins/LateNight/skin.xml +++ b/res/skins/LateNight/skin.xml @@ -57,7 +57,8 @@ 1 0 1 - 1 + 1 + 1 0 diff --git a/src/controllers/controlpickermenu.cpp b/src/controllers/controlpickermenu.cpp index 25bd1faad50..9755d789980 100644 --- a/src/controllers/controlpickermenu.cpp +++ b/src/controllers/controlpickermenu.cpp @@ -708,6 +708,9 @@ ControlPickerMenu::ControlPickerMenu(QWidget* pParent) addControl("[Library]", "show_coverart", tr("Cover Art Show/Hide"), tr("Show/hide cover art"), guiMenu); + addControl("[Master]", "show_xfader", + tr("Crossfader Show/Hide"), + tr("Show/hide the crossfader"), guiMenu); QString spinnyTitle = tr("Vinyl Spinner Show/Hide"); QString spinnyDescription = tr("Show/hide spinning vinyl widget"); diff --git a/src/mixxx.cpp b/src/mixxx.cpp index 1657ab5db79..dd0c831d949 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -111,6 +111,7 @@ MixxxMainWindow::MixxxMainWindow(QApplication* pApp, const CmdlineArgs& args) m_pShowLibrary(NULL), m_pShowMixer(NULL), m_pShowEqs(NULL), + m_pShowXFader(NULL), m_pShow4Decks(NULL), m_pMaximizeLibrary(NULL), @@ -579,6 +580,7 @@ MixxxMainWindow::~MixxxMainWindow() { delete m_pShowLibrary; delete m_pShowMixer; delete m_pShowEqs; + delete m_pShowXFader; delete m_pShow4Decks; delete m_pMaximizeLibrary; delete m_pNumAuxiliaries; @@ -876,6 +878,10 @@ void MixxxMainWindow::slotViewShowEqs(bool enable) { toggleVisibility(ConfigKey("[Master]", "show_eqs"), enable); } +void MixxxMainWindow::slotViewShowXFader(bool enable) { + toggleVisibility(ConfigKey("[Master]", "show_xfader"), enable); +} + void MixxxMainWindow::slotViewShow4Decks(bool enable) { toggleVisibility(ConfigKey("[Master]", "show_4decks"), enable); } @@ -937,6 +943,11 @@ void MixxxMainWindow::slotToggleCheckedEqs() { updateCheckedMenuAction(m_pViewShowEqs, key); } +void MixxxMainWindow::slotToggleCheckedXFader() { + ConfigKey key("[Master]", "show_xfader"); + updateCheckedMenuAction(m_pViewShowXFader, key); +} + void MixxxMainWindow::slotToggleChecked4Decks() { ConfigKey key("[Master]", "show_4decks"); updateCheckedMenuAction(m_pViewShow4Decks, key); @@ -977,6 +988,8 @@ void MixxxMainWindow::onNewSkinLoaded() { ConfigKey("[Master]", "show_mixer")); setVisibilityOptionState(m_pViewShowEqs, ConfigKey("[Master]", "show_eqs")); + setVisibilityOptionState(m_pViewShowXFader, + ConfigKey("[Master]", "show_xfader")); setVisibilityOptionState(m_pViewShow4Decks, ConfigKey("[Master]", "show_4decks")); setVisibilityOptionState(m_pViewMaximizeLibrary, @@ -1011,6 +1024,9 @@ void MixxxMainWindow::onNewSkinLoaded() { linkSkinWidget(&m_pShowEqs, ConfigKey("[Master]", "show_eqs"), SLOT(slotToggleCheckedEqs())); + linkSkinWidget(&m_pShowXFader, + ConfigKey("[Master]", "show_xfader"), + SLOT(slotToggleCheckedXFader())); linkSkinWidget(&m_pShow4Decks, ConfigKey("[Master]", "show_4decks"), SLOT(slotToggleChecked4Decks())); @@ -1547,6 +1563,20 @@ void MixxxMainWindow::initActions() connect(m_pViewShowEqs, SIGNAL(toggled(bool)), this, SLOT(slotViewShowEqs(bool))); + QString showXFaderTitle = tr("Show Crossfader"); + QString showXFaderText = tr("Show the crossfader on the mixer section.") + + " " + mayNotBeSupported; + m_pViewShowXFader = new QAction(showXFaderTitle, this); + m_pViewShowXFader->setCheckable(true); + m_pViewShowXFader->setShortcut( + QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", + "ViewMenu_ShowXFader"), + tr("Ctrl+1", "Menubar|View|Show Crossfader")))); + m_pViewShowXFader->setStatusTip(showXFaderText); + m_pViewShowXFader->setWhatsThis(buildWhatsThis(showXFaderTitle, showXFaderText)); + connect(m_pViewShowXFader, SIGNAL(toggled(bool)), + this, SLOT(slotViewShowXFader(bool))); + QString show4DecksTitle = tr("Show 4 Decks"); QString show4DecksText = tr("Show 4 Decks on the Mixxx interface.") + " " + mayNotBeSupported; @@ -1750,6 +1780,7 @@ void MixxxMainWindow::initMenuBar() { #endif m_pViewMenu->addAction(m_pViewShowMixer); m_pViewMenu->addAction(m_pViewShowEqs); + m_pViewMenu->addAction(m_pViewShowXFader); m_pViewMenu->addAction(m_pViewShowPreviewDeck); m_pViewMenu->addAction(m_pViewShowCoverArt); m_pViewMenu->addSeparator(); @@ -2200,6 +2231,7 @@ void MixxxMainWindow::rebootMixxxView() { delete m_pShowLibrary; delete m_pShowMixer; delete m_pShowEqs; + delete m_pShowXFader; delete m_pShow4Decks; delete m_pMaximizeLibrary; @@ -2211,6 +2243,7 @@ void MixxxMainWindow::rebootMixxxView() { m_pShowLibrary = NULL; m_pShowMixer = NULL; m_pShowEqs = NULL; + m_pShowXFader = NULL; m_pShow4Decks = NULL; m_pMaximizeLibrary = NULL; diff --git a/src/mixxx.h b/src/mixxx.h index 3022cb8d5e0..320a5c79ef9 100644 --- a/src/mixxx.h +++ b/src/mixxx.h @@ -124,6 +124,7 @@ class MixxxMainWindow : public QMainWindow { void slotViewShowLibrary(bool); void slotViewShowMixer(bool); void slotViewShowEqs(bool); + void slotViewShowXFader(bool); void slotViewShow4Decks(bool); void slotViewMaximizeLibrary(bool); // toogle full screen mode @@ -160,6 +161,7 @@ class MixxxMainWindow : public QMainWindow { void slotToggleCheckedLibrary(); void slotToggleCheckedMixer(); void slotToggleCheckedEqs(); + void slotToggleCheckedXFader(); void slotToggleChecked4Decks(); void slotToggleCheckedMaximizeLibrary(); @@ -269,6 +271,7 @@ class MixxxMainWindow : public QMainWindow { QAction* m_pViewShowLibrary; QAction* m_pViewShowMixer; QAction* m_pViewShowEqs; + QAction* m_pViewShowXFader; QAction* m_pViewShow4Decks; QAction* m_pViewMaximizeLibrary; QAction* m_pViewFullScreen; @@ -295,6 +298,7 @@ class MixxxMainWindow : public QMainWindow { ControlObjectSlave* m_pShowLibrary; ControlObjectSlave* m_pShowMixer; ControlObjectSlave* m_pShowEqs; + ControlObjectSlave* m_pShowXFader; ControlObjectSlave* m_pShow4Decks; ControlObjectSlave* m_pMaximizeLibrary; ControlObject* m_pNumAuxiliaries; diff --git a/src/skin/tooltips.cpp b/src/skin/tooltips.cpp index b478d450823..4d9296e3306 100644 --- a/src/skin/tooltips.cpp +++ b/src/skin/tooltips.cpp @@ -176,6 +176,10 @@ void Tooltips::addStandardTooltips() { << tr("Toggle 4 Decks") << tr("Switches between showing 2 decks and 4 decks."); + add("show_xfader") + << tr("Crossfader") + << tr("Show/hide the crossfader"); + add("show_library") << tr("Show Library") << tr("Show or hide the track library."); From 0dca0a67eeae84aae4d151f59b40e6e9ccd0fe72 Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Fri, 26 Jun 2015 17:10:16 +0200 Subject: [PATCH 461/481] Fixed double border when crossfader was hidden. --- res/skins/LateNight/style.qss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/skins/LateNight/style.qss b/res/skins/LateNight/style.qss index 496fdfa990b..8c19be85b2c 100644 --- a/res/skins/LateNight/style.qss +++ b/res/skins/LateNight/style.qss @@ -164,10 +164,6 @@ } #MixerChannelControls { - border: 1px solid #585858; - border-left: 0px; - border-right: 0px; - border-top: 0px; padding-bottom: 2px; } @@ -640,7 +636,6 @@ #CrossFadeContainer { qproperty-layoutAlignment: 'AlignCenter'; background-color: #1e1e1e; - padding-top: 5px; } #GuiToggleContainer { @@ -666,6 +661,11 @@ #Crossfader { qproperty-layoutAlignment: 'AlignCenter'; + border: 1px solid #585858; + border-left: 0px; + border-right: 0px; + border-bottom: 0px; + padding-top: 5px; /*background-color:#1e1e1e;*/ } From 4e24639676e51a21e6b2052a764a35a73d145f2d Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Thu, 2 Jul 2015 18:36:43 +0200 Subject: [PATCH 462/481] Updated keyboard shortcuts for layout options. --- res/keyboard/cs_CZ.kbd.cfg | 15 ++++++++++----- res/keyboard/da_DK.kbd.cfg | 15 ++++++++++----- res/keyboard/de_CH.kbd.cfg | 15 ++++++++++----- res/keyboard/de_DE.kbd.cfg | 15 ++++++++++----- res/keyboard/el_GR.kbd.cfg | 15 ++++++++++----- res/keyboard/en_US.kbd.cfg | 15 ++++++++++----- res/keyboard/es_ES.kbd.cfg | 15 ++++++++++----- res/keyboard/fi_FI.kbd.cfg | 15 ++++++++++----- res/keyboard/fr_CH.kbd.cfg | 15 ++++++++++----- res/keyboard/fr_FR.kbd.cfg | 15 ++++++++++----- res/keyboard/it_IT.kbd.cfg | 15 ++++++++++----- res/keyboard/ru_RU.kbd.cfg | 15 ++++++++++----- src/mixxx.cpp | 18 +++++++++--------- 13 files changed, 129 insertions(+), 69 deletions(-) diff --git a/res/keyboard/cs_CZ.kbd.cfg b/res/keyboard/cs_CZ.kbd.cfg index d4e2e274bbf..dd9512cdc35 100644 --- a/res/keyboard/cs_CZ.kbd.cfg +++ b/res/keyboard/cs_CZ.kbd.cfg @@ -128,12 +128,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl+' ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+y diff --git a/res/keyboard/da_DK.kbd.cfg b/res/keyboard/da_DK.kbd.cfg index 664690fc2f5..59f8a0e4567 100644 --- a/res/keyboard/da_DK.kbd.cfg +++ b/res/keyboard/da_DK.kbd.cfg @@ -128,12 +128,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl+' ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+y diff --git a/res/keyboard/de_CH.kbd.cfg b/res/keyboard/de_CH.kbd.cfg index 681ac2aa23c..6dc7528e1f8 100644 --- a/res/keyboard/de_CH.kbd.cfg +++ b/res/keyboard/de_CH.kbd.cfg @@ -125,12 +125,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl+' ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+t diff --git a/res/keyboard/de_DE.kbd.cfg b/res/keyboard/de_DE.kbd.cfg index d7d08ffd3de..b93faa77ed7 100644 --- a/res/keyboard/de_DE.kbd.cfg +++ b/res/keyboard/de_DE.kbd.cfg @@ -128,12 +128,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl++ ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+z diff --git a/res/keyboard/el_GR.kbd.cfg b/res/keyboard/el_GR.kbd.cfg index 71bd7a69aa8..c54b43926cd 100644 --- a/res/keyboard/el_GR.kbd.cfg +++ b/res/keyboard/el_GR.kbd.cfg @@ -128,12 +128,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl++ ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+y diff --git a/res/keyboard/en_US.kbd.cfg b/res/keyboard/en_US.kbd.cfg index 89e9acc27a8..a9323e3ada9 100644 --- a/res/keyboard/en_US.kbd.cfg +++ b/res/keyboard/en_US.kbd.cfg @@ -125,12 +125,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl+' ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+t diff --git a/res/keyboard/es_ES.kbd.cfg b/res/keyboard/es_ES.kbd.cfg index 356aff120bb..606b763addd 100644 --- a/res/keyboard/es_ES.kbd.cfg +++ b/res/keyboard/es_ES.kbd.cfg @@ -128,12 +128,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl+' ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+y diff --git a/res/keyboard/fi_FI.kbd.cfg b/res/keyboard/fi_FI.kbd.cfg index 92d4382c101..91c3492c97e 100644 --- a/res/keyboard/fi_FI.kbd.cfg +++ b/res/keyboard/fi_FI.kbd.cfg @@ -128,12 +128,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl++ ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+y diff --git a/res/keyboard/fr_CH.kbd.cfg b/res/keyboard/fr_CH.kbd.cfg index 9e82c10ab31..7ecc46c565c 100644 --- a/res/keyboard/fr_CH.kbd.cfg +++ b/res/keyboard/fr_CH.kbd.cfg @@ -125,12 +125,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl+' ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+t diff --git a/res/keyboard/fr_FR.kbd.cfg b/res/keyboard/fr_FR.kbd.cfg index 056c5334dd6..58387337b85 100644 --- a/res/keyboard/fr_FR.kbd.cfg +++ b/res/keyboard/fr_FR.kbd.cfg @@ -128,12 +128,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl+' ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+y diff --git a/res/keyboard/it_IT.kbd.cfg b/res/keyboard/it_IT.kbd.cfg index e15c28509a2..185f5c4705f 100644 --- a/res/keyboard/it_IT.kbd.cfg +++ b/res/keyboard/it_IT.kbd.cfg @@ -128,12 +128,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl+' ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+y diff --git a/res/keyboard/ru_RU.kbd.cfg b/res/keyboard/ru_RU.kbd.cfg index 9c266e73bab..bee11484579 100644 --- a/res/keyboard/ru_RU.kbd.cfg +++ b/res/keyboard/ru_RU.kbd.cfg @@ -128,12 +128,17 @@ FileMenu_LoadDeck2 Ctrl+Shift+O FileMenu_Quit Ctrl+q LibraryMenu_NewPlaylist Ctrl+n LibraryMenu_NewCrate Ctrl+Shift+N -ViewMenu_ShowSamplers Ctrl+1 +ViewMenu_ShowLibrary Ctrl+1 ViewMenu_ShowMicrophone Ctrl+2 -ViewMenu_ShowVinylControl Ctrl+3 -ViewMenu_ShowPreviewDeck Ctrl+4 -ViewMenu_ShowEffects Ctrl+5 -ViewMenu_ShowCoverArt Ctrl+6 +ViewMenu_ShowEffects Ctrl+3 +ViewMenu_ShowSamplers Ctrl+4 +ViewMenu_Show4Decks Ctrl+5 +ViewMenu_ShowVinylControl Ctrl+6 +ViewMenu_ShowMixer Ctrl+7 +ViewMenu_ShowEqs Ctrl+8 +ViewMenu_ShowXFader Ctrl+9 +ViewMenu_ShowPreviewDeck Ctrl+0 +ViewMenu_ShowCoverArt Ctrl+- ViewMenu_MaximizeLibrary Space OptionsMenu_EnableLiveBroadcasting Ctrl+l OptionsMenu_EnableVinyl1 Ctrl+y diff --git a/src/mixxx.cpp b/src/mixxx.cpp index dd0c831d949..ef660d93df9 100644 --- a/src/mixxx.cpp +++ b/src/mixxx.cpp @@ -1429,7 +1429,7 @@ void MixxxMainWindow::initActions() m_pViewShowSamplers->setShortcut( QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowSamplers"), - tr("Ctrl+1", "Menubar|View|Show Samplers")))); + tr("Ctrl+4", "Menubar|View|Show Samplers")))); m_pViewShowSamplers->setStatusTip(showSamplersText); m_pViewShowSamplers->setWhatsThis(buildWhatsThis(showSamplersTitle, showSamplersText)); connect(m_pViewShowSamplers, SIGNAL(toggled(bool)), @@ -1444,7 +1444,7 @@ void MixxxMainWindow::initActions() m_pViewVinylControl->setShortcut( QKeySequence(m_pKbdConfig->getValueString( ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowVinylControl"), - tr("Ctrl+3", "Menubar|View|Show Vinyl Control Section")))); + tr("Ctrl+6", "Menubar|View|Show Vinyl Control Section")))); m_pViewVinylControl->setStatusTip(showVinylControlText); m_pViewVinylControl->setWhatsThis(buildWhatsThis(showVinylControlTitle, showVinylControlText)); connect(m_pViewVinylControl, SIGNAL(toggled(bool)), @@ -1473,7 +1473,7 @@ void MixxxMainWindow::initActions() m_pViewShowPreviewDeck->setShortcut( QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowPreviewDeck"), - tr("Ctrl+4", "Menubar|View|Show Preview Deck")))); + tr("Ctrl+0", "Menubar|View|Show Preview Deck")))); m_pViewShowPreviewDeck->setStatusTip(showPreviewDeckText); m_pViewShowPreviewDeck->setWhatsThis(buildWhatsThis(showPreviewDeckTitle, showPreviewDeckText)); connect(m_pViewShowPreviewDeck, SIGNAL(toggled(bool)), @@ -1487,7 +1487,7 @@ void MixxxMainWindow::initActions() m_pViewShowEffects->setShortcut( QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowEffects"), - tr("Ctrl+5", "Menubar|View|Show Effect Rack")))); + tr("Ctrl+3", "Menubar|View|Show Effect Rack")))); m_pViewShowEffects->setStatusTip(showEffectsText); m_pViewShowEffects->setWhatsThis(buildWhatsThis(showEffectsTitle, showEffectsText)); connect(m_pViewShowEffects, SIGNAL(toggled(bool)), @@ -1501,7 +1501,7 @@ void MixxxMainWindow::initActions() m_pViewShowCoverArt->setShortcut( QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowCoverArt"), - tr("Ctrl+6", "Menubar|View|Show Cover Art")))); + tr("Ctrl+'", "Menubar|View|Show Cover Art")))); m_pViewShowCoverArt->setStatusTip(showCoverArtText); m_pViewShowCoverArt->setWhatsThis(buildWhatsThis(showCoverArtTitle, showCoverArtText)); connect(m_pViewShowCoverArt, SIGNAL(toggled(bool)), @@ -1543,7 +1543,7 @@ void MixxxMainWindow::initActions() m_pViewShowMixer->setShortcut( QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowMixer"), - tr("Ctrl+1", "Menubar|View|Show Mixer")))); + tr("Ctrl+7", "Menubar|View|Show Mixer")))); m_pViewShowMixer->setStatusTip(showMixerText); m_pViewShowMixer->setWhatsThis(buildWhatsThis(showMixerTitle, showMixerText)); connect(m_pViewShowMixer, SIGNAL(toggled(bool)), @@ -1557,7 +1557,7 @@ void MixxxMainWindow::initActions() m_pViewShowEqs->setShortcut( QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowEqs"), - tr("Ctrl+1", "Menubar|View|Show Eqs")))); + tr("Ctrl+8", "Menubar|View|Show Eqs")))); m_pViewShowEqs->setStatusTip(showEqsText); m_pViewShowEqs->setWhatsThis(buildWhatsThis(showEqsTitle, showEqsText)); connect(m_pViewShowEqs, SIGNAL(toggled(bool)), @@ -1571,7 +1571,7 @@ void MixxxMainWindow::initActions() m_pViewShowXFader->setShortcut( QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", "ViewMenu_ShowXFader"), - tr("Ctrl+1", "Menubar|View|Show Crossfader")))); + tr("Ctrl+9", "Menubar|View|Show Crossfader")))); m_pViewShowXFader->setStatusTip(showXFaderText); m_pViewShowXFader->setWhatsThis(buildWhatsThis(showXFaderTitle, showXFaderText)); connect(m_pViewShowXFader, SIGNAL(toggled(bool)), @@ -1585,7 +1585,7 @@ void MixxxMainWindow::initActions() m_pViewShow4Decks->setShortcut( QKeySequence(m_pKbdConfig->getValueString(ConfigKey("[KeyboardShortcuts]", "ViewMenu_Show4Decks"), - tr("Ctrl+1", "Menubar|View|Show 4 Decks")))); + tr("Ctrl+5", "Menubar|View|Show 4 Decks")))); m_pViewShow4Decks->setStatusTip(show4DecksText); m_pViewShow4Decks->setWhatsThis(buildWhatsThis(show4DecksTitle, show4DecksText)); connect(m_pViewShow4Decks, SIGNAL(toggled(bool)), From 46520d53ba4dca115b5371e4d399089a4174caff Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Mon, 6 Jul 2015 21:17:36 +0200 Subject: [PATCH 463/481] Added show crossfader option the Shade. --- res/skins/Shade/mixer_panel.xml | 4 ++++ res/skins/Shade/skin.xml | 1 + 2 files changed, 5 insertions(+) diff --git a/res/skins/Shade/mixer_panel.xml b/res/skins/Shade/mixer_panel.xml index 1abe74d76e9..b023e7c10c6 100644 --- a/res/skins/Shade/mixer_panel.xml +++ b/res/skins/Shade/mixer_panel.xml @@ -137,6 +137,10 @@ [Master],crossfader false + + [Master],show_xfader + visible + - - MixerLeftMargin - horizontal - me,min - 1, - - - - - MixerContainer - vertical - e,max - - - max,min - horizontal - 400,400 - - - max,min - horizontal - - - - - [Master],show_4decks - visible - - - - - - max,min + + MixerLeftMargin + horizontal + me,min + 1, + + + + + MixerContainer + vertical + e,max + + + max,min + horizontal + 400,400 + + + max,min + horizontal + + + + + [Master],show_4decks + visible + + + + + + max,min horizontal From 6ea5d0d4cf996a25ffce5f0de564d43d5e18e021 Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Wed, 8 Jul 2015 08:21:56 +0200 Subject: [PATCH 466/481] Moved clock and latency meter to bottom bar. Moved Big library button to the left. --- res/skins/LateNight/style.qss | 1 + res/skins/LateNight/toolbar_med.xml | 80 +++++++++++++++++++---------- 2 files changed, 53 insertions(+), 28 deletions(-) diff --git a/res/skins/LateNight/style.qss b/res/skins/LateNight/style.qss index 8c19be85b2c..769d1699dfb 100644 --- a/res/skins/LateNight/style.qss +++ b/res/skins/LateNight/style.qss @@ -151,6 +151,7 @@ #LatencyMeter { margin: 1px; + padding-right: 4px; } #PrimaryMixer { diff --git a/res/skins/LateNight/toolbar_med.xml b/res/skins/LateNight/toolbar_med.xml index e253816a07b..f88df5de5a5 100644 --- a/res/skins/LateNight/toolbar_med.xml +++ b/res/skins/LateNight/toolbar_med.xml @@ -10,23 +10,24 @@ min,min - 55f,20f + 80f,20f + maximize_library GuiToggleButton 2 0 - MIC/AUX + BIG LIBRARY 1 - MIC/AUX + BIG LIBRARY - [Microphone],show_microphone + [Master],maximize_library LeftButton - [Microphone],show_microphone + [Master],maximize_library @@ -38,23 +39,22 @@ 55f,20f - show_effects GuiToggleButton 2 0 - EFFECTS + MIC/AUX 1 - EFFECTS + MIC/AUX - [EffectRack1],show + [Microphone],show_microphone LeftButton - [EffectRack1],show + [Microphone],show_microphone @@ -65,62 +65,86 @@ min,min - 62f,20f - show_samplers + 55f,20f + show_effects GuiToggleButton 2 0 - SAMPLERS + EFFECTS 1 - SAMPLERS + EFFECTS - [Samplers],show_samplers + [EffectRack1],show LeftButton - [Samplers],show_samplers + [EffectRack1],show - - UIButtonMargin - horizontal - me,min - - GuiToggleContainer vertical min,min - 80f,20f - maximize_library + 62f,20f + show_samplers GuiToggleButton 2 0 - BIG LIBRARY + SAMPLERS 1 - BIG LIBRARY + SAMPLERS - [Master],maximize_library + [Samplers],show_samplers LeftButton - [Master],maximize_library + [Samplers],show_samplers + + ClockWidget + horizontal + me,min + + + + + + horizontal + min,min + + + horizontal + 197f,min + + + + + From ac5f959aaa0517030deba8d12a9fefdb858b1935 Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Wed, 8 Jul 2015 08:27:16 +0200 Subject: [PATCH 467/481] Removed top toolbar. --- res/skins/LateNight/style.qss | 3 +- res/skins/LateNight/waveform_and_mixer.xml | 52 +--------------------- 2 files changed, 2 insertions(+), 53 deletions(-) diff --git a/res/skins/LateNight/style.qss b/res/skins/LateNight/style.qss index 769d1699dfb..c7b541ec56a 100644 --- a/res/skins/LateNight/style.qss +++ b/res/skins/LateNight/style.qss @@ -797,9 +797,8 @@ #ToolbarTop { qproperty-layoutAlignment: 'AlignLeft | AlignTop'; border: 1px solid #585858; - border-top: 0px; padding: 0px; - background-color: #1e1e1e; + background-color: #0f0f0f; } #WaveformMixerContainer { diff --git a/res/skins/LateNight/waveform_and_mixer.xml b/res/skins/LateNight/waveform_and_mixer.xml index 3446b1c162a..d25d33e8f87 100644 --- a/res/skins/LateNight/waveform_and_mixer.xml +++ b/res/skins/LateNight/waveform_and_mixer.xml @@ -7,58 +7,8 @@ horizontal ToolbarTop - me,min + me,12f - - horizontal - 64f,4f - - - - - horizontal - me,min - - - - - ClockWidget - horizontal - min,min - - - - - - horizontal - me,min - - - - - horizontal - min,min - - - - horizontal - me,min - 5, - - - - - From 52a5d4dc8e8c99868567095c0d50436fa326ae66 Mon Sep 17 00:00:00 2001 From: Ferran Pujol Camins Date: Wed, 8 Jul 2015 09:17:11 +0200 Subject: [PATCH 468/481] Unified all LateNight toolbars. --- res/skins/LateNight/lower_half.xml | 10 +- res/skins/LateNight/skin.xml | 2 +- .../{toolbar_med.xml => toolbar.xml} | 0 res/skins/LateNight/toolbar_big_library.xml | 43 ----- res/skins/LateNight/toolbar_full.xml | 126 -------------- res/skins/LateNight/toolbar_small.xml | 159 ------------------ 6 files changed, 8 insertions(+), 332 deletions(-) rename res/skins/LateNight/{toolbar_med.xml => toolbar.xml} (100%) delete mode 100644 res/skins/LateNight/toolbar_big_library.xml delete mode 100644 res/skins/LateNight/toolbar_full.xml delete mode 100644 res/skins/LateNight/toolbar_small.xml diff --git a/res/skins/LateNight/lower_half.xml b/res/skins/LateNight/lower_half.xml index bf3f3af1995..1ecd07645da 100644 --- a/res/skins/LateNight/lower_half.xml +++ b/res/skins/LateNight/lower_half.xml @@ -9,7 +9,11 @@ 1920,249

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%0A}8-zI2VzVM@-DgTLj*VVG)RtlSfkA$&=r3e2%|? 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 $~Zjce*`!ujKg-$(fbgo1|+l_;I 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 (K6>6wO~2Ht#XHj=;Ny0)%HBN8r5dS} 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 cSU>x3pSOQz`qQNQ|IRPz{jX# Date: Thu, 18 Dec 2014 08:09:38 +0100 Subject: [PATCH 026/481] Replace Q_ASSERT with DEBUG_ASSERT when reading audio data --- plugins/soundsourcem4a/audiosourcem4a.cpp | 12 ++++++------ src/analyserqueue.cpp | 5 +++-- src/audiosource.h | 9 +++++---- src/audiosourceflac.cpp | 16 ++++++++-------- src/audiosourcemp3.cpp | 16 ++++++++-------- src/cachingreader.cpp | 14 +++++++------- 6 files changed, 37 insertions(+), 35 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 3f0ed3d27cd..5991784b594 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -218,13 +218,13 @@ AudioSource::diff_type AudioSourceM4A::seekFrame(diff_type frameIndex) { m_inputBufferLength = 0; } // decoding starts before the actual target position - Q_ASSERT(m_curFrameIndex <= frameIndex); + DEBUG_ASSERT(m_curFrameIndex <= frameIndex); const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; // prefetch (decode and discard) all samples up to the target position - Q_ASSERT(frames2samples(prefetchFrameCount) <= m_prefetchSampleBuffer.size()); + DEBUG_ASSERT(frames2samples(prefetchFrameCount) <= m_prefetchSampleBuffer.size()); readFrameSamplesInterleaved(prefetchFrameCount, &m_prefetchSampleBuffer[0]); } - Q_ASSERT(m_curFrameIndex == frameIndex); + DEBUG_ASSERT(m_curFrameIndex == frameIndex); return frameIndex; } @@ -257,17 +257,17 @@ AudioSource::size_type AudioSourceM4A::readFrameSamplesInterleaved( const size_type readFrameCount = m_curFrameIndex - readFrameIndex; const size_type decodeBufferCapacityInBytes = frames2samples( frameCount - readFrameCount) * sizeof(*sampleBuffer); - Q_ASSERT(0 < decodeBufferCapacityInBytes); + DEBUG_ASSERT(0 < decodeBufferCapacityInBytes); void* pDecodeBuffer = pSampleBuffer; NeAACDecDecode2(m_hDecoder, &decFrameInfo, &m_inputBuffer[m_inputBufferOffset], m_inputBufferLength / sizeof(m_inputBuffer[0]), &pDecodeBuffer, decodeBufferCapacityInBytes); // samples should have been decoded into our own buffer - Q_ASSERT(pSampleBuffer == pDecodeBuffer); + DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); pSampleBuffer += decFrameInfo.samples; // only the input data that is available should have been read - Q_ASSERT( + DEBUG_ASSERT( decFrameInfo.bytesconsumed <= (m_inputBufferLength / sizeof(m_inputBuffer[0]))); // consume input data diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index d67a83c12de..4a1146e94f1 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -15,6 +15,7 @@ #include "analyserbeats.h" #include "analyserkey.h" #include "vamp/vampanalyser.h" +#include "util/assert.h" #include "util/compatibility.h" #include "util/event.h" #include "util/trace.h" @@ -178,7 +179,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi // full block size. if (readFrameCount < kAnalysisFrameCount) { // The whole file should have been read now! - Q_ASSERT(pAudioSource->getFrameCount() == (progressFrameCount + readFrameCount)); + DEBUG_ASSERT(pAudioSource->getFrameCount() == (progressFrameCount + readFrameCount)); break; // done } @@ -195,7 +196,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi // because the finalise functions will take also some time //fp div here prevents insane signed overflow progressFrameCount += readFrameCount; - Q_ASSERT(progressFrameCount <= pAudioSource->getFrameCount()); + DEBUG_ASSERT(progressFrameCount <= pAudioSource->getFrameCount()); int progress = (int)(((float)progressFrameCount) / pAudioSource->getFrameCount() * (1000 - FINALIZE_PERCENT)); diff --git a/src/audiosource.h b/src/audiosource.h index 8ca19a273e9..ec03547a02b 100644 --- a/src/audiosource.h +++ b/src/audiosource.h @@ -1,6 +1,7 @@ #ifndef MIXXX_AUDIOSOURCE_H #define MIXXX_AUDIOSOURCE_H +#include "util/assert.h" #include "util/types.h" // CSAMPLE #include @@ -100,22 +101,22 @@ class AudioSource { return isValid(); } inline size_type getDuration() const { - Q_ASSERT(hasDuration()); // prevents division by zero + DEBUG_ASSERT(hasDuration()); // prevents division by zero return getFrameCount() / getFrameRate(); } // #frames -> #samples template inline T frames2samples(T frameCount) const { - Q_ASSERT(isChannelCountValid()); + DEBUG_ASSERT(isChannelCountValid()); return frameCount * getChannelCount(); } // #samples -> #frames template inline T samples2frames(T sampleCount) const { - Q_ASSERT(isChannelCountValid()); - Q_ASSERT(0 == (sampleCount % getChannelCount())); + DEBUG_ASSERT(isChannelCountValid()); + DEBUG_ASSERT(0 == (sampleCount % getChannelCount())); return sampleCount / getChannelCount(); } diff --git a/src/audiosourceflac.cpp b/src/audiosourceflac.cpp index c8d7234c8d3..f8b69160caf 100644 --- a/src/audiosourceflac.cpp +++ b/src/audiosourceflac.cpp @@ -145,7 +145,7 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readFrameSamplesInterleaved( sample_type* outBuffer = sampleBuffer; size_type framesRemaining = frameCount; while (0 < framesRemaining) { - Q_ASSERT( + DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); // if our buffer from libflac is empty (either because we explicitly cleared @@ -165,7 +165,7 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readFrameSamplesInterleaved( break; } } - Q_ASSERT( + DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); const size_type decodeBufferSamples = m_decodeSampleBufferWriteOffset @@ -193,7 +193,7 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readFrameSamplesInterleaved( } m_decodeSampleBufferReadOffset += samplesToCopy; framesRemaining -= framesToCopy; - Q_ASSERT( + DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); } @@ -255,14 +255,14 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( << frame->header.channels; return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - Q_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - Q_ASSERT( + DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + DEBUG_ASSERT( (m_decodeSampleBuffer.size() - m_decodeSampleBufferWriteOffset) >= frames2samples(frame->header.blocksize)); switch (getChannelCount()) { case 1: { // optimized code for 1 channel (mono) - Q_ASSERT(1 <= frame->header.channels); + DEBUG_ASSERT(1 <= frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = buffer[0][i] * m_sampleScale; @@ -271,7 +271,7 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( } case 2: { // optimized code for 2 channels (stereo) - Q_ASSERT(2 <= frame->header.channels); + DEBUG_ASSERT(2 <= frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = buffer[0][i] * m_sampleScale; @@ -290,7 +290,7 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( } } } - Q_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; } diff --git a/src/audiosourcemp3.cpp b/src/audiosourcemp3.cpp index 9b28ed40bae..e5e003e4114 100644 --- a/src/audiosourcemp3.cpp +++ b/src/audiosourcemp3.cpp @@ -242,8 +242,8 @@ AudioSourceMp3::MadSeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(d AudioSourceMp3::MadSeekFrameList::size_type lowerBound = 0; AudioSourceMp3::MadSeekFrameList::size_type upperBound = m_seekFrameList.size(); while ((upperBound - lowerBound) > 1) { - Q_ASSERT(seekFrameIndex >= lowerBound); - Q_ASSERT(seekFrameIndex < upperBound); + DEBUG_ASSERT(seekFrameIndex >= lowerBound); + DEBUG_ASSERT(seekFrameIndex < upperBound); if (m_seekFrameList[seekFrameIndex].frameIndex > frameIndex) { upperBound = seekFrameIndex; } else { @@ -251,9 +251,9 @@ AudioSourceMp3::MadSeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(d } seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; } - Q_ASSERT(m_seekFrameList.size() > seekFrameIndex); - Q_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); - Q_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); + DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); + DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); + DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); return seekFrameIndex; } @@ -272,7 +272,7 @@ AudioSource::diff_type AudioSourceMp3::seekFrame(diff_type frameIndex) { } else { seekFrameIndex -= kSeekFramePrefetchCount; } - Q_ASSERT(seekFrameIndex < m_seekFrameList.size()); + DEBUG_ASSERT(seekFrameIndex < m_seekFrameList.size()); const MadSeekFrameType& seekFrame(m_seekFrameList[seekFrameIndex]); // restart decoder mad_synth_finish(&m_madSynth); @@ -287,9 +287,9 @@ AudioSource::diff_type AudioSourceMp3::seekFrame(diff_type frameIndex) { m_madSynthCount = 0; } // decode and discard prefetch data - Q_ASSERT(m_curFrameIndex <= frameIndex); + DEBUG_ASSERT(m_curFrameIndex <= frameIndex); skipFrameSamples(frameIndex - m_curFrameIndex); - Q_ASSERT(m_curFrameIndex == frameIndex); + DEBUG_ASSERT(m_curFrameIndex == frameIndex); return m_curFrameIndex; } diff --git a/src/cachingreader.cpp b/src/cachingreader.cpp index 2b20a163902..f715be9913b 100644 --- a/src/cachingreader.cpp +++ b/src/cachingreader.cpp @@ -324,8 +324,8 @@ int CachingReader::read(int sample, int num_samples, CSAMPLE* buffer) { //if we're in preroll... if (sample < 0) { int zero_samples = math_min(-sample, samples_remaining); - Q_ASSERT(0 <= zero_samples); - Q_ASSERT(zero_samples <= samples_remaining); + DEBUG_ASSERT(0 <= zero_samples); + DEBUG_ASSERT(zero_samples <= samples_remaining); SampleUtil::clear(buffer, zero_samples); samples_remaining -= zero_samples; if (samples_remaining == 0) { @@ -333,12 +333,12 @@ int CachingReader::read(int sample, int num_samples, CSAMPLE* buffer) { } buffer += zero_samples; sample += zero_samples; - Q_ASSERT(0 <= sample); + DEBUG_ASSERT(0 <= sample); } - Q_ASSERT(0 == (sample % CachingReaderWorker::kChunkChannels)); + DEBUG_ASSERT(0 == (sample % CachingReaderWorker::kChunkChannels)); const int frame = sample / CachingReaderWorker::kChunkChannels; - Q_ASSERT(0 == (samples_remaining % CachingReaderWorker::kChunkChannels)); + DEBUG_ASSERT(0 == (samples_remaining % CachingReaderWorker::kChunkChannels)); int frames_remaining = samples_remaining / CachingReaderWorker::kChunkChannels; const int start_frame = math_min(m_iTrackNumFramesCallbackSafe, frame); const int start_chunk = chunkForFrame(start_frame); @@ -477,9 +477,9 @@ void CachingReader::hintAndMaybeWake(const QVector& hintList) { qDebug() << "ERROR: Negative hint length. Ignoring."; continue; } - Q_ASSERT(0 == (hint.sample % CachingReaderWorker::kChunkChannels)); + DEBUG_ASSERT(0 == (hint.sample % CachingReaderWorker::kChunkChannels)); const int frame = hint.sample / CachingReaderWorker::kChunkChannels; - Q_ASSERT(0 == (hint.length % CachingReaderWorker::kChunkChannels)); + DEBUG_ASSERT(0 == (hint.length % CachingReaderWorker::kChunkChannels)); const int frame_count = hint.length / CachingReaderWorker::kChunkChannels; const int start_frame = math_clamp(frame, 0, m_iTrackNumFramesCallbackSafe); From ad5819559b56522c3013127afd329cfdd488303f Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 18 Dec 2014 17:20:41 +0100 Subject: [PATCH 027/481] Move Audio-/SoundSources code into separate directory --- build/depends.py | 11 ++++++----- build/features.py | 12 ++++++------ plugins/soundsourcem4a/SConscript | 4 ++-- plugins/soundsourcem4a/audiosourcem4a.h | 2 +- plugins/soundsourcem4a/soundsourcem4a.cpp | 2 +- plugins/soundsourcem4a/soundsourcem4a.h | 2 +- plugins/soundsourcemediafoundation/SConscript | 2 +- .../audiosourcemediafoundation.h | 2 +- .../soundsourcemediafoundation.cpp | 2 +- .../soundsourcemediafoundation.h | 2 +- plugins/soundsourcewv/SConscript | 4 ++-- plugins/soundsourcewv/audiosourcewv.h | 2 +- plugins/soundsourcewv/soundsourcewv.cpp | 2 +- plugins/soundsourcewv/soundsourcewv.h | 2 +- src/analyserqueue.h | 2 +- src/cachingreaderworker.h | 2 +- src/dlgprefmodplug.cpp | 2 +- src/soundsourceproxy.cpp | 16 ++++++++-------- src/soundsourceproxy.h | 2 +- src/{ => sources}/audiosource.cpp | 2 +- src/{ => sources}/audiosource.h | 0 src/{ => sources}/audiosourcecoreaudio.cpp | 2 +- src/{ => sources}/audiosourcecoreaudio.h | 2 +- src/{ => sources}/audiosourceffmpeg.cpp | 2 +- src/{ => sources}/audiosourceffmpeg.h | 2 +- src/{ => sources}/audiosourceflac.cpp | 2 +- src/{ => sources}/audiosourceflac.h | 2 +- src/{ => sources}/audiosourcemodplug.cpp | 2 +- src/{ => sources}/audiosourcemodplug.h | 2 +- src/{ => sources}/audiosourcemp3.cpp | 2 +- src/{ => sources}/audiosourcemp3.h | 2 +- src/{ => sources}/audiosourceoggvorbis.cpp | 2 +- src/{ => sources}/audiosourceoggvorbis.h | 2 +- src/{ => sources}/audiosourceopus.cpp | 2 +- src/{ => sources}/audiosourceopus.h | 2 +- src/{ => sources}/audiosourcesndfile.cpp | 2 +- src/{ => sources}/audiosourcesndfile.h | 2 +- src/{ => sources}/soundsource.cpp | 2 +- src/{ => sources}/soundsource.h | 2 +- src/{ => sources}/soundsourcecoreaudio.cpp | 6 +++--- src/{ => sources}/soundsourcecoreaudio.h | 2 +- src/{ => sources}/soundsourceffmpeg.cpp | 4 ++-- src/{ => sources}/soundsourceffmpeg.h | 2 +- src/{ => sources}/soundsourceflac.cpp | 4 ++-- src/{ => sources}/soundsourceflac.h | 2 +- src/{ => sources}/soundsourcemodplug.cpp | 4 ++-- src/{ => sources}/soundsourcemodplug.h | 2 +- src/{ => sources}/soundsourcemp3.cpp | 4 ++-- src/{ => sources}/soundsourcemp3.h | 2 +- src/{ => sources}/soundsourceoggvorbis.cpp | 4 ++-- src/{ => sources}/soundsourceoggvorbis.h | 2 +- src/{ => sources}/soundsourceopus.cpp | 4 ++-- src/{ => sources}/soundsourceopus.h | 2 +- src/{ => sources}/soundsourcesndfile.cpp | 4 ++-- src/{ => sources}/soundsourcesndfile.h | 2 +- 55 files changed, 82 insertions(+), 81 deletions(-) rename src/{ => sources}/audiosource.cpp (98%) rename src/{ => sources}/audiosource.h (100%) rename src/{ => sources}/audiosourcecoreaudio.cpp (99%) rename src/{ => sources}/audiosourcecoreaudio.h (97%) rename src/{ => sources}/audiosourceffmpeg.cpp (99%) rename src/{ => sources}/audiosourceffmpeg.h (98%) rename src/{ => sources}/audiosourceflac.cpp (99%) rename src/{ => sources}/audiosourceflac.h (98%) rename src/{ => sources}/audiosourcemodplug.cpp (99%) rename src/{ => sources}/audiosourcemodplug.h (97%) rename src/{ => sources}/audiosourcemp3.cpp (99%) rename src/{ => sources}/audiosourcemp3.h (98%) rename src/{ => sources}/audiosourceoggvorbis.cpp (99%) rename src/{ => sources}/audiosourceoggvorbis.h (96%) rename src/{ => sources}/audiosourceopus.cpp (98%) rename src/{ => sources}/audiosourceopus.h (96%) rename src/{ => sources}/audiosourcesndfile.cpp (98%) rename src/{ => sources}/audiosourcesndfile.h (96%) rename src/{ => sources}/soundsource.cpp (97%) rename src/{ => sources}/soundsource.h (98%) rename src/{ => sources}/soundsourcecoreaudio.cpp (97%) rename src/{ => sources}/soundsourcecoreaudio.h (97%) rename src/{ => sources}/soundsourceffmpeg.cpp (98%) rename src/{ => sources}/soundsourceffmpeg.h (97%) rename src/{ => sources}/soundsourceflac.cpp (97%) rename src/{ => sources}/soundsourceflac.h (97%) rename src/{ => sources}/soundsourcemodplug.cpp (96%) rename src/{ => sources}/soundsourcemodplug.h (94%) rename src/{ => sources}/soundsourcemp3.cpp (97%) rename src/{ => sources}/soundsourcemp3.h (97%) rename src/{ => sources}/soundsourceoggvorbis.cpp (96%) rename src/{ => sources}/soundsourceoggvorbis.h (97%) rename src/{ => sources}/soundsourceopus.cpp (98%) rename src/{ => sources}/soundsourceopus.h (93%) rename src/{ => sources}/soundsourcesndfile.cpp (97%) rename src/{ => sources}/soundsourcesndfile.h (93%) diff --git a/build/depends.py b/build/depends.py index f8644619896..f5b9a97aa77 100644 --- a/build/depends.py +++ b/build/depends.py @@ -126,7 +126,7 @@ def configure(self, build, conf): 'Did not find libvorbisenc.a, libvorbisenc.lib, or the libvorbisenc development headers.') def sources(self, build): - return ['audiosourceoggvorbis.cpp','soundsourceoggvorbis.cpp'] + return ['sources/audiosourceoggvorbis.cpp','sources/soundsourceoggvorbis.cpp'] class SndFile(Dependence): @@ -139,7 +139,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='__SNDFILE__') def sources(self, build): - return ['audiosourcesndfile.cpp','soundsourcesndfile.cpp'] + return ['sources/audiosourcesndfile.cpp','sources/soundsourcesndfile.cpp'] class FLAC(Dependence): @@ -154,7 +154,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='FLAC__NO_DLL') def sources(self, build): - return ['audiosourceflac.cpp','soundsourceflac.cpp',] + return ['sources/audiosourceflac.cpp','sources/soundsourceflac.cpp',] class Qt(Dependence): @@ -656,8 +656,9 @@ def sources(self, build): "errordialoghandler.cpp", "upgrade.cpp", - "soundsource.cpp", - "audiosource.cpp", + "sources/soundsource.cpp", + "sources/audiosource.cpp", + "trackmetadata.cpp", "trackmetadatataglib.cpp", diff --git a/build/features.py b/build/features.py index 16176e38dcc..2b775d0a666 100644 --- a/build/features.py +++ b/build/features.py @@ -179,7 +179,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='__MAD__') def sources(self, build): - return ['audiosourcemp3.cpp','soundsourcemp3.cpp'] + return ['sources/soundsourcemp3.cpp','sources/audiosourcemp3.cpp'] class CoreAudio(Feature): @@ -214,7 +214,7 @@ def configure(self, build, conf): build.env.Append(CPPDEFINES='__COREAUDIO__') def sources(self, build): - return ['audiosourcecoreaudio.cpp','soundsourcecoreaudio.cpp', + return ['sources/soundsourcecoreaudio.cpp','sources/audiosourcecoreaudio.cpp', '#lib/apple/CAStreamBasicDescription.cpp'] @@ -429,7 +429,7 @@ def configure(self, build, conf): def sources(self, build): depends.Qt.uic(build)('dlgprefmodplugdlg.ui') - return ['audiosourcemodplug.cpp','soundsourcemodplug.cpp','dlgprefmodplug.cpp'] + return ['sources/soundsourcemodplug.cpp', 'sources/audiosourcemodplug.cpp', 'dlgprefmodplug.cpp'] class FAAD(Feature): @@ -802,7 +802,7 @@ def configure(self, build, conf): build.env.ParseConfig('pkg-config opusfile opus --silence-errors --cflags --libs') def sources(self, build): - return ['audiosourceopus.cpp','soundsourceopus.cpp'] + return ['sources/soundsourceopus.cpp','sources/audiosourceopus.cpp'] class FFMPEG(Feature): @@ -930,8 +930,8 @@ def configure(self, build, conf): 'include', 'ffmpeg')) def sources(self, build): - return ['audiosourceffmpeg.cpp', - 'soundsourceffmpeg.cpp', + return ['sources/soundsourceffmpeg.cpp', + 'sources/audiosourceffmpeg.cpp', 'encoder/encoderffmpegresample.cpp', 'encoder/encoderffmpegcore.cpp', 'encoder/encoderffmpegmp3.cpp', diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index 76aa5ce98b3..d40ca6bc552 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -13,8 +13,8 @@ Import('build') m4a_sources = [ "audiosourcem4a.cpp", # MP4/M4A audio support through FAAD/libmp4v2 "soundsourcem4a.cpp", # MP4/M4A tag support - "audiosource.cpp", # required to subclass AudioSource - "soundsource.cpp", # required to subclass SoundSource + "sources/audiosource.cpp", # required to subclass AudioSource + "sources/soundsource.cpp", # required to subclass SoundSource "trackmetadatataglib.cpp", # TagLib dependencies "trackmetadata.cpp", "sampleutil.cpp", # utility functions diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index c2d7a9fd9d6..007635a9f0d 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCEM4A_H #define AUDIOSOURCEM4A_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #ifdef __MP4V2__ diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 0cbd057350e..5981bc18363 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -15,8 +15,8 @@ ***************************************************************************/ #include "soundsourcem4a.h" - #include "audiosourcem4a.h" + #include "trackmetadatataglib.h" #include diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 713801ea77b..c4874739a5e 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -17,7 +17,7 @@ #ifndef SOUNDSOURCEM4A_H #define SOUNDSOURCEM4A_H -#include "soundsource.h" +#include "sources/soundsource.h" #include "defs_version.h" //As per QLibrary docs: http://doc.trolltech.com/4.6/qlibrary.html#resolve diff --git a/plugins/soundsourcemediafoundation/SConscript b/plugins/soundsourcemediafoundation/SConscript index d8152dc809e..2c8449d1222 100644 --- a/plugins/soundsourcemediafoundation/SConscript +++ b/plugins/soundsourcemediafoundation/SConscript @@ -18,7 +18,7 @@ if int(build.flags['mediafoundation']): else: env["LINKFLAGS"].remove("/subsystem:windows,5.01") ssmediafoundation_bin = env.SharedLibrary('soundsourcemediafoundation', - ['soundsourcemediafoundation.cpp', 'audiosourcemediafoundation.cpp', 'soundsource.cpp', 'audiosource.cpp', 'trackmetadatataglib.cpp', 'trackmetadata.cpp', 'sampleutil.cpp'], + ['soundsourcemediafoundation.cpp', 'audiosourcemediafoundation.cpp', 'sources/soundsource.cpp', 'sources/audiosource.cpp', 'trackmetadatataglib.cpp', 'trackmetadata.cpp', 'sampleutil.cpp'], LINKCOM = [env['LINKCOM'], 'mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;1']) Return("ssmediafoundation_bin") diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index 7e870fbc74a..d8f3afd8480 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H #define AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #include diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 5c975fe9bed..a12a5036c04 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -21,8 +21,8 @@ ***************************************************************************/ #include "soundsourcemediafoundation.h" - #include "audiosourcemediafoundation.h" + #include "trackmetadatataglib.h" #include diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index c23cf3c546c..b2ec7baf8fe 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -20,7 +20,7 @@ #ifndef SOUNDSOURCEMEDIAFOUNDATION_H #define SOUNDSOURCEMEDIAFOUNDATION_H -#include "soundsource.h" +#include "sources/soundsource.h" #include "defs_version.h" #ifdef Q_OS_WIN diff --git a/plugins/soundsourcewv/SConscript b/plugins/soundsourcewv/SConscript index bc9e129f945..8a0d364c9ed 100644 --- a/plugins/soundsourcewv/SConscript +++ b/plugins/soundsourcewv/SConscript @@ -13,8 +13,8 @@ Import('build') wv_sources = [ "soundsourcewv.cpp", # Wavpack support (tags) "audiosourcewv.cpp", # Wavpack support (audio) - "soundsource.cpp", # required to subclass SoundSource - "audiosource.cpp", # required to subclass AudioSource + "sources/soundsource.cpp", # required to subclass SoundSource + "sources/audiosource.cpp", # required to subclass AudioSource "trackmetadatataglib.cpp", # TagLib dependencies "trackmetadata.cpp", "sampleutil.cpp", # utility functions diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index 17bdc630b43..d9fad4ded83 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCEWV_H #define AUDIOSOURCEWV_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #include "wavpack/wavpack.h" diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 16a434d83f3..64479f8256a 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -1,6 +1,6 @@ #include "soundsourcewv.h" - #include "audiosourcewv.h" + #include "trackmetadatataglib.h" #include diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 39c30e47345..68ddbcf614f 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -1,7 +1,7 @@ #ifndef SOUNDSOURCEWV_H #define SOUNDSOURCEWV_H -#include "soundsource.h" +#include "sources/soundsource.h" #include "defs_version.h" #ifdef Q_OS_WIN diff --git a/src/analyserqueue.h b/src/analyserqueue.h index 1b6ae6fea8b..c6787f47f2e 100644 --- a/src/analyserqueue.h +++ b/src/analyserqueue.h @@ -4,7 +4,7 @@ #include "configobject.h" #include "analyser.h" #include "trackinfoobject.h" -#include "audiosource.h" +#include "sources/audiosource.h" #include #include diff --git a/src/cachingreaderworker.h b/src/cachingreaderworker.h index 9cb57c7d6e1..8244f14e086 100644 --- a/src/cachingreaderworker.h +++ b/src/cachingreaderworker.h @@ -7,9 +7,9 @@ #include #include -#include "audiosource.h" #include "trackinfoobject.h" #include "engine/engineworker.h" +#include "sources/audiosource.h" #include "util/fifo.h" diff --git a/src/dlgprefmodplug.cpp b/src/dlgprefmodplug.cpp index 85b71d45a05..a4fa985f395 100644 --- a/src/dlgprefmodplug.cpp +++ b/src/dlgprefmodplug.cpp @@ -5,7 +5,7 @@ #include "ui_dlgprefmodplugdlg.h" #include "configobject.h" -#include "audiosourcemodplug.h" +#include "sources/audiosourcemodplug.h" #define kConfigKey "[Modplug]" diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index 640ffaf96a6..21016f09252 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -28,25 +28,25 @@ #include "trackinfoobject.h" #include "soundsourceproxy.h" #ifdef __MAD__ -#include "soundsourcemp3.h" +#include "sources/soundsourcemp3.h" #endif -#include "soundsourceoggvorbis.h" +#include "sources/soundsourceoggvorbis.h" #ifdef __OPUS__ -#include "soundsourceopus.h" +#include "sources/soundsourceopus.h" #endif #ifdef __COREAUDIO__ -#include "soundsourcecoreaudio.h" +#include "sources/soundsourcecoreaudio.h" #endif #ifdef __SNDFILE__ -#include "soundsourcesndfile.h" +#include "sources/soundsourcesndfile.h" #endif #ifdef __FFMPEGFILE__ -#include "soundsourceffmpeg.h" +#include "sources/soundsourceffmpeg.h" #endif #ifdef __MODPLUG__ -#include "soundsourcemodplug.h" +#include "sources/soundsourcemodplug.h" #endif -#include "soundsourceflac.h" +#include "sources/soundsourceflac.h" #include "util/cmdlineargs.h" #include "util/regex.h" diff --git a/src/soundsourceproxy.h b/src/soundsourceproxy.h index 631d8d1f355..3da0d95f976 100644 --- a/src/soundsourceproxy.h +++ b/src/soundsourceproxy.h @@ -24,8 +24,8 @@ #include #include -#include "soundsource.h" #include "trackinfoobject.h" +#include "sources/soundsource.h" #include "util/sandbox.h" /** diff --git a/src/audiosource.cpp b/src/sources/audiosource.cpp similarity index 98% rename from src/audiosource.cpp rename to src/sources/audiosource.cpp index 6d8451c3f95..2443b7dbb5d 100644 --- a/src/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -1,4 +1,4 @@ -#include "audiosource.h" +#include "sources/audiosource.h" #include "sampleutil.h" diff --git a/src/audiosource.h b/src/sources/audiosource.h similarity index 100% rename from src/audiosource.h rename to src/sources/audiosource.h diff --git a/src/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp similarity index 99% rename from src/audiosourcecoreaudio.cpp rename to src/sources/audiosourcecoreaudio.cpp index 3ef3047caea..b49708ff6fd 100644 --- a/src/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -1,4 +1,4 @@ -#include "audiosourcecoreaudio.h" +#include "sources/audiosourcecoreaudio.h" #include "util/math.h" diff --git a/src/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h similarity index 97% rename from src/audiosourcecoreaudio.h rename to src/sources/audiosourcecoreaudio.h index f46b348c7fd..34fe431ad51 100644 --- a/src/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCECOREAUDIO_H #define AUDIOSOURCECOREAUDIO_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #include diff --git a/src/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp similarity index 99% rename from src/audiosourceffmpeg.cpp rename to src/sources/audiosourceffmpeg.cpp index f9df8c84880..3f45b66dd66 100644 --- a/src/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -1,4 +1,4 @@ -#include "audiosourceffmpeg.h" +#include "sources/audiosourceffmpeg.h" #include diff --git a/src/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h similarity index 98% rename from src/audiosourceffmpeg.h rename to src/sources/audiosourceffmpeg.h index 8bed1627d9b..465b8698261 100644 --- a/src/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCEFFMPEG_H #define AUDIOSOURCEFFMPEG_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #include diff --git a/src/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp similarity index 99% rename from src/audiosourceflac.cpp rename to src/sources/audiosourceflac.cpp index f8b69160caf..a5548bb8a78 100644 --- a/src/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -1,4 +1,4 @@ -#include "audiosourceflac.h" +#include "sources/audiosourceflac.h" #include "sampleutil.h" #include "util/math.h" diff --git a/src/audiosourceflac.h b/src/sources/audiosourceflac.h similarity index 98% rename from src/audiosourceflac.h rename to src/sources/audiosourceflac.h index fb1c7a74c65..04befb5bcb3 100644 --- a/src/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCEFLAC_H #define AUDIOSOURCEFLAC_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #include diff --git a/src/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp similarity index 99% rename from src/audiosourcemodplug.cpp rename to src/sources/audiosourcemodplug.cpp index de4b497dc59..99a7f5b7b62 100644 --- a/src/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -1,4 +1,4 @@ -#include "audiosourcemodplug.h" +#include "sources/audiosourcemodplug.h" #include "util/timer.h" diff --git a/src/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h similarity index 97% rename from src/audiosourcemodplug.h rename to src/sources/audiosourcemodplug.h index fe256719c55..951c5956625 100644 --- a/src/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCEMODPLUG_H #define AUDIOSOURCEMODPLUG_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" namespace ModPlug { diff --git a/src/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp similarity index 99% rename from src/audiosourcemp3.cpp rename to src/sources/audiosourcemp3.cpp index e5e003e4114..fcac2742c57 100644 --- a/src/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -1,4 +1,4 @@ -#include "audiosourcemp3.h" +#include "sources/audiosourcemp3.h" #include "util/math.h" diff --git a/src/audiosourcemp3.h b/src/sources/audiosourcemp3.h similarity index 98% rename from src/audiosourcemp3.h rename to src/sources/audiosourcemp3.h index 45db6972907..2151454ddd7 100644 --- a/src/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCEMP3_H #define AUDIOSOURCEMP3_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #ifdef _MSC_VER diff --git a/src/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp similarity index 99% rename from src/audiosourceoggvorbis.cpp rename to src/sources/audiosourceoggvorbis.cpp index eb141235cc7..8669cce6ec1 100644 --- a/src/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -1,4 +1,4 @@ -#include "audiosourceoggvorbis.h" +#include "sources/audiosourceoggvorbis.h" #include diff --git a/src/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h similarity index 96% rename from src/audiosourceoggvorbis.h rename to src/sources/audiosourceoggvorbis.h index e72dce33247..2f0cc847886 100644 --- a/src/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCEOGGVORBIS_H #define AUDIOSOURCEOGGVORBIS_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #define OV_EXCLUDE_STATIC_CALLBACKS diff --git a/src/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp similarity index 98% rename from src/audiosourceopus.cpp rename to src/sources/audiosourceopus.cpp index 1d972473338..839a4b8a1a6 100644 --- a/src/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -1,4 +1,4 @@ -#include "audiosourceopus.h" +#include "sources/audiosourceopus.h" namespace Mixxx { diff --git a/src/audiosourceopus.h b/src/sources/audiosourceopus.h similarity index 96% rename from src/audiosourceopus.h rename to src/sources/audiosourceopus.h index ce331211aea..50f1dc1d7a3 100644 --- a/src/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCEOPUS_H #define AUDIOSOURCEOPUS_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #define OV_EXCLUDE_STATIC_CALLBACKS diff --git a/src/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp similarity index 98% rename from src/audiosourcesndfile.cpp rename to src/sources/audiosourcesndfile.cpp index 265278a427b..35b68254af6 100644 --- a/src/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -1,4 +1,4 @@ -#include "audiosourcesndfile.h" +#include "sources/audiosourcesndfile.h" namespace Mixxx { diff --git a/src/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h similarity index 96% rename from src/audiosourcesndfile.h rename to src/sources/audiosourcesndfile.h index b5d188399f6..1545dce7109 100644 --- a/src/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -1,7 +1,7 @@ #ifndef AUDIOSOURCESNDFILE_H #define AUDIOSOURCESNDFILE_H -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #ifdef Q_OS_WIN diff --git a/src/soundsource.cpp b/src/sources/soundsource.cpp similarity index 97% rename from src/soundsource.cpp rename to src/sources/soundsource.cpp index 9b045c9f218..3eddd2bce6b 100644 --- a/src/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -15,7 +15,7 @@ * * ***************************************************************************/ -#include "soundsource.h" +#include "sources/soundsource.h" #include "trackmetadata.h" namespace Mixxx { diff --git a/src/soundsource.h b/src/sources/soundsource.h similarity index 98% rename from src/soundsource.h rename to src/sources/soundsource.h index d90b6f31641..00fd863c746 100644 --- a/src/soundsource.h +++ b/src/sources/soundsource.h @@ -29,7 +29,7 @@ 7 - Mixxx 1.13.0 New AudioSource API */ -#include "audiosource.h" +#include "sources/audiosource.h" #include "util/defs.h" #include diff --git a/src/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp similarity index 97% rename from src/soundsourcecoreaudio.cpp rename to src/sources/soundsourcecoreaudio.cpp index 698f7b2185d..c3e26a0e170 100644 --- a/src/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -13,9 +13,9 @@ * * ***************************************************************************/ -#include "soundsourcecoreaudio.h" +#include "sources/soundsourcecoreaudio.h" -#include "audiosourcecoreaudio.h" +#include "sources/audiosourcecoreaudio.h" #include "trackmetadatataglib.h" #include @@ -37,7 +37,7 @@ QList SoundSourceCoreAudio::supportedFileExtensions() { } SoundSourceCoreAudio::SoundSourceCoreAudio(QString fileName) - : Super(filename) { + : Super(fileName) { } Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/soundsourcecoreaudio.h b/src/sources/soundsourcecoreaudio.h similarity index 97% rename from src/soundsourcecoreaudio.h rename to src/sources/soundsourcecoreaudio.h index 4fd4ba4a430..abe5cf95ccd 100644 --- a/src/soundsourcecoreaudio.h +++ b/src/sources/soundsourcecoreaudio.h @@ -19,7 +19,7 @@ #ifndef SOUNDSOURCECOREAUDIO_H #define SOUNDSOURCECOREAUDIO_H -#include "soundsource.h" +#include "sources/soundsource.h" class SoundSourceCoreAudio : public Mixxx::SoundSource { typedef SoundSource Super; diff --git a/src/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp similarity index 98% rename from src/soundsourceffmpeg.cpp rename to src/sources/soundsourceffmpeg.cpp index 6bb965a8643..1f3baec2b5e 100644 --- a/src/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -21,9 +21,9 @@ * * ***************************************************************************/ -#include "soundsourceffmpeg.h" +#include "sources/soundsourceffmpeg.h" -#include "audiosourceffmpeg.h" +#include "sources/audiosourceffmpeg.h" #include "trackmetadata.h" #include diff --git a/src/soundsourceffmpeg.h b/src/sources/soundsourceffmpeg.h similarity index 97% rename from src/soundsourceffmpeg.h rename to src/sources/soundsourceffmpeg.h index dd84f9e8d4b..017f08c591f 100644 --- a/src/soundsourceffmpeg.h +++ b/src/sources/soundsourceffmpeg.h @@ -18,7 +18,7 @@ #ifndef SOUNDSOURCEFFMPEG_H #define SOUNDSOURCEFFMPEG_H -#include "soundsource.h" +#include "sources/soundsource.h" class SoundSourceFFmpeg : public Mixxx::SoundSource { typedef SoundSource Super; diff --git a/src/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp similarity index 97% rename from src/soundsourceflac.cpp rename to src/sources/soundsourceflac.cpp index cb30a4d7808..ce5d253eddb 100644 --- a/src/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -13,10 +13,10 @@ * * ***************************************************************************/ -#include "soundsourceflac.h" +#include "sources/soundsourceflac.h" #include "trackmetadatataglib.h" -#include "audiosourceflac.h" +#include "sources/audiosourceflac.h" #include diff --git a/src/soundsourceflac.h b/src/sources/soundsourceflac.h similarity index 97% rename from src/soundsourceflac.h rename to src/sources/soundsourceflac.h index 6dcf0d44275..7a27997a629 100644 --- a/src/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -18,7 +18,7 @@ #ifndef SOUNDSOURCEFLAC_H #define SOUNDSOURCEFLAC_H -#include "soundsource.h" +#include "sources/soundsource.h" class SoundSourceFLAC : public Mixxx::SoundSource { typedef SoundSource Super; diff --git a/src/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp similarity index 96% rename from src/soundsourcemodplug.cpp rename to src/sources/soundsourcemodplug.cpp index ce848616f7b..ddb59f8ec03 100644 --- a/src/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -1,6 +1,6 @@ -#include "soundsourcemodplug.h" +#include "sources/soundsourcemodplug.h" -#include "audiosourcemodplug.h" +#include "sources/audiosourcemodplug.h" #include "trackmetadata.h" #include "util/timer.h" diff --git a/src/soundsourcemodplug.h b/src/sources/soundsourcemodplug.h similarity index 94% rename from src/soundsourcemodplug.h rename to src/sources/soundsourcemodplug.h index f7302b4c389..5b66619efc9 100644 --- a/src/soundsourcemodplug.h +++ b/src/sources/soundsourcemodplug.h @@ -1,7 +1,7 @@ #ifndef SOUNDSOURCEMODPLUG_H #define SOUNDSOURCEMODPLUG_H -#include "soundsource.h" +#include "sources/soundsource.h" // Class for reading tracker files using libmodplug. // The whole file is decoded at once and saved diff --git a/src/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp similarity index 97% rename from src/soundsourcemp3.cpp rename to src/sources/soundsourcemp3.cpp index acd85768724..478cb74d610 100644 --- a/src/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -14,9 +14,9 @@ * * ***************************************************************************/ -#include "soundsourcemp3.h" +#include "sources/soundsourcemp3.h" -#include "audiosourcemp3.h" +#include "sources/audiosourcemp3.h" #include "trackmetadatataglib.h" #include diff --git a/src/soundsourcemp3.h b/src/sources/soundsourcemp3.h similarity index 97% rename from src/soundsourcemp3.h rename to src/sources/soundsourcemp3.h index 11c2c994693..74efc9ca5b7 100644 --- a/src/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -18,7 +18,7 @@ #ifndef SOUNDSOURCEMP3_H #define SOUNDSOURCEMP3_H -#include "soundsource.h" +#include "sources/soundsource.h" /** *@author Tue and Ken Haste Andersen diff --git a/src/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp similarity index 96% rename from src/soundsourceoggvorbis.cpp rename to src/sources/soundsourceoggvorbis.cpp index 7824fbac05f..a6e92a557eb 100644 --- a/src/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -14,9 +14,9 @@ * * ***************************************************************************/ -#include "soundsourceoggvorbis.h" +#include "sources/soundsourceoggvorbis.h" -#include "audiosourceoggvorbis.h" +#include "sources/audiosourceoggvorbis.h" #include "trackmetadatataglib.h" #include diff --git a/src/soundsourceoggvorbis.h b/src/sources/soundsourceoggvorbis.h similarity index 97% rename from src/soundsourceoggvorbis.h rename to src/sources/soundsourceoggvorbis.h index ef44671229f..284a166de92 100644 --- a/src/soundsourceoggvorbis.h +++ b/src/sources/soundsourceoggvorbis.h @@ -17,7 +17,7 @@ #ifndef SOUNDSOURCEOGGVORBIS_H #define SOUNDSOURCEOGGVORBIS_H -#include "soundsource.h" +#include "sources/soundsource.h" class SoundSourceOggVorbis: public Mixxx::SoundSource { typedef SoundSource Super; diff --git a/src/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp similarity index 98% rename from src/soundsourceopus.cpp rename to src/sources/soundsourceopus.cpp index a3dd74adcb4..ebdac389486 100644 --- a/src/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -1,6 +1,6 @@ -#include "soundsourceopus.h" +#include "sources/soundsourceopus.h" -#include "audiosourceopus.h" +#include "sources/audiosourceopus.h" #include "trackmetadatataglib.h" // Include this if taglib if new enough (version 1.9.1 have opusfile) diff --git a/src/soundsourceopus.h b/src/sources/soundsourceopus.h similarity index 93% rename from src/soundsourceopus.h rename to src/sources/soundsourceopus.h index a52c1cccc04..8303dd97147 100644 --- a/src/soundsourceopus.h +++ b/src/sources/soundsourceopus.h @@ -1,7 +1,7 @@ #ifndef SOUNDSOURCEOPUS_H #define SOUNDSOURCEOPUS_H -#include "soundsource.h" +#include "sources/soundsource.h" class SoundSourceOpus: public Mixxx::SoundSource { typedef SoundSource Super; diff --git a/src/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp similarity index 97% rename from src/soundsourcesndfile.cpp rename to src/sources/soundsourcesndfile.cpp index 9040b69b92a..46c3892ff22 100644 --- a/src/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -1,6 +1,6 @@ -#include "soundsourcesndfile.h" +#include "sources/soundsourcesndfile.h" -#include "audiosourcesndfile.h" +#include "sources/audiosourcesndfile.h" #include "trackmetadatataglib.h" #include diff --git a/src/soundsourcesndfile.h b/src/sources/soundsourcesndfile.h similarity index 93% rename from src/soundsourcesndfile.h rename to src/sources/soundsourcesndfile.h index 28eb5d1c97f..b2ffa518970 100644 --- a/src/soundsourcesndfile.h +++ b/src/sources/soundsourcesndfile.h @@ -1,7 +1,7 @@ #ifndef SOUNDSOURCESNDFILE_H #define SOUNDSOURCESNDFILE_H -#include "soundsource.h" +#include "sources/soundsource.h" class SoundSourceSndFile: public Mixxx::SoundSource { typedef SoundSource Super; From e82f87273db41f349cf9bd19580571053ecf7691 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 18 Dec 2014 17:21:24 +0100 Subject: [PATCH 028/481] Move metadata code into separate directory --- build/depends.py | 4 ++-- plugins/soundsourcem4a/SConscript | 14 +++++++------- plugins/soundsourcem4a/soundsourcem4a.cpp | 2 +- plugins/soundsourcemediafoundation/SConscript | 2 +- .../soundsourcemediafoundation.cpp | 2 +- plugins/soundsourcewv/SConscript | 14 +++++++------- plugins/soundsourcewv/soundsourcewv.cpp | 2 +- src/{ => metadata}/trackmetadata.cpp | 2 +- src/{ => metadata}/trackmetadata.h | 0 src/{ => metadata}/trackmetadatataglib.cpp | 2 +- src/{ => metadata}/trackmetadatataglib.h | 2 +- src/sources/soundsource.cpp | 2 +- src/sources/soundsourcecoreaudio.cpp | 2 +- src/sources/soundsourceffmpeg.cpp | 2 +- src/sources/soundsourceflac.cpp | 2 +- src/sources/soundsourcemodplug.cpp | 2 +- src/sources/soundsourcemp3.cpp | 2 +- src/sources/soundsourceoggvorbis.cpp | 2 +- src/sources/soundsourceopus.cpp | 2 +- src/sources/soundsourcesndfile.cpp | 2 +- src/test/soundproxy_test.cpp | 2 +- src/trackinfoobject.cpp | 2 +- 22 files changed, 34 insertions(+), 34 deletions(-) rename src/{ => metadata}/trackmetadata.cpp (97%) rename src/{ => metadata}/trackmetadata.h (100%) rename src/{ => metadata}/trackmetadatataglib.cpp (99%) rename src/{ => metadata}/trackmetadatataglib.h (98%) diff --git a/build/depends.py b/build/depends.py index f5b9a97aa77..964156b8385 100644 --- a/build/depends.py +++ b/build/depends.py @@ -659,8 +659,8 @@ def sources(self, build): "sources/soundsource.cpp", "sources/audiosource.cpp", - "trackmetadata.cpp", - "trackmetadatataglib.cpp", + "metadata/trackmetadata.cpp", + "metadata/trackmetadatataglib.cpp", "sharedglcontext.cpp", "widget/controlwidgetconnection.cpp", diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index d40ca6bc552..46d3449d144 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -11,13 +11,13 @@ Import('build') # On Posix default SCons.LIBPREFIX = 'lib', on Windows default SCons.LIBPREFIX = '' m4a_sources = [ - "audiosourcem4a.cpp", # MP4/M4A audio support through FAAD/libmp4v2 - "soundsourcem4a.cpp", # MP4/M4A tag support - "sources/audiosource.cpp", # required to subclass AudioSource - "sources/soundsource.cpp", # required to subclass SoundSource - "trackmetadatataglib.cpp", # TagLib dependencies - "trackmetadata.cpp", - "sampleutil.cpp", # utility functions + "soundsourcem4a.cpp", + "audiosourcem4a.cpp", + "sources/soundsource.cpp", + "sources/audiosource.cpp", + "metadata/trackmetadata.cpp", + "metadata/trackmetadatataglib.cpp", + "sampleutil.cpp", ] #Tell SCons to build the SoundSourceM4A plugin diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 5981bc18363..9246c26c43f 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -17,7 +17,7 @@ #include "soundsourcem4a.h" #include "audiosourcem4a.h" -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" #include diff --git a/plugins/soundsourcemediafoundation/SConscript b/plugins/soundsourcemediafoundation/SConscript index 2c8449d1222..4e72423911e 100644 --- a/plugins/soundsourcemediafoundation/SConscript +++ b/plugins/soundsourcemediafoundation/SConscript @@ -18,7 +18,7 @@ if int(build.flags['mediafoundation']): else: env["LINKFLAGS"].remove("/subsystem:windows,5.01") ssmediafoundation_bin = env.SharedLibrary('soundsourcemediafoundation', - ['soundsourcemediafoundation.cpp', 'audiosourcemediafoundation.cpp', 'sources/soundsource.cpp', 'sources/audiosource.cpp', 'trackmetadatataglib.cpp', 'trackmetadata.cpp', 'sampleutil.cpp'], + ['soundsourcemediafoundation.cpp', 'audiosourcemediafoundation.cpp', 'sources/soundsource.cpp', 'sources/audiosource.cpp', 'metadata/trackmetadata.cpp', 'metadata/trackmetadatataglib.cpp', 'sampleutil.cpp'], LINKCOM = [env['LINKCOM'], 'mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;1']) Return("ssmediafoundation_bin") diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index a12a5036c04..b9369de6152 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -23,7 +23,7 @@ #include "soundsourcemediafoundation.h" #include "audiosourcemediafoundation.h" -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" #include diff --git a/plugins/soundsourcewv/SConscript b/plugins/soundsourcewv/SConscript index 8a0d364c9ed..4c996c35220 100644 --- a/plugins/soundsourcewv/SConscript +++ b/plugins/soundsourcewv/SConscript @@ -11,13 +11,13 @@ Import('build') # On Posix default SCons.LIBPREFIX = 'lib', on Windows default SCons.LIBPREFIX = '' wv_sources = [ - "soundsourcewv.cpp", # Wavpack support (tags) - "audiosourcewv.cpp", # Wavpack support (audio) - "sources/soundsource.cpp", # required to subclass SoundSource - "sources/audiosource.cpp", # required to subclass AudioSource - "trackmetadatataglib.cpp", # TagLib dependencies - "trackmetadata.cpp", - "sampleutil.cpp", # utility functions + "soundsourcewv.cpp", + "audiosourcewv.cpp", + "sources/soundsource.cpp", + "sources/audiosource.cpp", + "metadata/trackmetadata.cpp", + "metadata/trackmetadatataglib.cpp", + "sampleutil.cpp" ] diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 64479f8256a..60a3e692080 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -1,7 +1,7 @@ #include "soundsourcewv.h" #include "audiosourcewv.h" -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" #include diff --git a/src/trackmetadata.cpp b/src/metadata/trackmetadata.cpp similarity index 97% rename from src/trackmetadata.cpp rename to src/metadata/trackmetadata.cpp index f1c891a80a7..01cbd1d3430 100644 --- a/src/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -1,4 +1,4 @@ -#include "trackmetadata.h" +#include "metadata/trackmetadata.h" #include "util/math.h" diff --git a/src/trackmetadata.h b/src/metadata/trackmetadata.h similarity index 100% rename from src/trackmetadata.h rename to src/metadata/trackmetadata.h diff --git a/src/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp similarity index 99% rename from src/trackmetadatataglib.cpp rename to src/metadata/trackmetadatataglib.cpp index c2d81d38d38..168dc317ae0 100644 --- a/src/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -1,4 +1,4 @@ -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" #include #include diff --git a/src/trackmetadatataglib.h b/src/metadata/trackmetadatataglib.h similarity index 98% rename from src/trackmetadatataglib.h rename to src/metadata/trackmetadatataglib.h index 0577e5c6893..33e09697a0b 100644 --- a/src/trackmetadatataglib.h +++ b/src/metadata/trackmetadatataglib.h @@ -10,7 +10,7 @@ #ifndef SOUNDSOURCETAGLIB_H #define SOUNDSOURCETAGLIB_H -#include "trackmetadata.h" +#include "metadata/trackmetadata.h" #include #include diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index 3eddd2bce6b..81c97e3c098 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -16,7 +16,7 @@ ***************************************************************************/ #include "sources/soundsource.h" -#include "trackmetadata.h" +#include "metadata/trackmetadata.h" namespace Mixxx { diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index c3e26a0e170..e9f2f6e934e 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -16,7 +16,7 @@ #include "sources/soundsourcecoreaudio.h" #include "sources/audiosourcecoreaudio.h" -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" #include #include diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index 1f3baec2b5e..c4486470b7a 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -24,7 +24,7 @@ #include "sources/soundsourceffmpeg.h" #include "sources/audiosourceffmpeg.h" -#include "trackmetadata.h" +#include "metadata/trackmetadata.h" #include diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index ce5d253eddb..cd163800e82 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -15,7 +15,7 @@ #include "sources/soundsourceflac.h" -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" #include "sources/audiosourceflac.h" #include diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index ddb59f8ec03..fafcb2fa20e 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -1,7 +1,7 @@ #include "sources/soundsourcemodplug.h" #include "sources/audiosourcemodplug.h" -#include "trackmetadata.h" +#include "metadata/trackmetadata.h" #include "util/timer.h" #include diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 478cb74d610..926c161d075 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -17,7 +17,7 @@ #include "sources/soundsourcemp3.h" #include "sources/audiosourcemp3.h" -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" #include diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index a6e92a557eb..322e79fffc2 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -17,7 +17,7 @@ #include "sources/soundsourceoggvorbis.h" #include "sources/audiosourceoggvorbis.h" -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" #include diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index ebdac389486..b0fab591cb5 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -1,7 +1,7 @@ #include "sources/soundsourceopus.h" #include "sources/audiosourceopus.h" -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" // Include this if taglib if new enough (version 1.9.1 have opusfile) #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 46c3892ff22..94c63fad90e 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -1,7 +1,7 @@ #include "sources/soundsourcesndfile.h" #include "sources/audiosourcesndfile.h" -#include "trackmetadatataglib.h" +#include "metadata/trackmetadatataglib.h" #include #include diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 841aceeb462..b4728db3cfd 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -6,7 +6,7 @@ #include "test/mixxxtest.h" #include "soundsourceproxy.h" -#include "trackmetadata.h" +#include "metadata/trackmetadata.h" class SoundSourceProxyTest : public MixxxTest { protected: diff --git a/src/trackinfoobject.cpp b/src/trackinfoobject.cpp index 29368756899..81ce21470d2 100644 --- a/src/trackinfoobject.cpp +++ b/src/trackinfoobject.cpp @@ -30,7 +30,7 @@ #include "controlobject.h" #include "soundsourceproxy.h" -#include "trackmetadata.h" +#include "metadata/trackmetadata.h" #include "xmlparse.h" #include "track/beatfactory.h" #include "track/keyfactory.h" From 925a8b6ca41b08afc4aaf5df94db88d3a84a5fe8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 18 Dec 2014 17:22:08 +0100 Subject: [PATCH 029/481] Remove some TagLib dependencies from AudioTagger --- src/audiotagger.cpp | 211 +------------ src/audiotagger.h | 43 +-- src/library/browse/browsetablemodel.cpp | 57 ++-- src/library/dao/trackdao.cpp | 33 +- src/metadata/trackmetadata.h | 2 +- src/metadata/trackmetadatataglib.cpp | 381 +++++++++++++++++------- src/metadata/trackmetadatataglib.h | 30 +- src/trackinfoobject.cpp | 2 +- 8 files changed, 354 insertions(+), 405 deletions(-) diff --git a/src/audiotagger.cpp b/src/audiotagger.cpp index b5bd5fcf914..f0f9ccc1748 100644 --- a/src/audiotagger.cpp +++ b/src/audiotagger.cpp @@ -1,14 +1,6 @@ #include "audiotagger.h" -#include -#include -#include - -#include -#include -#include -#include -#include +#include "metadata/trackmetadatataglib.h" #include #include @@ -19,7 +11,8 @@ #include #include #include -#include + +#include AudioTagger::AudioTagger(const QString& file, SecurityTokenPointer pToken) : m_file(file), @@ -30,55 +23,7 @@ AudioTagger::AudioTagger(const QString& file, SecurityTokenPointer pToken) AudioTagger::~AudioTagger() { } -void AudioTagger::setArtist(QString artist) { - m_artist = artist; -} - -void AudioTagger::setTitle(QString title) { - m_title = title; -} - -void AudioTagger::setAlbum(QString album) { - m_album = album; -} - -void AudioTagger::setAlbumArtist (QString albumArtist) { - m_albumArtist = albumArtist; -} - -void AudioTagger::setGenre (QString genre) { - m_genre = genre; -} - -void AudioTagger::setComposer(QString composer) { - m_composer = composer; -} - -void AudioTagger::setGrouping (QString grouping) { - m_grouping = grouping; -} - -void AudioTagger::setYear (QString year) { - m_year = year; -} - -void AudioTagger::setComment(QString comment) { - m_comment = comment; -} - -void AudioTagger::setKey(QString key) { - m_key = key; -} - -void AudioTagger::setBpm(QString bpm) { - m_bpm = bpm; -} - -void AudioTagger::setTracknumber(QString tracknumber) { - m_tracknumber = tracknumber; -} - -bool AudioTagger::save() { +bool AudioTagger::save(const Mixxx::TrackMetadata& trackMetadata) { TagLib::File* file = NULL; const QString& filePath = m_file.canonicalFilePath(); @@ -89,43 +34,40 @@ bool AudioTagger::save() { // process special ID3 fields, APEv2 fiels, etc // If the mp3 has no ID3v2 tag, we create a new one and add the TBPM and TKEY frame - addID3v2Tag(((TagLib::MPEG::File*) file)->ID3v2Tag(true)); + writeID3v2Tag(((TagLib::MPEG::File*) file)->ID3v2Tag(true), trackMetadata); // If the mp3 has an APE tag, we update - addAPETag(((TagLib::MPEG::File*) file)->APETag(false)); + writeAPETag(((TagLib::MPEG::File*) file)->APETag(false), trackMetadata); } if (filePath.endsWith(".m4a", Qt::CaseInsensitive)) { file = new TagLib::MP4::File(fileBA.constData()); // process special ID3 fields, APEv2 fiels, etc - processMP4Tag(((TagLib::MP4::File*) file)->tag()); - + writeMP4Tag(((TagLib::MP4::File*) file)->tag(), trackMetadata); } if (filePath.endsWith(".ogg", Qt::CaseInsensitive)) { file = new TagLib::Ogg::Vorbis::File(fileBA.constData()); // process special ID3 fields, APEv2 fiels, etc - addXiphComment(((TagLib::Ogg::Vorbis::File*)file)->tag()); - + writeXiphComment(((TagLib::Ogg::Vorbis::File*)file)->tag(), trackMetadata); } if (filePath.endsWith(".wav", Qt::CaseInsensitive)) { file = new TagLib::RIFF::WAV::File(fileBA.constData()); //If the flac has no ID3v2 tag, we create a new one and add the TBPM and TKEY frame - addID3v2Tag(((TagLib::RIFF::WAV::File*)file)->tag()); - + writeID3v2Tag(((TagLib::RIFF::WAV::File*)file)->tag(), trackMetadata); } if (filePath.endsWith(".flac", Qt::CaseInsensitive)) { file = new TagLib::FLAC::File(fileBA.constData()); //If the flac has no ID3v2 tag, we create a new one and add the TBPM and TKEY frame - addID3v2Tag(((TagLib::FLAC::File*)file)->ID3v2Tag(true)); + writeID3v2Tag(((TagLib::FLAC::File*)file)->ID3v2Tag(true), trackMetadata); //If the flac has no APE tag, we create a new one and add the TBPM and TKEY frame - addXiphComment(((TagLib::FLAC::File*) file)->xiphComment(true)); + writeXiphComment(((TagLib::FLAC::File*) file)->xiphComment(true), trackMetadata); } if (filePath.endsWith(".aif", Qt::CaseInsensitive) || filePath.endsWith(".aiff", Qt::CaseInsensitive)) { file = new TagLib::RIFF::AIFF::File(fileBA.constData()); //If the flac has no ID3v2 tag, we create a new one and add the TBPM and TKEY frame - addID3v2Tag(((TagLib::RIFF::AIFF::File*)file)->tag()); + writeID3v2Tag(((TagLib::RIFF::AIFF::File*)file)->tag(), trackMetadata); } @@ -133,18 +75,7 @@ bool AudioTagger::save() { if (file) { TagLib::Tag *tag = file->tag(); if (tag) { - tag->setArtist(m_artist.toStdString()); - tag->setTitle(m_title.toStdString()); - tag->setAlbum(m_album.toStdString()); - tag->setGenre(m_genre.toStdString()); - tag->setComment(m_comment.toStdString()); - uint year = m_year.toUInt(); - if (year > 0) - tag->setYear(year); - uint tracknumber = m_tracknumber.toUInt(); - if (tracknumber > 0) - tag->setTrack(tracknumber); - + writeTag(tag, trackMetadata); } //write audio tags to file int success = file->save(); @@ -160,119 +91,3 @@ bool AudioTagger::save() { return false; } } - -void AudioTagger::addID3v2Tag(TagLib::ID3v2::Tag* id3v2) { - if (!id3v2) - return; - - TagLib::ID3v2::FrameList albumArtistFrame = id3v2->frameListMap()["TPE2"]; - if (!albumArtistFrame.isEmpty()) { - albumArtistFrame.front()->setText(m_albumArtist.toStdString()); - } else { - //add new frame - TagLib::ID3v2::TextIdentificationFrame* newFrame = - new TagLib::ID3v2::TextIdentificationFrame( - "TPE2", TagLib::String::Latin1); - newFrame->setText(m_albumArtist.toStdString()); - id3v2->addFrame(newFrame); - } - - TagLib::ID3v2::FrameList bpmFrame = id3v2->frameListMap()["TBPM"]; - if (!bpmFrame.isEmpty()) { - bpmFrame.front()->setText(m_bpm.toStdString()); - } else { - // add new frame TextIdentificationFrame which is responsible for TKEY and TBPM - // see http://developer.kde.org/~wheeler/taglib/api/classTagLib_1_1ID3v2_1_1TextIdentificationFrame.html - - TagLib::ID3v2::TextIdentificationFrame* newFrame = new TagLib::ID3v2::TextIdentificationFrame("TBPM", TagLib::String::Latin1); - - newFrame->setText(m_bpm.toStdString()); - id3v2->addFrame(newFrame); - - } - - TagLib::ID3v2::FrameList keyFrame = id3v2->frameListMap()["TKEY"]; - if (!keyFrame.isEmpty()) { - keyFrame.front()->setText(m_key.toStdString()); - } else { - //add new frame - TagLib::ID3v2::TextIdentificationFrame* newFrame = new TagLib::ID3v2::TextIdentificationFrame("TKEY", TagLib::String::Latin1); - - newFrame->setText(m_key.toStdString()); - id3v2->addFrame(newFrame); - - } - - TagLib::ID3v2::FrameList composerFrame = id3v2->frameListMap()["TCOM"]; - if (!composerFrame.isEmpty()) { - composerFrame.front()->setText(m_composer.toStdString()); - } else { - //add new frame - TagLib::ID3v2::TextIdentificationFrame* newFrame = - new TagLib::ID3v2::TextIdentificationFrame( - "TCOM", TagLib::String::Latin1); - newFrame->setText(m_composer.toStdString()); - id3v2->addFrame(newFrame); - } - - TagLib::ID3v2::FrameList groupingFrame = id3v2->frameListMap()["TIT1"]; - if (!groupingFrame.isEmpty()) { - groupingFrame.front()->setText(m_grouping.toStdString()); - } else { - //add new frame - TagLib::ID3v2::TextIdentificationFrame* newFrame = - new TagLib::ID3v2::TextIdentificationFrame( - "TIT1", TagLib::String::Latin1); - newFrame->setText(m_grouping.toStdString()); - id3v2->addFrame(newFrame); - } -} - -void AudioTagger::addAPETag(TagLib::APE::Tag* ape) { - if (!ape) - return; - // Adds to the item specified by key the data value. - // If replace is true, then all of the other values on the same key will be removed first. - ape->addValue("BPM",m_bpm.toStdString(), true); - ape->addValue("Album Artist",m_albumArtist.toStdString(), true); - ape->addValue("Composer",m_composer.toStdString(), true); - ape->addValue("Grouping",m_grouping.toStdString(), true); - -} - -void AudioTagger::addXiphComment(TagLib::Ogg::XiphComment* xiph) { - if (!xiph) - return; - - // Taglib does not support the update of Vorbis comments. - // thus, we have to reomve the old comment and add the new one - - xiph->removeField("ALBUMARTIST"); - xiph->addField("ALBUMARTIST", m_albumArtist.toStdString()); - - // Some tools use "BPM" so write that. - xiph->removeField("BPM"); - xiph->addField("BPM", m_bpm.toStdString()); - xiph->removeField("TEMPO"); - xiph->addField("TEMPO", m_bpm.toStdString()); - - xiph->removeField("INITIALKEY"); - xiph->addField("INITIALKEY", m_key.toStdString()); - xiph->removeField("KEY"); - xiph->addField("KEY", m_key.toStdString()); - - xiph->removeField("COMPOSER"); - xiph->addField("COMPOSER", m_composer.toStdString()); - - xiph->removeField("GROUPING"); - xiph->addField("GROUPING", m_grouping.toStdString()); -} - -void AudioTagger::processMP4Tag(TagLib::MP4::Tag* mp4) { - mp4->itemListMap()["aART"] = TagLib::StringList(m_albumArtist.toStdString()); - mp4->itemListMap()["tmpo"] = TagLib::StringList(m_bpm.toStdString()); - mp4->itemListMap()["----:com.apple.iTunes:BPM"] = TagLib::StringList(m_bpm.toStdString()); - mp4->itemListMap()["----:com.apple.iTunes:KEY"] = TagLib::StringList(m_key.toStdString()); - mp4->itemListMap()["\251wrt"] = TagLib::StringList(m_composer.toStdString()); - mp4->itemListMap()["\251grp"] = TagLib::StringList(m_grouping.toStdString()); -} diff --git a/src/audiotagger.h b/src/audiotagger.h index 4133bfb4aa8..215b2f0ffb4 100644 --- a/src/audiotagger.h +++ b/src/audiotagger.h @@ -2,56 +2,21 @@ #ifndef AUDIOTAGGER_H #define AUDIOTAGGER_H -#include -#include -#include -#include -#include -#include - +#include "metadata/trackmetadata.h" #include "util/sandbox.h" +#include + class AudioTagger { public: AudioTagger(const QString& file, SecurityTokenPointer pToken); virtual ~AudioTagger(); - void setArtist(QString artist); - void setTitle(QString title); - void setAlbum(QString album); - void setAlbumArtist(QString albumArtist); - void setGenre(QString genre); - void setComposer(QString composer); - void setGrouping(QString grouping); - void setYear(QString year); - void setComment(QString comment); - void setKey(QString key); - void setBpm(QString bpm); - void setTracknumber(QString tracknumber); - bool save(); + bool save(const Mixxx::TrackMetadata& trackMetadata); private: - QString m_artist; - QString m_title; - QString m_albumArtist; - QString m_genre; - QString m_composer; - QString m_album; - QString m_grouping; - QString m_year; - QString m_comment; - QString m_key; - QString m_bpm; - QString m_tracknumber; - QFileInfo m_file; SecurityTokenPointer m_pSecurityToken; - - /** adds or modifies the ID3v2 tag to include BPM and KEY information **/ - void addID3v2Tag(TagLib::ID3v2::Tag* id3v2); - void addAPETag(TagLib::APE::Tag* ape); - void addXiphComment(TagLib::Ogg::XiphComment* xiph); - void processMP4Tag(TagLib::MP4::Tag* mp4); }; #endif // AUDIOTAGGER_H diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp index 6d02481364e..adb8182a0e7 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/browse/browsetablemodel.cpp @@ -316,53 +316,54 @@ bool BrowseTableModel::setData(const QModelIndex &index, const QVariant &value, qDebug() << "BrowseTableModel::setData(" << index.data() << ")"; int row = index.row(); int col = index.column(); - QString track_location = getTrackLocation(index); - AudioTagger tagger(track_location, m_current_directory.token()); + + Mixxx::TrackMetadata trackMetadata; // set tagger information - tagger.setArtist(this->index(row, COLUMN_ARTIST).data().toString()); - tagger.setTitle(this->index(row, COLUMN_TITLE).data().toString()); - tagger.setAlbum(this->index(row, COLUMN_ALBUM).data().toString()); - tagger.setKey(this->index(row, COLUMN_KEY).data().toString()); - tagger.setBpm(this->index(row, COLUMN_BPM).data().toString()); - tagger.setComment(this->index(row, COLUMN_COMMENT).data().toString()); - tagger.setTracknumber( - this->index(row, COLUMN_TRACK_NUMBER).data().toString()); - tagger.setYear(this->index(row, COLUMN_YEAR).data().toString()); - tagger.setGenre(this->index(row, COLUMN_GENRE).data().toString()); - tagger.setComposer(this->index(row, COLUMN_COMPOSER).data().toString()); - tagger.setAlbumArtist(this->index(row, COLUMN_ALBUMARTIST).data().toString()); - tagger.setGrouping(this->index(row, COLUMN_GROUPING).data().toString()); + trackMetadata.setArtist(this->index(row, COLUMN_ARTIST).data().toString()); + trackMetadata.setTitle(this->index(row, COLUMN_TITLE).data().toString()); + trackMetadata.setAlbum(this->index(row, COLUMN_ALBUM).data().toString()); + trackMetadata.setKey(this->index(row, COLUMN_KEY).data().toString()); + trackMetadata.setBpmString(this->index(row, COLUMN_BPM).data().toString()); + trackMetadata.setComment(this->index(row, COLUMN_COMMENT).data().toString()); + trackMetadata.setTrackNumber(this->index(row, COLUMN_TRACK_NUMBER).data().toString()); + trackMetadata.setYear(this->index(row, COLUMN_YEAR).data().toString()); + trackMetadata.setGenre(this->index(row, COLUMN_GENRE).data().toString()); + trackMetadata.setComposer(this->index(row, COLUMN_COMPOSER).data().toString()); + trackMetadata.setAlbumArtist(this->index(row, COLUMN_ALBUMARTIST).data().toString()); + trackMetadata.setGrouping(this->index(row, COLUMN_GROUPING).data().toString()); // check if one the item were edited if (col == COLUMN_ARTIST) { - tagger.setArtist(value.toString()); + trackMetadata.setArtist(value.toString()); } else if (col == COLUMN_TITLE) { - tagger.setTitle(value.toString()); + trackMetadata.setTitle(value.toString()); } else if (col == COLUMN_ALBUM) { - tagger.setAlbum(value.toString()); + trackMetadata.setAlbum(value.toString()); } else if (col == COLUMN_BPM) { - tagger.setBpm(value.toString()); + trackMetadata.setBpmString(value.toString()); } else if (col == COLUMN_KEY) { - tagger.setKey(value.toString()); + trackMetadata.setKey(value.toString()); } else if (col == COLUMN_TRACK_NUMBER) { - tagger.setTracknumber(value.toString()); + trackMetadata.setTrackNumber(value.toString()); } else if (col == COLUMN_COMMENT) { - tagger.setComment(value.toString()); + trackMetadata.setComment(value.toString()); } else if (col == COLUMN_GENRE) { - tagger.setGenre(value.toString()); + trackMetadata.setGenre(value.toString()); } else if (col == COLUMN_COMPOSER) { - tagger.setComposer(value.toString()); + trackMetadata.setComposer(value.toString()); } else if (col == COLUMN_YEAR) { - tagger.setYear(value.toString()); + trackMetadata.setYear(value.toString()); } else if (col == COLUMN_ALBUMARTIST) { - tagger.setAlbumArtist(value.toString()); + trackMetadata.setAlbumArtist(value.toString()); } else if (col == COLUMN_GROUPING) { - tagger.setGrouping(value.toString()); + trackMetadata.setGrouping(value.toString()); } QStandardItem* item = itemFromIndex(index); - if (tagger.save()) { + QString track_location = getTrackLocation(index); + AudioTagger tagger(track_location, m_current_directory.token()); + if (tagger.save(trackMetadata)) { // Modify underlying interalPointer object item->setText(value.toString()); item->setToolTip(item->text()); diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index 5e0bca8ab19..dbc619d84d9 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -1787,23 +1787,24 @@ void TrackDAO::markTracksAsMixxxDeleted(const QString& dir) { void TrackDAO::writeAudioMetaData(TrackInfoObject* pTrack) { if (m_pConfig && m_pConfig->getValueString(ConfigKey("[Library]","WriteAudioTags")).toInt() == 1) { - AudioTagger tagger(pTrack->getLocation(), pTrack->getSecurityToken()); - tagger.setArtist(pTrack->getArtist()); - tagger.setTitle(pTrack->getTitle()); - tagger.setGenre(pTrack->getGenre()); - tagger.setComposer(pTrack->getComposer()); - tagger.setGrouping(pTrack->getGrouping()); - tagger.setAlbum(pTrack->getAlbum()); - tagger.setAlbumArtist(pTrack->getAlbumArtist()); - tagger.setComment(pTrack->getComment()); - tagger.setTracknumber(pTrack->getTrackNumber()); - tagger.setBpm(pTrack->getBpmStr()); - tagger.setKey(pTrack->getKeyText()); - tagger.setComposer(pTrack->getComposer()); - tagger.setGrouping(pTrack->getGrouping()); - - tagger.save(); + Mixxx::TrackMetadata trackMetadata; + trackMetadata.setArtist(pTrack->getArtist()); + trackMetadata.setTitle(pTrack->getTitle()); + trackMetadata.setGenre(pTrack->getGenre()); + trackMetadata.setComposer(pTrack->getComposer()); + trackMetadata.setGrouping(pTrack->getGrouping()); + trackMetadata.setAlbum(pTrack->getAlbum()); + trackMetadata.setAlbumArtist(pTrack->getAlbumArtist()); + trackMetadata.setComment(pTrack->getComment()); + trackMetadata.setTrackNumber(pTrack->getTrackNumber()); + trackMetadata.setBpm(pTrack->getBpm()); + trackMetadata.setKey(pTrack->getKeyText()); + trackMetadata.setComposer(pTrack->getComposer()); + trackMetadata.setGrouping(pTrack->getGrouping()); + + AudioTagger tagger(pTrack->getLocation(), pTrack->getSecurityToken()); + tagger.save(trackMetadata); } } diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 10fb0794371..e32a2ad104d 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -121,7 +121,7 @@ class TrackMetadata { } // beats / minute - inline float getBPM() const { + inline float getBpm() const { return m_bpm; } inline void setBpm(float bpm) { diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 168dc317ae0..d0cc707be89 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -1,5 +1,6 @@ #include "metadata/trackmetadatataglib.h" +#include #include #include #include @@ -21,39 +22,44 @@ namespace Mixxx // static namespace { - const bool kDebugMetadata = false; - - // Taglib strings can be NULL and using it could cause some segfaults, - // so in this case it will return a QString() - inline - QString toQString(const TagLib::String& tString) { - if (tString.isNull()) { - return QString(); - } else { - return TStringToQString(tString); - } +const bool kDebugMetadata = false; + +// Taglib strings can be NULL and using it could cause some segfaults, +// so in this case it will return a QString() +inline +QString toQString(const TagLib::String& tString) { + if (tString.isNull()) { + return QString(); + } else { + return TStringToQString(tString); } +} - // Concatenates the elements of a TagLib string list - // into a single string. - inline - QString toQString(const TagLib::StringList& strList) { - return toQString(strList.toString()); - } +// Concatenates the elements of a TagLib string list +// into a single string. +inline +QString toQString(const TagLib::StringList& strList) { + return toQString(strList.toString()); +} - // Concatenates the string list of an MP4 atom - // into a single string - inline - QString toQString(const TagLib::MP4::Item& mp4Item) { - return toQString(mp4Item.toStringList()); - } +// Concatenates the string list of an MP4 atom +// into a single string +inline +QString toQString(const TagLib::MP4::Item& mp4Item) { + return toQString(mp4Item.toStringList()); +} - inline - QString toQString(const TagLib::APE::Item& apeItem) { - return toQString(apeItem.toString()); - } +inline +QString toQString(const TagLib::APE::Item& apeItem) { + return toQString(apeItem.toString()); } +template +inline +QString formatString(const T& value) { + return QString("%1").arg(value); +} +} bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& f) { if (kDebugMetadata) { @@ -111,26 +117,26 @@ void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { } } -void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& id3v2) { +void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) { // Print every frame in the file. if (kDebugMetadata) { - TagLib::ID3v2::FrameList::ConstIterator it = id3v2.frameList().begin(); - for(; it != id3v2.frameList().end(); it++) { + TagLib::ID3v2::FrameList::ConstIterator it = tag.frameList().begin(); + for(; it != tag.frameList().end(); it++) { qDebug() << "ID3V2" << (*it)->frameID().data() << "-" << toQString((*it)->toString()); } } - readTag(pTrackMetadata, id3v2); + readTag(pTrackMetadata, tag); - TagLib::ID3v2::FrameList bpmFrame = id3v2.frameListMap()["TBPM"]; + TagLib::ID3v2::FrameList bpmFrame = tag.frameListMap()["TBPM"]; if (!bpmFrame.isEmpty()) { QString sBpm = toQString(bpmFrame.front()->toString()); pTrackMetadata->setBpmString(sBpm); } - TagLib::ID3v2::FrameList keyFrame = id3v2.frameListMap()["TKEY"]; + TagLib::ID3v2::FrameList keyFrame = tag.frameListMap()["TKEY"]; if (!keyFrame.isEmpty()) { QString sKey = toQString(keyFrame.front()->toString()); pTrackMetadata->setKey(sKey); @@ -138,7 +144,7 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& id3v2 // Foobar2000-style ID3v2.3.0 tags // TODO: Check if everything is ok. - TagLib::ID3v2::FrameList frames = id3v2.frameListMap()["TXXX"]; + TagLib::ID3v2::FrameList frames = tag.frameListMap()["TXXX"]; for (TagLib::ID3v2::FrameList::Iterator it = frames.begin(); it != frames.end(); ++it) { TagLib::ID3v2::UserTextIdentificationFrame* ReplayGainframe = dynamic_cast(*it); @@ -156,7 +162,7 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& id3v2 } } - TagLib::ID3v2::FrameList albumArtistFrame = id3v2.frameListMap()["TPE2"]; + TagLib::ID3v2::FrameList albumArtistFrame = tag.frameListMap()["TPE2"]; if (!albumArtistFrame.isEmpty()) { QString sAlbumArtist = toQString(albumArtistFrame.front()->toString()); pTrackMetadata->setAlbumArtist(sAlbumArtist); @@ -165,86 +171,86 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& id3v2 pTrackMetadata->setArtist(sAlbumArtist); } } - TagLib::ID3v2::FrameList originalAlbumFrame = id3v2.frameListMap()["TOAL"]; + TagLib::ID3v2::FrameList originalAlbumFrame = tag.frameListMap()["TOAL"]; if (pTrackMetadata->getAlbum().length() == 0 && !originalAlbumFrame.isEmpty()) { QString sOriginalAlbum = TStringToQString(originalAlbumFrame.front()->toString()); pTrackMetadata->setAlbum(sOriginalAlbum); } - TagLib::ID3v2::FrameList composerFrame = id3v2.frameListMap()["TCOM"]; + TagLib::ID3v2::FrameList composerFrame = tag.frameListMap()["TCOM"]; if (!composerFrame.isEmpty()) { QString sComposer = toQString(composerFrame.front()->toString()); pTrackMetadata->setComposer(sComposer); } - TagLib::ID3v2::FrameList groupingFrame = id3v2.frameListMap()["TIT1"]; + TagLib::ID3v2::FrameList groupingFrame = tag.frameListMap()["TIT1"]; if (!groupingFrame.isEmpty()) { QString sGrouping = toQString(groupingFrame.front()->toString()); pTrackMetadata->setGrouping(sGrouping); } } -void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& ape) { +void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { if (kDebugMetadata) { - for(TagLib::APE::ItemListMap::ConstIterator it = ape.itemListMap().begin(); - it != ape.itemListMap().end(); ++it) { + for(TagLib::APE::ItemListMap::ConstIterator it = tag.itemListMap().begin(); + it != tag.itemListMap().end(); ++it) { qDebug() << "APE" << toQString((*it).first) << "-" << toQString((*it).second.toString()); } } - readTag(pTrackMetadata, ape); + readTag(pTrackMetadata, tag); - if (ape.itemListMap().contains("BPM")) { - pTrackMetadata->setBpmString(toQString(ape.itemListMap()["BPM"])); + if (tag.itemListMap().contains("BPM")) { + pTrackMetadata->setBpmString(toQString(tag.itemListMap()["BPM"])); } - if (ape.itemListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pTrackMetadata->setReplayGainString(toQString(ape.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); + if (tag.itemListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { + pTrackMetadata->setReplayGainString(toQString(tag.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. - if (ape.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainString(toQString(ape.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); + if (tag.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { + pTrackMetadata->setReplayGainString(toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); } - if (ape.itemListMap().contains("Album Artist")) { - pTrackMetadata->setAlbumArtist(toQString(ape.itemListMap()["Album Artist"])); + if (tag.itemListMap().contains("Album Artist")) { + pTrackMetadata->setAlbumArtist(toQString(tag.itemListMap()["Album Artist"])); } - if (ape.itemListMap().contains("Composer")) { - pTrackMetadata->setComposer(toQString(ape.itemListMap()["Composer"])); + if (tag.itemListMap().contains("Composer")) { + pTrackMetadata->setComposer(toQString(tag.itemListMap()["Composer"])); } - if (ape.itemListMap().contains("Grouping")) { - pTrackMetadata->setGrouping(toQString(ape.itemListMap()["Grouping"])); + if (tag.itemListMap().contains("Grouping")) { + pTrackMetadata->setGrouping(toQString(tag.itemListMap()["Grouping"])); } } -void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& xiph) { +void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag) { if (kDebugMetadata) { - for (TagLib::Ogg::FieldListMap::ConstIterator it = xiph.fieldListMap().begin(); - it != xiph.fieldListMap().end(); ++it) { + for (TagLib::Ogg::FieldListMap::ConstIterator it = tag.fieldListMap().begin(); + it != tag.fieldListMap().end(); ++it) { qDebug() << "XIPH" << toQString((*it).first) << "-" << toQString((*it).second.toString()); } } - readTag(pTrackMetadata, xiph); + readTag(pTrackMetadata, tag); // Some tags use "BPM" so check for that. - if (xiph.fieldListMap().contains("BPM")) { - pTrackMetadata->setBpmString(toQString(xiph.fieldListMap()["BPM"])); + if (tag.fieldListMap().contains("BPM")) { + pTrackMetadata->setBpmString(toQString(tag.fieldListMap()["BPM"])); } // Give preference to the "TEMPO" tag which seems to be more standard - if (xiph.fieldListMap().contains("TEMPO")) { - pTrackMetadata->setBpmString(toQString(xiph.fieldListMap()["TEMPO"])); + if (tag.fieldListMap().contains("TEMPO")) { + pTrackMetadata->setBpmString(toQString(tag.fieldListMap()["TEMPO"])); } - if (xiph.fieldListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pTrackMetadata->setReplayGainString(toQString(xiph.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); + if (tag.fieldListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { + pTrackMetadata->setReplayGainString(toQString(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. - if (xiph.fieldListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainString(toQString(xiph.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); + if (tag.fieldListMap().contains("REPLAYGAIN_TRACK_GAIN")) { + pTrackMetadata->setReplayGainString(toQString(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); } /* @@ -255,89 +261,89 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme * Assuming no distinction between start and end key, RE uses a "INITIALKEY" * or a "KEY" vorbis comment. */ - if (xiph.fieldListMap().contains("KEY")) { - pTrackMetadata->setKey(toQString(xiph.fieldListMap()["KEY"])); + if (tag.fieldListMap().contains("KEY")) { + pTrackMetadata->setKey(toQString(tag.fieldListMap()["KEY"])); } - if (pTrackMetadata->getKey().isEmpty() && xiph.fieldListMap().contains("INITIALKEY")) { - pTrackMetadata->setKey(toQString(xiph.fieldListMap()["INITIALKEY"])); + if (pTrackMetadata->getKey().isEmpty() && tag.fieldListMap().contains("INITIALKEY")) { + pTrackMetadata->setKey(toQString(tag.fieldListMap()["INITIALKEY"])); } - if (xiph.fieldListMap().contains("ALBUMARTIST")) { - pTrackMetadata->setAlbumArtist(toQString(xiph.fieldListMap()["ALBUMARTIST"])); + if (tag.fieldListMap().contains("ALBUMARTIST")) { + pTrackMetadata->setAlbumArtist(toQString(tag.fieldListMap()["ALBUMARTIST"])); } else { // try alternative field name - if (xiph.fieldListMap().contains("ALBUM_ARTIST")) { - pTrackMetadata->setAlbumArtist(toQString(xiph.fieldListMap()["ALBUM_ARTIST"])); + if (tag.fieldListMap().contains("ALBUM_ARTIST")) { + pTrackMetadata->setAlbumArtist(toQString(tag.fieldListMap()["ALBUM_ARTIST"])); } } - if (xiph.fieldListMap().contains("COMPOSER")) { - pTrackMetadata->setComposer(toQString(xiph.fieldListMap()["COMPOSER"])); + if (tag.fieldListMap().contains("COMPOSER")) { + pTrackMetadata->setComposer(toQString(tag.fieldListMap()["COMPOSER"])); } - if (xiph.fieldListMap().contains("GROUPING")) { - pTrackMetadata->setGrouping(toQString(xiph.fieldListMap()["GROUPING"])); + if (tag.fieldListMap().contains("GROUPING")) { + pTrackMetadata->setGrouping(toQString(tag.fieldListMap()["GROUPING"])); } } -void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& mp4) { +void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) { if (kDebugMetadata) { - for(TagLib::MP4::ItemListMap::ConstIterator it = mp4.itemListMap().begin(); - it != mp4.itemListMap().end(); ++it) { + for(TagLib::MP4::ItemListMap::ConstIterator it = tag.itemListMap().begin(); + it != tag.itemListMap().end(); ++it) { qDebug() << "MP4" << toQString((*it).first) << "-" << toQString((*it).second); } } - readTag(pTrackMetadata, mp4); + readTag(pTrackMetadata, tag); // Get BPM - if (mp4.itemListMap().contains("tmpo")) { - pTrackMetadata->setBpmString(toQString(mp4.itemListMap()["tmpo"])); - } else if (mp4.itemListMap().contains("----:com.apple.iTunes:BPM")) { + if (tag.itemListMap().contains("tmpo")) { + pTrackMetadata->setBpmString(toQString(tag.itemListMap()["tmpo"])); + } else if (tag.itemListMap().contains("----:com.apple.iTunes:BPM")) { // This is an alternate way of storing BPM. - pTrackMetadata->setBpmString(toQString(mp4.itemListMap()["----:com.apple.iTunes:BPM"])); + pTrackMetadata->setBpmString(toQString(tag.itemListMap()["----:com.apple.iTunes:BPM"])); } // Get Album Artist - if (mp4.itemListMap().contains("aART")) { - pTrackMetadata->setAlbumArtist(toQString(mp4.itemListMap()["aART"])); + if (tag.itemListMap().contains("aART")) { + pTrackMetadata->setAlbumArtist(toQString(tag.itemListMap()["aART"])); } // Get Composer - if (mp4.itemListMap().contains("\251wrt")) { - pTrackMetadata->setComposer(toQString(mp4.itemListMap()["\251wrt"])); + if (tag.itemListMap().contains("\251wrt")) { + pTrackMetadata->setComposer(toQString(tag.itemListMap()["\251wrt"])); } // Get Grouping - if (mp4.itemListMap().contains("\251grp")) { - pTrackMetadata->setGrouping(toQString(mp4.itemListMap()["\251grp"])); + if (tag.itemListMap().contains("\251grp")) { + pTrackMetadata->setGrouping(toQString(tag.itemListMap()["\251grp"])); } // Get KEY (conforms to Rapid Evolution) - if (mp4.itemListMap().contains("----:com.apple.iTunes:KEY")) { + if (tag.itemListMap().contains("----:com.apple.iTunes:KEY")) { QString key = toQString( - mp4.itemListMap()["----:com.apple.iTunes:KEY"]); + tag.itemListMap()["----:com.apple.iTunes:KEY"]); pTrackMetadata->setKey(key); } // Apparently iTunes stores replaygain in this property. - if (mp4.itemListMap().contains("----:com.apple.iTunes:replaygain_album_gain")) { + if (tag.itemListMap().contains("----:com.apple.iTunes:replaygain_album_gain")) { // TODO(XXX) find tracks with this property and check what it looks // like. - //QString replaygain = toQString(mp4.itemListMap()["----:com.apple.iTunes:replaygain_album_gain"]); + //QString replaygain = toQString(tag.itemListMap()["----:com.apple.iTunes:replaygain_album_gain"]); } //Prefer track gain over album gain. - if (mp4.itemListMap().contains("----:com.apple.iTunes:replaygain_track_gain")) { + if (tag.itemListMap().contains("----:com.apple.iTunes:replaygain_track_gain")) { // TODO(XXX) find tracks with this property and check what it looks // like. - //QString replaygain = toQString(mp4.itemListMap()["----:com.apple.iTunes:replaygain_track_gain"]); + //QString replaygain = toQString(tag.itemListMap()["----:com.apple.iTunes:replaygain_track_gain"]); } } -QImage readID3v2TagCover(const TagLib::ID3v2::Tag& id3v2) { +QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag) { QImage coverArt; - TagLib::ID3v2::FrameList covertArtFrame = id3v2.frameListMap()["APIC"]; + TagLib::ID3v2::FrameList covertArtFrame = tag.frameListMap()["APIC"]; if (!covertArtFrame.isEmpty()) { TagLib::ID3v2::AttachedPictureFrame* picframe = static_cast (covertArtFrame.front()); @@ -348,12 +354,12 @@ QImage readID3v2TagCover(const TagLib::ID3v2::Tag& id3v2) { return coverArt; } -QImage readAPETagCover(const TagLib::APE::Tag& ape) { +QImage readAPETagCover(const TagLib::APE::Tag& tag) { QImage coverArt; - if (ape.itemListMap().contains("COVER ART (FRONT)")) + if (tag.itemListMap().contains("COVER ART (FRONT)")) { const TagLib::ByteVector nullStringTerminator(1, 0); - TagLib::ByteVector item = ape.itemListMap()["COVER ART (FRONT)"].value(); + TagLib::ByteVector item = tag.itemListMap()["COVER ART (FRONT)"].value(); int pos = item.find(nullStringTerminator); // skip the filename if (++pos > 0) { const TagLib::ByteVector& data = item.mid(pos); @@ -364,27 +370,27 @@ QImage readAPETagCover(const TagLib::APE::Tag& ape) { return coverArt; } -QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& xiph) { +QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag) { QImage coverArt; - if (xiph.fieldListMap().contains("METADATA_BLOCK_PICTURE")) { + if (tag.fieldListMap().contains("METADATA_BLOCK_PICTURE")) { QByteArray data(QByteArray::fromBase64( - xiph.fieldListMap()["METADATA_BLOCK_PICTURE"].front().toCString())); + tag.fieldListMap()["METADATA_BLOCK_PICTURE"].front().toCString())); TagLib::ByteVector tdata(data.data(), data.size()); TagLib::FLAC::Picture p(tdata); data = QByteArray(p.data().data(), p.data().size()); coverArt = QImage::fromData(data); - } else if (xiph.fieldListMap().contains("COVERART")) { + } else if (tag.fieldListMap().contains("COVERART")) { QByteArray data(QByteArray::fromBase64( - xiph.fieldListMap()["COVERART"].toString().toCString())); + tag.fieldListMap()["COVERART"].toString().toCString())); coverArt = QImage::fromData(data); } return coverArt; } -QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& mp4) { +QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& tag) { QImage coverArt; - if (mp4.itemListMap().contains("covr")) { - TagLib::MP4::CoverArtList coverArtList = mp4.itemListMap()["covr"] + if (tag.itemListMap().contains("covr")) { + TagLib::MP4::CoverArtList coverArtList = tag.itemListMap()["covr"] .toCoverArtList(); TagLib::ByteVector data = coverArtList.front().data(); coverArt = QImage::fromData( @@ -393,4 +399,157 @@ QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& mp4) { return coverArt; } +bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { + if (NULL == pTag) { + return false; + } + + pTag->setArtist(trackMetadata.getArtist().toStdString()); + pTag->setTitle(trackMetadata.getTitle().toStdString()); + pTag->setAlbum(trackMetadata.getAlbum().toStdString()); + pTag->setGenre(trackMetadata.getGenre().toStdString()); + pTag->setComment(trackMetadata.getComment().toStdString()); + uint year = trackMetadata.getYear().toUInt(); + if (year > 0) { + pTag->setYear(year); + } + uint track = trackMetadata.getTrackNumber().toUInt(); + if (track > 0) { + pTag->setTrack(track); + } + + return true; +} + +bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) { + if (NULL == pTag) { + return false; + } + + TagLib::ID3v2::FrameList albumArtistFrame = pTag->frameListMap()["TPE2"]; + if (!albumArtistFrame.isEmpty()) { + albumArtistFrame.front()->setText(trackMetadata.getAlbumArtist().toStdString()); + } else { + //add new frame + TagLib::ID3v2::TextIdentificationFrame* newFrame = + new TagLib::ID3v2::TextIdentificationFrame( + "TPE2", TagLib::String::Latin1); + newFrame->setText(trackMetadata.getAlbumArtist().toStdString()); + pTag->addFrame(newFrame); + } + + TagLib::ID3v2::FrameList bpmFrame = pTag->frameListMap()["TBPM"]; + if (!bpmFrame.isEmpty()) { + bpmFrame.front()->setText(formatString(trackMetadata.getBpm()).toStdString()); + } else { + // add new frame TextIdentificationFrame which is responsible for TKEY and TBPM + // see http://developer.kde.org/~wheeler/taglib/api/classTagLib_1_1ID3v2_1_1TextIdentificationFrame.html + + TagLib::ID3v2::TextIdentificationFrame* newFrame = new TagLib::ID3v2::TextIdentificationFrame("TBPM", TagLib::String::Latin1); + + newFrame->setText(formatString(trackMetadata.getBpm()).toStdString()); + pTag->addFrame(newFrame); + + } + + TagLib::ID3v2::FrameList keyFrame = pTag->frameListMap()["TKEY"]; + if (!keyFrame.isEmpty()) { + keyFrame.front()->setText(trackMetadata.getKey().toStdString()); + } else { + //add new frame + TagLib::ID3v2::TextIdentificationFrame* newFrame = new TagLib::ID3v2::TextIdentificationFrame("TKEY", TagLib::String::Latin1); + + newFrame->setText(trackMetadata.getKey().toStdString()); + pTag->addFrame(newFrame); + + } + + TagLib::ID3v2::FrameList composerFrame = pTag->frameListMap()["TCOM"]; + if (!composerFrame.isEmpty()) { + composerFrame.front()->setText(trackMetadata.getComposer().toStdString()); + } else { + //add new frame + TagLib::ID3v2::TextIdentificationFrame* newFrame = + new TagLib::ID3v2::TextIdentificationFrame( + "TCOM", TagLib::String::Latin1); + newFrame->setText(trackMetadata.getComposer().toStdString()); + pTag->addFrame(newFrame); + } + + TagLib::ID3v2::FrameList groupingFrame = pTag->frameListMap()["TIT1"]; + if (!groupingFrame.isEmpty()) { + groupingFrame.front()->setText(trackMetadata.getGrouping().toStdString()); + } else { + //add new frame + TagLib::ID3v2::TextIdentificationFrame* newFrame = + new TagLib::ID3v2::TextIdentificationFrame( + "TIT1", TagLib::String::Latin1); + newFrame->setText(trackMetadata.getGrouping().toStdString()); + pTag->addFrame(newFrame); + } + + return true; +} + +bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { + if (NULL == pTag) { + return false; + } + + // Adds to the item specified by key the data value. + // If replace is true, then all of the other values on the same key will be removed first. + pTag->addValue("BPM", formatString(trackMetadata.getBpm()).toStdString(), true); + pTag->addValue("Album Artist", trackMetadata.getAlbumArtist().toStdString(), true); + pTag->addValue("Composer", trackMetadata.getComposer().toStdString(), true); + pTag->addValue("Grouping", trackMetadata.getGrouping().toStdString(), true); + + return true; +} + +bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& trackMetadata) { + if (NULL == pTag) { + return false; + } + + // Taglib does not support the update of Vorbis comments. + // thus, we have to reomve the old comment and add the new one + + pTag->removeField("ALBUMARTIST"); + pTag->addField("ALBUMARTIST", trackMetadata.getAlbumArtist().toStdString()); + + // Some tools use "BPM" so write that. + pTag->removeField("BPM"); + pTag->addField("BPM",formatString(trackMetadata.getBpm()).toStdString()); + pTag->removeField("TEMPO"); + pTag->addField("TEMPO",formatString(trackMetadata.getBpm()).toStdString()); + + pTag->removeField("INITIALKEY"); + pTag->addField("INITIALKEY", trackMetadata.getKey().toStdString()); + pTag->removeField("KEY"); + pTag->addField("KEY", trackMetadata.getKey().toStdString()); + + pTag->removeField("COMPOSER"); + pTag->addField("COMPOSER", trackMetadata.getComposer().toStdString()); + + pTag->removeField("GROUPING"); + pTag->addField("GROUPING", trackMetadata.getGrouping().toStdString()); + + return true; +} + +bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { + if (NULL == pTag) { + return false; + } + + pTag->itemListMap()["aART"] = TagLib::StringList(trackMetadata.getAlbumArtist().toStdString()); + pTag->itemListMap()["tmpo"] = TagLib::StringList(formatString(trackMetadata.getBpm()).toStdString()); + pTag->itemListMap()["----:com.apple.iTunes:BPM"] = TagLib::StringList(formatString(trackMetadata.getBpm()).toStdString()); + pTag->itemListMap()["----:com.apple.iTunes:KEY"] = TagLib::StringList(trackMetadata.getKey().toStdString()); + pTag->itemListMap()["\251wrt"] = TagLib::StringList(trackMetadata.getComposer().toStdString()); + pTag->itemListMap()["\251grp"] = TagLib::StringList(trackMetadata.getGrouping().toStdString()); + + return true; +} + } //namespace Mixxx diff --git a/src/metadata/trackmetadatataglib.h b/src/metadata/trackmetadatataglib.h index 33e09697a0b..e4e7e0ecd49 100644 --- a/src/metadata/trackmetadatataglib.h +++ b/src/metadata/trackmetadatataglib.h @@ -12,7 +12,6 @@ #include "metadata/trackmetadata.h" -#include #include #include #include @@ -24,21 +23,30 @@ namespace Mixxx { bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& taglibFile); - // Read generic tags (will implicitly be invoked from the specialized functions) + // Read common metadata (will implicitly be invoked from the specialized functions) void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag); - // Read specific tags - void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& id3v2Tag); - void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& ape); - void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& xiph); - void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& mp4); + // Read additional metadata + void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag); + void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag); + void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag); + void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag); + + // Write common metadata + bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata); + + // Write additional metadata + bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata); + bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata); + bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& trackMetadata); + bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata); // In order to avoid processing images when it's not // needed (TIO building), we must process it separately. - QImage readID3v2TagCover(const TagLib::ID3v2::Tag& id3v2); - QImage readAPETagCover(const TagLib::APE::Tag& ape); - QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& xiph); - QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& mp4); + QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag); + QImage readAPETagCover(const TagLib::APE::Tag& tag); + QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag); + QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& tag); } //namespace Mixxx diff --git a/src/trackinfoobject.cpp b/src/trackinfoobject.cpp index 81ce21470d2..18d38fd7fe4 100644 --- a/src/trackinfoobject.cpp +++ b/src/trackinfoobject.cpp @@ -218,7 +218,7 @@ void TrackInfoObject::parse(bool parseCoverArt) { // Need to set BPM after sample rate since beat grid creation depends on // knowing the sample rate. Bug #1020438. - float bpm = trackMetadata.getBPM(); + float bpm = trackMetadata.getBpm(); if (bpm > 0) { // do not delete beat grid if bpm is not set in file setBpm(bpm); From f32b46dad6bf05c6b93248f8ed16e1d57031a247 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 18 Dec 2014 17:22:56 +0100 Subject: [PATCH 030/481] Move AudioTagger into metadata directory --- build/depends.py | 2 +- src/library/browse/browsetablemodel.cpp | 2 +- src/library/dao/trackdao.cpp | 2 +- src/{ => metadata}/audiotagger.cpp | 2 +- src/{ => metadata}/audiotagger.h | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/{ => metadata}/audiotagger.cpp (99%) rename src/{ => metadata}/audiotagger.h (100%) diff --git a/build/depends.py b/build/depends.py index 964156b8385..2197c392b42 100644 --- a/build/depends.py +++ b/build/depends.py @@ -661,6 +661,7 @@ def sources(self, build): "metadata/trackmetadata.cpp", "metadata/trackmetadatataglib.cpp", + "metadata/audiotagger.cpp", "sharedglcontext.cpp", "widget/controlwidgetconnection.cpp", @@ -802,7 +803,6 @@ def sources(self, build): "library/bpmdelegate.cpp", "library/previewbuttondelegate.cpp", "library/coverartdelegate.cpp", - "audiotagger.cpp", "library/treeitemmodel.cpp", "library/treeitem.cpp", diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp index adb8182a0e7..0b40b32af4e 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/browse/browsetablemodel.cpp @@ -10,7 +10,7 @@ #include "playerinfo.h" #include "controlobject.h" #include "library/dao/trackdao.h" -#include "audiotagger.h" +#include "metadata/audiotagger.h" #include "util/dnd.h" BrowseTableModel::BrowseTableModel(QObject* parent, diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index dbc619d84d9..e1055f9b6cc 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -10,7 +10,7 @@ #include "library/dao/trackdao.h" -#include "audiotagger.h" +#include "metadata/audiotagger.h" #include "library/queryutil.h" #include "soundsourceproxy.h" #include "track/beatfactory.h" diff --git a/src/audiotagger.cpp b/src/metadata/audiotagger.cpp similarity index 99% rename from src/audiotagger.cpp rename to src/metadata/audiotagger.cpp index f0f9ccc1748..0655f471d79 100644 --- a/src/audiotagger.cpp +++ b/src/metadata/audiotagger.cpp @@ -1,4 +1,4 @@ -#include "audiotagger.h" +#include "metadata/audiotagger.h" #include "metadata/trackmetadatataglib.h" diff --git a/src/audiotagger.h b/src/metadata/audiotagger.h similarity index 100% rename from src/audiotagger.h rename to src/metadata/audiotagger.h From 8c062c9cd085609bae7ab5dde51afb46e0e7df06 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 19 Dec 2014 15:21:57 +0100 Subject: [PATCH 031/481] Fix Windows build --- .../audiosourcemediafoundation.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index 1344a924bf4..a91024ac893 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -58,8 +58,8 @@ template static void safeRelease(T **ppT) { } -AudioSourceMediaFoundation::AudioSourceMediaFoundation(QString fileName) - : Super(fileName, "m4a"), m_hrCoInitialize(E_FAIL), m_hrMFStartup(E_FAIL), +AudioSourceMediaFoundation::AudioSourceMediaFoundation() + : m_hrCoInitialize(E_FAIL), m_hrMFStartup(E_FAIL), m_pReader(NULL), m_pAudioType(NULL), m_wcFilename( NULL), m_nextFrame(0), m_leftoverBuffer(NULL), m_leftoverBufferSize( 0), m_leftoverBufferLength(0), m_leftoverBufferPosition(0), m_mfDuration( @@ -121,7 +121,7 @@ Result AudioSourceMediaFoundation::postConstruct(QString fileName) { m_wcFilename[wcFilenameLength] = '\0'; // Create the source reader to read the input file. - hr = MFCreateSourceReaderFromURL(m_wcFilename, NULL, &m_pReader); + HRESULT hr = MFCreateSourceReaderFromURL(m_wcFilename, NULL, &m_pReader); if (FAILED(hr)) { qWarning() << "SSMF: Error opening input file:" << fileName; return ERR; @@ -248,10 +248,8 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readFrameSamplesInterl ×tamp, // [out] LONGLONG *pllTimestamp, &pSample); // [out] IMFSample **ppSample if (FAILED(hr)) { - if (sDebug) { - qDebug() << "ReadSample failed."; - } - break; + qWarning() << "ReadSample failed!"; + break; // abort } if (sDebug) { From af5f1bcacdc04e2dbf2893acc1706227d7ca1dd7 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 22 Dec 2014 22:42:11 +0100 Subject: [PATCH 032/481] Fix string encoding issues when writing tags --- src/metadata/trackmetadatataglib.cpp | 156 ++++++++++++--------------- 1 file changed, 70 insertions(+), 86 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index d0cc707be89..626521292b9 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -54,6 +54,12 @@ QString toQString(const TagLib::APE::Item& apeItem) { return toQString(apeItem.toString()); } +inline +TagLib::String toTagLibString(const QString& str) { + const QByteArray qba(str.toUtf8()); + return TagLib::String(qba.constData(), TagLib::String::UTF8); +} + template inline QString formatString(const T& value) { @@ -399,93 +405,71 @@ QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& tag) { return coverArt; } + bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { if (NULL == pTag) { return false; } - pTag->setArtist(trackMetadata.getArtist().toStdString()); - pTag->setTitle(trackMetadata.getTitle().toStdString()); - pTag->setAlbum(trackMetadata.getAlbum().toStdString()); - pTag->setGenre(trackMetadata.getGenre().toStdString()); - pTag->setComment(trackMetadata.getComment().toStdString()); - uint year = trackMetadata.getYear().toUInt(); - if (year > 0) { + pTag->setArtist(toTagLibString(trackMetadata.getArtist())); + pTag->setTitle(toTagLibString(trackMetadata.getTitle())); + pTag->setAlbum(toTagLibString(trackMetadata.getAlbum())); + pTag->setGenre(toTagLibString(trackMetadata.getGenre())); + pTag->setComment(toTagLibString(trackMetadata.getComment())); + bool yearValid = false; + uint year = trackMetadata.getYear().toUInt(&yearValid); + if (yearValid && (year > 0)) { pTag->setYear(year); } - uint track = trackMetadata.getTrackNumber().toUInt(); - if (track > 0) { + bool trackNumberValid = false; + uint track = trackMetadata.getTrackNumber().toUInt(&trackNumberValid); + if (trackNumberValid && (track > 0)) { pTag->setTrack(track); } return true; } +namespace { + void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { + pTag->removeFrames(pFrame->frameID()); + pTag->addFrame(pFrame); + } + + void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, const TagLib::ByteVector &id, const QString& text) { + TagLib::String::Type textType; + QByteArray textData; + if (4 <= pTag->header()->majorVersion()) { + // prefer UTF-8 for ID3v2.4.0 or higher + textType = TagLib::String::UTF8; + textData = text.toUtf8(); + } else { + textType = TagLib::String::Latin1; + textData = text.toLatin1(); + } + QScopedPointer pNewFrame( + new TagLib::ID3v2::TextIdentificationFrame(id, textType)); + pNewFrame->setText(toTagLibString(text)); + replaceID3v2Frame(pTag, pNewFrame.data()); + pNewFrame.take(); // release ownership + } +} + bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) { if (NULL == pTag) { return false; } - - TagLib::ID3v2::FrameList albumArtistFrame = pTag->frameListMap()["TPE2"]; - if (!albumArtistFrame.isEmpty()) { - albumArtistFrame.front()->setText(trackMetadata.getAlbumArtist().toStdString()); - } else { - //add new frame - TagLib::ID3v2::TextIdentificationFrame* newFrame = - new TagLib::ID3v2::TextIdentificationFrame( - "TPE2", TagLib::String::Latin1); - newFrame->setText(trackMetadata.getAlbumArtist().toStdString()); - pTag->addFrame(newFrame); - } - - TagLib::ID3v2::FrameList bpmFrame = pTag->frameListMap()["TBPM"]; - if (!bpmFrame.isEmpty()) { - bpmFrame.front()->setText(formatString(trackMetadata.getBpm()).toStdString()); - } else { - // add new frame TextIdentificationFrame which is responsible for TKEY and TBPM - // see http://developer.kde.org/~wheeler/taglib/api/classTagLib_1_1ID3v2_1_1TextIdentificationFrame.html - - TagLib::ID3v2::TextIdentificationFrame* newFrame = new TagLib::ID3v2::TextIdentificationFrame("TBPM", TagLib::String::Latin1); - - newFrame->setText(formatString(trackMetadata.getBpm()).toStdString()); - pTag->addFrame(newFrame); - - } - - TagLib::ID3v2::FrameList keyFrame = pTag->frameListMap()["TKEY"]; - if (!keyFrame.isEmpty()) { - keyFrame.front()->setText(trackMetadata.getKey().toStdString()); - } else { - //add new frame - TagLib::ID3v2::TextIdentificationFrame* newFrame = new TagLib::ID3v2::TextIdentificationFrame("TKEY", TagLib::String::Latin1); - - newFrame->setText(trackMetadata.getKey().toStdString()); - pTag->addFrame(newFrame); - - } - - TagLib::ID3v2::FrameList composerFrame = pTag->frameListMap()["TCOM"]; - if (!composerFrame.isEmpty()) { - composerFrame.front()->setText(trackMetadata.getComposer().toStdString()); - } else { - //add new frame - TagLib::ID3v2::TextIdentificationFrame* newFrame = - new TagLib::ID3v2::TextIdentificationFrame( - "TCOM", TagLib::String::Latin1); - newFrame->setText(trackMetadata.getComposer().toStdString()); - pTag->addFrame(newFrame); + const TagLib::ID3v2::Header* pHeader = pTag->header(); + if ((NULL == pHeader) || (3 > pHeader->majorVersion())) { + // only ID3v2.3.x and higher (currently only ID3v2.4.x) are supported + return false; } - TagLib::ID3v2::FrameList groupingFrame = pTag->frameListMap()["TIT1"]; - if (!groupingFrame.isEmpty()) { - groupingFrame.front()->setText(trackMetadata.getGrouping().toStdString()); - } else { - //add new frame - TagLib::ID3v2::TextIdentificationFrame* newFrame = - new TagLib::ID3v2::TextIdentificationFrame( - "TIT1", TagLib::String::Latin1); - newFrame->setText(trackMetadata.getGrouping().toStdString()); - pTag->addFrame(newFrame); + writeID3v2TextIdentificationFrame(pTag, "TPE2", trackMetadata.getAlbumArtist()); + writeID3v2TextIdentificationFrame(pTag, "TBPM", formatString(trackMetadata.getBpm())); + writeID3v2TextIdentificationFrame(pTag, "TKEY", formatString(trackMetadata.getKey())); + writeID3v2TextIdentificationFrame(pTag, "TCOM", formatString(trackMetadata.getComposer())); + writeID3v2TextIdentificationFrame(pTag, "TIT1", formatString(trackMetadata.getGrouping())); } return true; @@ -498,10 +482,10 @@ bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { // Adds to the item specified by key the data value. // If replace is true, then all of the other values on the same key will be removed first. - pTag->addValue("BPM", formatString(trackMetadata.getBpm()).toStdString(), true); - pTag->addValue("Album Artist", trackMetadata.getAlbumArtist().toStdString(), true); - pTag->addValue("Composer", trackMetadata.getComposer().toStdString(), true); - pTag->addValue("Grouping", trackMetadata.getGrouping().toStdString(), true); + pTag->addValue("BPM", toTagLibString(formatString(trackMetadata.getBpm())), true); + pTag->addValue("Album Artist", toTagLibString(trackMetadata.getAlbumArtist()), true); + pTag->addValue("Composer", toTagLibString(trackMetadata.getComposer()), true); + pTag->addValue("Grouping", toTagLibString(trackMetadata.getGrouping()), true); return true; } @@ -512,27 +496,27 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& track } // Taglib does not support the update of Vorbis comments. - // thus, we have to reomve the old comment and add the new one + // thus, we have to remove the old comment and add the new one pTag->removeField("ALBUMARTIST"); - pTag->addField("ALBUMARTIST", trackMetadata.getAlbumArtist().toStdString()); + pTag->addField("ALBUMARTIST", toTagLibString(trackMetadata.getAlbumArtist())); // Some tools use "BPM" so write that. pTag->removeField("BPM"); - pTag->addField("BPM",formatString(trackMetadata.getBpm()).toStdString()); + pTag->addField("BPM", toTagLibString(formatString(trackMetadata.getBpm()))); pTag->removeField("TEMPO"); - pTag->addField("TEMPO",formatString(trackMetadata.getBpm()).toStdString()); + pTag->addField("TEMPO", toTagLibString(formatString(trackMetadata.getBpm()))); pTag->removeField("INITIALKEY"); - pTag->addField("INITIALKEY", trackMetadata.getKey().toStdString()); + pTag->addField("INITIALKEY", toTagLibString(trackMetadata.getKey())); pTag->removeField("KEY"); - pTag->addField("KEY", trackMetadata.getKey().toStdString()); + pTag->addField("KEY", toTagLibString(trackMetadata.getKey())); pTag->removeField("COMPOSER"); - pTag->addField("COMPOSER", trackMetadata.getComposer().toStdString()); + pTag->addField("COMPOSER", toTagLibString(trackMetadata.getComposer())); pTag->removeField("GROUPING"); - pTag->addField("GROUPING", trackMetadata.getGrouping().toStdString()); + pTag->addField("GROUPING", toTagLibString(trackMetadata.getGrouping())); return true; } @@ -542,12 +526,12 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { return false; } - pTag->itemListMap()["aART"] = TagLib::StringList(trackMetadata.getAlbumArtist().toStdString()); - pTag->itemListMap()["tmpo"] = TagLib::StringList(formatString(trackMetadata.getBpm()).toStdString()); - pTag->itemListMap()["----:com.apple.iTunes:BPM"] = TagLib::StringList(formatString(trackMetadata.getBpm()).toStdString()); - pTag->itemListMap()["----:com.apple.iTunes:KEY"] = TagLib::StringList(trackMetadata.getKey().toStdString()); - pTag->itemListMap()["\251wrt"] = TagLib::StringList(trackMetadata.getComposer().toStdString()); - pTag->itemListMap()["\251grp"] = TagLib::StringList(trackMetadata.getGrouping().toStdString()); + pTag->itemListMap()["aART"] = TagLib::StringList(toTagLibString(trackMetadata.getAlbumArtist())); + pTag->itemListMap()["tmpo"] = TagLib::StringList(toTagLibString(formatString(trackMetadata.getBpm()))); + pTag->itemListMap()["----:com.apple.iTunes:BPM"] = TagLib::StringList(toTagLibString(formatString(trackMetadata.getBpm()))); + pTag->itemListMap()["----:com.apple.iTunes:KEY"] = TagLib::StringList(toTagLibString(trackMetadata.getKey())); + pTag->itemListMap()["\251wrt"] = TagLib::StringList(toTagLibString(trackMetadata.getComposer())); + pTag->itemListMap()["\251grp"] = TagLib::StringList(toTagLibString(trackMetadata.getGrouping())); return true; } From 8a8b340c4ff03c01caa0dbeced6090416efc42c1 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 22 Dec 2014 22:47:21 +0100 Subject: [PATCH 033/481] Read and write year/date tag as string if supported --- src/metadata/trackmetadatataglib.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 626521292b9..e81c86a3fa3 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -194,6 +194,13 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) QString sGrouping = toQString(groupingFrame.front()->toString()); pTrackMetadata->setGrouping(sGrouping); } + + // ID3v2.4.0: TDRC replaces TYER + TDAT + TagLib::ID3v2::FrameList recordingDateFrame = tag.frameListMap()["TDRC"]; + if (!recordingDateFrame.isEmpty()) { + QString sRecordingDate = toQString(recordingDateFrame.front()->toString()); + pTrackMetadata->setYear(sRecordingDate); + } } void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { @@ -229,6 +236,10 @@ void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { if (tag.itemListMap().contains("Grouping")) { pTrackMetadata->setGrouping(toQString(tag.itemListMap()["Grouping"])); } + + if (tag.itemListMap().contains("Year")) { + pTrackMetadata->setYear(toQString(tag.itemListMap()["Year"])); + } } void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag) { @@ -290,6 +301,10 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme if (tag.fieldListMap().contains("GROUPING")) { pTrackMetadata->setGrouping(toQString(tag.fieldListMap()["GROUPING"])); } + + if (tag.fieldListMap().contains("DATE")) { + pTrackMetadata->setYear(toQString(tag.fieldListMap()["DATE"])); + } } void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) { @@ -326,6 +341,11 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) pTrackMetadata->setGrouping(toQString(tag.itemListMap()["\251grp"])); } + // Get date/year as string + if (tag.itemListMap().contains("\251day")) { + pTrackMetadata->setYear(toQString(tag.itemListMap()["\251day"])); + } + // Get KEY (conforms to Rapid Evolution) if (tag.itemListMap().contains("----:com.apple.iTunes:KEY")) { QString key = toQString( @@ -470,6 +490,9 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) writeID3v2TextIdentificationFrame(pTag, "TKEY", formatString(trackMetadata.getKey())); writeID3v2TextIdentificationFrame(pTag, "TCOM", formatString(trackMetadata.getComposer())); writeID3v2TextIdentificationFrame(pTag, "TIT1", formatString(trackMetadata.getGrouping())); + if (4 <= pHeader->majorVersion()) { + // ID3v2.4.0: TDRC replaces TYER + TDAT + writeID3v2TextIdentificationFrame(pTag, "TDRC", formatString(trackMetadata.getYear())); } return true; @@ -486,6 +509,7 @@ bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { pTag->addValue("Album Artist", toTagLibString(trackMetadata.getAlbumArtist()), true); pTag->addValue("Composer", toTagLibString(trackMetadata.getComposer()), true); pTag->addValue("Grouping", toTagLibString(trackMetadata.getGrouping()), true); + pTag->addValue("Year", toTagLibString(trackMetadata.getYear()), true); return true; } @@ -518,6 +542,9 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& track pTag->removeField("GROUPING"); pTag->addField("GROUPING", toTagLibString(trackMetadata.getGrouping())); + pTag->removeField("DATE"); + pTag->addField("DATE", toTagLibString(trackMetadata.getYear())); + return true; } @@ -532,6 +559,7 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { pTag->itemListMap()["----:com.apple.iTunes:KEY"] = TagLib::StringList(toTagLibString(trackMetadata.getKey())); pTag->itemListMap()["\251wrt"] = TagLib::StringList(toTagLibString(trackMetadata.getComposer())); pTag->itemListMap()["\251grp"] = TagLib::StringList(toTagLibString(trackMetadata.getGrouping())); + pTag->itemListMap()["\251day"] = TagLib::StringList(toTagLibString(trackMetadata.getYear())); return true; } From 12c201e79a39dc0da7ecd0d339aa1c2fb9f7d6ea Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 1 Jan 2015 22:55:04 +0100 Subject: [PATCH 034/481] Fix decoding of corrupt FLAC files (fixes bug #1406815) https://bugs.launchpad.net/mixxx/+bug/1406815 --- src/sources/audiosourceflac.cpp | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index a5548bb8a78..049c382961e 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -151,8 +151,6 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readFrameSamplesInterleaved( // if our buffer from libflac is empty (either because we explicitly cleared // it or because we've simply used all the samples), ask for a new buffer if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { - m_decodeSampleBufferReadOffset = 0; - m_decodeSampleBufferWriteOffset = 0; if (FLAC__stream_decoder_process_single(m_decoder)) { if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { @@ -249,16 +247,29 @@ FLAC__bool AudioSourceFLAC::flacEOF() { FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( const FLAC__Frame *frame, const FLAC__int32 * const buffer[]) { + // decode buffer must be empty before decoding the next frame + DEBUG_ASSERT(m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset); + // reset decode buffer + m_decodeSampleBufferReadOffset = 0; + m_decodeSampleBufferWriteOffset = 0; if (getChannelCount() != frame->header.channels) { - qWarning() << "Invalid number of channels in FLAC frame header:" - << "expected" << getChannelCount() << "actual" - << frame->header.channels; + qWarning() << "Corrupt FLAC file:" + << "Invalid number of channels in FLAC frame header" + << frame->header.channels << "<>" << getChannelCount(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); - DEBUG_ASSERT( - (m_decodeSampleBuffer.size() - m_decodeSampleBufferWriteOffset) - >= frames2samples(frame->header.blocksize)); + if (getFrameRate() != frame->header.sample_rate) { + qWarning() << "Corrupt FLAC file:" + << "Invalid sample rate in FLAC frame header" + << frame->header.sample_rate << "<>" << getFrameRate(); + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; + } + const SampleBuffer::size_type maxBlocksize = samples2frames(m_decodeSampleBuffer.size()); + if (maxBlocksize < frame->header.blocksize) { + qWarning() << "Corrupt FLAC file:" + << "Block size in FLAC frame header exceeds the maximum block size" + << frame->header.blocksize << ">" << maxBlocksize; + } switch (getChannelCount()) { case 1: { // optimized code for 1 channel (mono) From f1dd4b821e5fb206b599062dd028aeee38225a9a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 2 Jan 2015 03:10:29 +0100 Subject: [PATCH 035/481] Fix bitrate calculation for MP3 files --- src/sources/audiosourcemp3.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index fcac2742c57..7d4b299a340 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -197,7 +197,7 @@ Result AudioSourceMp3::postConstruct() { setFrameCount(mad_timer_count(madFileDuration, madUnits)); m_avgSeekFrameCount = getFrameCount() / madFrameCount; int avgBitrate = sumBitrate / madFrameCount; - setBitrate(avgBitrate); + setBitrate(avgBitrate / 1000); } else { // This is not a working MP3 file. qWarning() << "SSMP3: This is not a working MP3 file:" << m_file.fileName(); From 0471f362714527dc34f6b5ea76d1d9c6d95dbb7e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 2 Jan 2015 17:20:43 +0100 Subject: [PATCH 036/481] Some cleanup in MP3 decoder --- src/sources/audiosourcemp3.cpp | 66 +++++++++++++++++----------------- src/sources/audiosourcemp3.h | 21 ++++++----- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 7d4b299a340..c2fe73e7173 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -49,9 +49,8 @@ AudioSourceMp3::AudioSourceMp3(QString fileName) m_avgSeekFrameCount(0), m_curFrameIndex(0), m_madSynthCount(0) { - mad_stream_init(&m_madStream); - mad_frame_init(&m_madFrame); - mad_synth_init(&m_madSynth); + initDecoding(); + m_seekFrameList.reserve(kSeekFrameListCapacity); } AudioSourceMp3::~AudioSourceMp3() { @@ -79,14 +78,9 @@ Result AudioSourceMp3::postConstruct() { // Get a pointer to the file using memory mapped IO m_fileSize = m_file.size(); m_pFileData = m_file.map(0, m_fileSize); - // Transfer it to the mad stream-buffer: - mad_stream_init(&m_madStream); - mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); mad_stream_buffer(&m_madStream, m_pFileData, m_fileSize); - m_seekFrameList.reserve(kSeekFrameListCapacity); - // Decode all the headers and calculate audio properties unsigned long sumBitrate = 0; unsigned int madFrameCount = 0; @@ -180,7 +174,7 @@ Result AudioSourceMp3::postConstruct() { } // Add frame to list of frames - MadSeekFrameType seekFrame; + SeekFrameType seekFrame; seekFrame.pFileData = m_madStream.this_frame; seekFrame.frameIndex = mad_timer_count(madFileDuration, madUnits); m_seekFrameList.push_back(seekFrame); @@ -204,22 +198,39 @@ Result AudioSourceMp3::postConstruct() { return ERR; // abort } - // restart decoding - m_curFrameIndex = getFrameCount(); - seekFrame(0); + restartDecoding(m_seekFrameList.front()); return OK; } -void AudioSourceMp3::close() throw() { - m_seekFrameList.clear(); - m_avgSeekFrameCount = 0; - m_curFrameIndex = 0; +void AudioSourceMp3::initDecoding() { + mad_stream_init(&m_madStream); + mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); + mad_frame_init(&m_madFrame); + mad_synth_init(&m_madSynth); +} +void AudioSourceMp3::finishDecoding() { mad_synth_finish(&m_madSynth); mad_frame_finish(&m_madFrame); mad_stream_finish(&m_madStream); m_madSynthCount = 0; + m_curFrameIndex = getFrameCount(); // invalidate +} + +void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { + finishDecoding(); + initDecoding(); + mad_stream_buffer(&m_madStream, seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); + m_curFrameIndex = seekFrame.frameIndex; +} + +void AudioSourceMp3::close() throw() { + finishDecoding(); + + m_seekFrameList.clear(); + m_avgSeekFrameCount = 0; + m_curFrameIndex = 0; m_file.unmap(m_pFileData); m_fileSize = 0; @@ -229,18 +240,18 @@ void AudioSourceMp3::close() throw() { Super::reset(); } -AudioSourceMp3::MadSeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { +AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { if ((0 >= frameIndex) || m_seekFrameList.empty()) { return 0; } // Guess position of frame in m_seekFrameList based on average frame size - AudioSourceMp3::MadSeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; + AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; if (seekFrameIndex >= m_seekFrameList.size()) { seekFrameIndex = m_seekFrameList.size() - 1; } // binary search starting at seekFrameIndex - AudioSourceMp3::MadSeekFrameList::size_type lowerBound = 0; - AudioSourceMp3::MadSeekFrameList::size_type upperBound = m_seekFrameList.size(); + AudioSourceMp3::SeekFrameList::size_type lowerBound = 0; + AudioSourceMp3::SeekFrameList::size_type upperBound = m_seekFrameList.size(); while ((upperBound - lowerBound) > 1) { DEBUG_ASSERT(seekFrameIndex >= lowerBound); DEBUG_ASSERT(seekFrameIndex < upperBound); @@ -266,25 +277,14 @@ AudioSource::diff_type AudioSourceMp3::seekFrame(diff_type frameIndex) { } // simply skip frames when jumping no more than kMaxSkipFrameSamplesWhenSeeking frames forward if ((frameIndex < m_curFrameIndex) || ((frameIndex - m_curFrameIndex) > kMaxSkipFrameSamplesWhenSeeking)) { - MadSeekFrameList::size_type seekFrameIndex = findSeekFrameIndex(frameIndex); + SeekFrameList::size_type seekFrameIndex = findSeekFrameIndex(frameIndex); if (seekFrameIndex <= kSeekFramePrefetchCount) { seekFrameIndex = 0; } else { seekFrameIndex -= kSeekFramePrefetchCount; } DEBUG_ASSERT(seekFrameIndex < m_seekFrameList.size()); - const MadSeekFrameType& seekFrame(m_seekFrameList[seekFrameIndex]); - // restart decoder - mad_synth_finish(&m_madSynth); - mad_frame_finish(&m_madFrame); - mad_stream_finish(&m_madStream); - mad_stream_init(&m_madStream); - mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); - mad_stream_buffer(&m_madStream, seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); - mad_frame_init(&m_madFrame); - mad_synth_init(&m_madSynth); - m_curFrameIndex = seekFrame.frameIndex; - m_madSynthCount = 0; + restartDecoding(m_seekFrameList[seekFrameIndex]); } // decode and discard prefetch data DEBUG_ASSERT(m_curFrameIndex <= frameIndex); diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index 2151454ddd7..cb29daa9b9a 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -51,26 +51,29 @@ class AudioSourceMp3 : public AudioSource { mad_stream m_madStream; /** Struct used to store mad frames for seeking */ - struct MadSeekFrameType { + struct SeekFrameType { diff_type frameIndex; const unsigned char* pFileData; }; /** It is not possible to make a precise seek in an mp3 file without decoding the whole stream. * To have precise seek within a limited range from the current decode position, we keep track - * of past decodeded frame, and their exact position. If a seek occours and it is within the - * range of frames we keep track of a precise seek occours, otherwise an unprecise seek is performed + * of past decoded frame, and their exact position. If a seek occurs and it is within the + * range of frames we keep track of a precise seek occurs, otherwise an unprecise seek is performed */ - typedef std::vector MadSeekFrameList; - MadSeekFrameList m_seekFrameList; // ordered-by frameIndex - size_type m_avgSeekFrameCount; // avg. samples frames per MP3 frame + typedef std::vector SeekFrameList; + SeekFrameList m_seekFrameList; // ordered-by frameIndex + size_type m_avgSeekFrameCount; // avg. sample frames per MP3 frame - /** Returns the position of the frame which was found. The found frame is set to - * the current element in m_qSeekList */ - MadSeekFrameList::size_type findSeekFrameIndex(diff_type frameIndex) const; + /** Returns the position in m_seekFrameList of the requested frame index. */ + SeekFrameList::size_type findSeekFrameIndex(diff_type frameIndex) const; diff_type m_curFrameIndex; + void initDecoding(); + void finishDecoding(); + void restartDecoding(const SeekFrameType& seekFrame); + // current play position mad_frame m_madFrame; mad_synth m_madSynth; From 757e3b10d0cfc4775e69c60e53fc9f923259c897 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 2 Jan 2015 17:21:37 +0100 Subject: [PATCH 037/481] Minor changes in M4A/FLAC/OPUS decoders --- plugins/soundsourcem4a/audiosourcem4a.cpp | 24 +++++++++++++---------- src/sources/audiosourceflac.cpp | 7 ++++--- src/sources/audiosourceopus.cpp | 1 + 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 5991784b594..2f2c3cedc7e 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -247,8 +247,8 @@ AudioSource::size_type AudioSourceM4A::readFrameSamplesInterleaved( } } if (0 == m_inputBufferLength) { - // no more input data available (EOF) - break;// done + // EOF + break; // done } faacDecFrameInfo decFrameInfo; decFrameInfo.bytesconsumed = 0; @@ -263,6 +263,11 @@ AudioSource::size_type AudioSourceM4A::readFrameSamplesInterleaved( &m_inputBuffer[m_inputBufferOffset], m_inputBufferLength / sizeof(m_inputBuffer[0]), &pDecodeBuffer, decodeBufferCapacityInBytes); + if (0 != decFrameInfo.error) { + qWarning() << "AAC decoding error:" + << faacDecGetErrorMessage(decFrameInfo.error); + break; // abort + } // samples should have been decoded into our own buffer DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); pSampleBuffer += decFrameInfo.samples; @@ -276,18 +281,17 @@ AudioSource::size_type AudioSourceM4A::readFrameSamplesInterleaved( m_inputBufferLength -= decFrameInfo.bytesconsumed * sizeof(m_inputBuffer[0]); m_curFrameIndex += samples2frames(decFrameInfo.samples); - if (0 != decFrameInfo.error) { - qWarning() << "AAC decoding error:" - << faacDecGetErrorMessage(decFrameInfo.error); - break; // abort - } + // verify output sample data for consistency if (getChannelCount() != decFrameInfo.channels) { - qWarning() << "Unexpected number of channels:" - << decFrameInfo.channels; + qWarning() << "Corrupt or unsupported AAC file:" + "Unexpected number of channels" + << decFrameInfo.channels << "<>" << getChannelCount(); break; // abort } if (getFrameRate() != decFrameInfo.samplerate) { - qWarning() << "Unexpected sample rate:" << decFrameInfo.samplerate; + qWarning() << "Corrupt or unsupported AAC file:" + << "Unexpected sample rate" + << decFrameInfo.samplerate << "<>" << getFrameRate(); break; // abort } } diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 049c382961e..bf756a69369 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -253,20 +253,20 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; if (getChannelCount() != frame->header.channels) { - qWarning() << "Corrupt FLAC file:" + qWarning() << "Corrupt or unsupported FLAC file:" << "Invalid number of channels in FLAC frame header" << frame->header.channels << "<>" << getChannelCount(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } if (getFrameRate() != frame->header.sample_rate) { - qWarning() << "Corrupt FLAC file:" + qWarning() << "Corrupt or unsupported FLAC file:" << "Invalid sample rate in FLAC frame header" << frame->header.sample_rate << "<>" << getFrameRate(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } const SampleBuffer::size_type maxBlocksize = samples2frames(m_decodeSampleBuffer.size()); if (maxBlocksize < frame->header.blocksize) { - qWarning() << "Corrupt FLAC file:" + qWarning() << "Corrupt or unsupported FLAC file:" << "Block size in FLAC frame header exceeds the maximum block size" << frame->header.blocksize << ">" << maxBlocksize; } @@ -293,6 +293,7 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( } default: { // generic code for multiple channels + DEBUG_ASSERT(getChannelCount() == frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { for (unsigned j = 0; j < frame->header.channels; ++j) { m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 839a4b8a1a6..04d21ea7cbc 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -77,6 +77,7 @@ AudioSource::size_type AudioSourceOpus::readFrameSamplesInterleaved( sampleBuffer + frames2samples(readCount), frames2samples(frameCount - readCount), NULL); if (0 == readResult) { + // EOF break; // done } if (0 < readResult) { From 4f970b6ffeb63f4adc677a7ba61c43b40ba40b10 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Fri, 2 Jan 2015 23:17:05 +0100 Subject: [PATCH 038/481] Avoid intermediate string conversions of BPM column --- src/library/browse/browsetablemodel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library/browse/browsetablemodel.cpp b/src/library/browse/browsetablemodel.cpp index 0b40b32af4e..48b0df35db1 100644 --- a/src/library/browse/browsetablemodel.cpp +++ b/src/library/browse/browsetablemodel.cpp @@ -324,7 +324,7 @@ bool BrowseTableModel::setData(const QModelIndex &index, const QVariant &value, trackMetadata.setTitle(this->index(row, COLUMN_TITLE).data().toString()); trackMetadata.setAlbum(this->index(row, COLUMN_ALBUM).data().toString()); trackMetadata.setKey(this->index(row, COLUMN_KEY).data().toString()); - trackMetadata.setBpmString(this->index(row, COLUMN_BPM).data().toString()); + trackMetadata.setBpm(this->index(row, COLUMN_BPM).data().toDouble()); trackMetadata.setComment(this->index(row, COLUMN_COMMENT).data().toString()); trackMetadata.setTrackNumber(this->index(row, COLUMN_TRACK_NUMBER).data().toString()); trackMetadata.setYear(this->index(row, COLUMN_YEAR).data().toString()); @@ -341,7 +341,7 @@ bool BrowseTableModel::setData(const QModelIndex &index, const QVariant &value, } else if (col == COLUMN_ALBUM) { trackMetadata.setAlbum(value.toString()); } else if (col == COLUMN_BPM) { - trackMetadata.setBpmString(value.toString()); + trackMetadata.setBpm(value.toDouble()); } else if (col == COLUMN_KEY) { trackMetadata.setKey(value.toString()); } else if (col == COLUMN_TRACK_NUMBER) { From b67de6c3c4584f76d3170c270907501f0870013c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 3 Jan 2015 11:00:21 +0100 Subject: [PATCH 039/481] Improve parsing of BPM and ReplayGain tags --- src/metadata/trackmetadata.cpp | 55 ++++++++++++---------------- src/metadata/trackmetadata.h | 28 +++++++++++--- src/metadata/trackmetadatataglib.cpp | 18 ++++----- src/sources/soundsourceffmpeg.cpp | 2 +- src/sources/soundsourceopus.cpp | 2 +- 5 files changed, 57 insertions(+), 48 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 01cbd1d3430..1d4b3bc2d5b 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -4,46 +4,39 @@ namespace Mixxx { -namespace { -const float BPM_ZERO = 0.0f; -const float BPM_MAX = 300.0f; +TrackMetadata::TrackMetadata() + : m_channels(0), m_sampleRate(0), m_bitrate(0), m_duration(0), m_bpm(BPM_UNDEFINED), m_replayGain(REPLAYGAIN_UNDEFINED) { +} -float parseBpmString(const QString& sBpm) { - float bpm = sBpm.toFloat(); +double TrackMetadata::parseBpmString(const QString& sBpm) { + bool bpmValid = false; + double bpm = sBpm.toDouble(&bpmValid); + if ((!bpmValid) || (BPM_MIN > bpm)) { + return BPM_UNDEFINED; + } while (bpm > BPM_MAX) { - bpm /= 10.0f; + bpm /= 10.0; } return bpm; } -float parseReplayGainString(QString sReplayGain) { - QString ReplayGainstring = sReplayGain.remove(" dB"); - float fReplayGain = db2ratio(ReplayGainstring.toFloat()); +float TrackMetadata::parseReplayGainDbString(QString sReplayGainDb) { + sReplayGainDb.remove("dB"); + bool replayGainDbValid = false; + const double replayGainDb = sReplayGainDb.toDouble(&replayGainDbValid); + if (!replayGainDbValid) { + return REPLAYGAIN_UNDEFINED; + } + float replayGain = db2ratio(replayGainDb); + if (REPLAYGAIN_MIN > replayGain) { + return REPLAYGAIN_UNDEFINED; + } // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by setting value to 0 (i.e. rescan via analyserrg) - if (fReplayGain == 1.0f) { - fReplayGain = 0.0f; + if (replayGain == REPLAYGAIN_0DB) { + replayGain = REPLAYGAIN_UNDEFINED; } - return fReplayGain; -} - -} - -TrackMetadata::TrackMetadata() - : m_channels(0), m_sampleRate(0), m_bitrate(0), m_duration(0), m_replayGain(0.0f), m_bpm(BPM_ZERO) { -} - -void TrackMetadata::setBpmString(QString sBpm) { - if (!sBpm.isEmpty()) { - float fBpm = parseBpmString(sBpm); - if (BPM_ZERO < fBpm) { - setBpm(fBpm); - } - } -} - -void TrackMetadata::setReplayGainString(QString sReplayGain) { - setReplayGain(parseReplayGainString(sReplayGain)); + return replayGain; } } //namespace Mixxx diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index e32a2ad104d..3d1a307ba41 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -121,21 +121,39 @@ class TrackMetadata { } // beats / minute - inline float getBpm() const { + static const double BPM_UNDEFINED = 0.0; + static const double BPM_MIN = 0.0; + static const double BPM_MAX = 300.0; + inline double getBpm() const { return m_bpm; } - inline void setBpm(float bpm) { + inline void setBpm(double bpm) { m_bpm = bpm; } - void setBpmString(QString sBpm); + inline void resetBpm() { + m_bpm = BPM_UNDEFINED; + } + static double parseBpmString(const QString& sBpm); + inline void setBpmString(const QString& sBpm) { + setBpm(parseBpmString(sBpm)); + } + static const double REPLAYGAIN_UNDEFINED = 0.0f; + static const double REPLAYGAIN_MIN = 0.0f; + static const double REPLAYGAIN_0DB = 1.0f; inline float getReplayGain() const { return m_replayGain; } inline void setReplayGain(float replayGain) { m_replayGain = replayGain; } - void setReplayGainString(QString sReplayGain); + inline void resetReplayGain() { + m_replayGain = REPLAYGAIN_UNDEFINED; + } + static float parseReplayGainDbString(QString sReplayGainDb); // in dB + inline void setReplayGainDbString(QString sReplayGainDb) { // in dB + setReplayGain(parseReplayGainDbString(sReplayGainDb)); + } private: QString m_artist; @@ -157,8 +175,8 @@ class TrackMetadata { int m_sampleRate; int m_bitrate; int m_duration; + double m_bpm; float m_replayGain; - float m_bpm; }; } diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index e81c86a3fa3..0aaa814fcf1 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -16,12 +16,10 @@ #include -namespace Mixxx -{ +namespace Mixxx { // static -namespace -{ +namespace { const bool kDebugMetadata = false; // Taglib strings can be NULL and using it could cause some segfaults, @@ -158,12 +156,12 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) QString desc = toQString(ReplayGainframe->description()).toLower(); if (desc == "replaygain_album_gain") { QString sReplayGain = toQString(ReplayGainframe->fieldList()[1]); - pTrackMetadata->setReplayGainString(sReplayGain); + pTrackMetadata->setReplayGainDbString(sReplayGain); } //Prefer track gain over album gain. if (desc == "replaygain_track_gain") { QString sReplayGain = toQString(ReplayGainframe->fieldList()[1]); - pTrackMetadata->setReplayGainString(sReplayGain); + pTrackMetadata->setReplayGainDbString(sReplayGain); } } } @@ -218,11 +216,11 @@ void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { } if (tag.itemListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pTrackMetadata->setReplayGainString(toQString(tag.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); + pTrackMetadata->setReplayGainDbString(toQString(tag.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. if (tag.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainString(toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); + pTrackMetadata->setReplayGainDbString(toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); } if (tag.itemListMap().contains("Album Artist")) { @@ -263,11 +261,11 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme } if (tag.fieldListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pTrackMetadata->setReplayGainString(toQString(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); + pTrackMetadata->setReplayGainDbString(toQString(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. if (tag.fieldListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainString(toQString(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); + pTrackMetadata->setReplayGainDbString(toQString(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); } /* diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index c4486470b7a..f42f7e14e2e 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -163,7 +163,7 @@ Result SoundSourceFFmpeg::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { pMetadata->setTitle(strValue); } else if (!strncmp(FmtTag->key, "REPLAYGAIN_TRACK_PEAK", 20)) { } else if (!strncmp(FmtTag->key, "REPLAYGAIN_TRACK_GAIN", 20)) { - pMetadata->setReplayGainString (strValue); + pMetadata->setReplayGainDbString (strValue); } else if (!strncmp(FmtTag->key, "REPLAYGAIN_ALBUM_PEAK", 20)) { } else if (!strncmp(FmtTag->key, "REPLAYGAIN_ALBUM_GAIN", 20)) { } diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index b0fab591cb5..e1faa6e49a6 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -103,7 +103,7 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { pMetadata->setTitle(l_SPayload); } else if (!l_STag.compare("REPLAYGAIN_TRACK_PEAK")) { } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) { - pMetadata->setReplayGainString (l_SPayload); + pMetadata->setReplayGainDbString (l_SPayload); } else if (!l_STag.compare("REPLAYGAIN_ALBUM_PEAK")) { } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) { } From 6ff2333d5484f124b9e651a48cce75f5e9957f31 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 3 Jan 2015 12:29:06 +0100 Subject: [PATCH 040/481] Igore invalid/undefined BPM and ReplayGain values in tags --- src/metadata/trackmetadata.cpp | 26 +++++++++++++++++++++++--- src/metadata/trackmetadata.h | 8 ++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 1d4b3bc2d5b..ace3ccfae04 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -20,6 +20,16 @@ double TrackMetadata::parseBpmString(const QString& sBpm) { return bpm; } +bool TrackMetadata::setBpmString(const QString& sBpm) { + const double bpm = parseBpmString(sBpm); + if (BPM_UNDEFINED != bpm) { + setBpm(parseBpmString(sBpm)); + return true; + } else { + return false; + } +} + float TrackMetadata::parseReplayGainDbString(QString sReplayGainDb) { sReplayGainDb.remove("dB"); bool replayGainDbValid = false; @@ -27,16 +37,26 @@ float TrackMetadata::parseReplayGainDbString(QString sReplayGainDb) { if (!replayGainDbValid) { return REPLAYGAIN_UNDEFINED; } - float replayGain = db2ratio(replayGainDb); + const float replayGain = db2ratio(replayGainDb); if (REPLAYGAIN_MIN > replayGain) { return REPLAYGAIN_UNDEFINED; } // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by setting value to 0 (i.e. rescan via analyserrg) - if (replayGain == REPLAYGAIN_0DB) { - replayGain = REPLAYGAIN_UNDEFINED; + if (REPLAYGAIN_0DB == replayGain) { + return REPLAYGAIN_UNDEFINED; } return replayGain; } +bool TrackMetadata::setReplayGainDbString(QString sReplayGainDb) { + const float replayGain = parseReplayGainDbString(sReplayGainDb); + if (REPLAYGAIN_UNDEFINED != replayGain) { + setReplayGain(replayGain); + return true; + } else { + return false; + } +} + } //namespace Mixxx diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 3d1a307ba41..54c6bef0d99 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -134,9 +134,7 @@ class TrackMetadata { m_bpm = BPM_UNDEFINED; } static double parseBpmString(const QString& sBpm); - inline void setBpmString(const QString& sBpm) { - setBpm(parseBpmString(sBpm)); - } + bool setBpmString(const QString& sBpm); static const double REPLAYGAIN_UNDEFINED = 0.0f; static const double REPLAYGAIN_MIN = 0.0f; @@ -151,9 +149,7 @@ class TrackMetadata { m_replayGain = REPLAYGAIN_UNDEFINED; } static float parseReplayGainDbString(QString sReplayGainDb); // in dB - inline void setReplayGainDbString(QString sReplayGainDb) { // in dB - setReplayGain(parseReplayGainDbString(sReplayGainDb)); - } + bool setReplayGainDbString(QString sReplayGainDb); // in dB private: QString m_artist; From 36988fcd936d294fccf0a848b07bae2de47dfe74 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 4 Jan 2015 15:59:30 +0100 Subject: [PATCH 041/481] Improve metadata handling in TrackInfoObject --- src/library/dao/trackdao.cpp | 20 +--- src/library/dao/trackdao.h | 2 +- src/metadata/trackmetadata.h | 12 ++- src/soundsourceproxy.cpp | 6 +- src/trackinfoobject.cpp | 192 ++++++++++++++++++----------------- src/trackinfoobject.h | 11 +- 6 files changed, 125 insertions(+), 118 deletions(-) diff --git a/src/library/dao/trackdao.cpp b/src/library/dao/trackdao.cpp index e1055f9b6cc..40b6b8c58e6 100644 --- a/src/library/dao/trackdao.cpp +++ b/src/library/dao/trackdao.cpp @@ -273,8 +273,8 @@ void TrackDAO::saveTrack(TrackInfoObject* pTrack) { updateTrack(pTrack); // Write audio meta data, if enabled in the preferences - // TODO(DSC) Only wite tag if file Metatdate is dirty - writeAudioMetaData(pTrack); + // TODO(DSC) Only wite tag if file Metatdata is dirty + writeMetadataToFile(pTrack); //qDebug() << this << "Dirty tracks remaining after clean save:" << m_dirtyTracks.size(); } else { @@ -1785,23 +1785,11 @@ void TrackDAO::markTracksAsMixxxDeleted(const QString& dir) { } } -void TrackDAO::writeAudioMetaData(TrackInfoObject* pTrack) { +void TrackDAO::writeMetadataToFile(TrackInfoObject* pTrack) { if (m_pConfig && m_pConfig->getValueString(ConfigKey("[Library]","WriteAudioTags")).toInt() == 1) { Mixxx::TrackMetadata trackMetadata; - trackMetadata.setArtist(pTrack->getArtist()); - trackMetadata.setTitle(pTrack->getTitle()); - trackMetadata.setGenre(pTrack->getGenre()); - trackMetadata.setComposer(pTrack->getComposer()); - trackMetadata.setGrouping(pTrack->getGrouping()); - trackMetadata.setAlbum(pTrack->getAlbum()); - trackMetadata.setAlbumArtist(pTrack->getAlbumArtist()); - trackMetadata.setComment(pTrack->getComment()); - trackMetadata.setTrackNumber(pTrack->getTrackNumber()); - trackMetadata.setBpm(pTrack->getBpm()); - trackMetadata.setKey(pTrack->getKeyText()); - trackMetadata.setComposer(pTrack->getComposer()); - trackMetadata.setGrouping(pTrack->getGrouping()); + pTrack->getMetadata(&trackMetadata); AudioTagger tagger(pTrack->getLocation(), pTrack->getSecurityToken()); tagger.save(trackMetadata); diff --git a/src/library/dao/trackdao.h b/src/library/dao/trackdao.h index 77b58543fe1..f992917adbb 100644 --- a/src/library/dao/trackdao.h +++ b/src/library/dao/trackdao.h @@ -199,7 +199,7 @@ class TrackDAO : public QObject, public virtual DAO { void bindTrackToTrackLocationsInsert(TrackInfoObject* pTrack); void bindTrackToLibraryInsert(TrackInfoObject* pTrack, int trackLocationId); - void writeAudioMetaData(TrackInfoObject* pTrack); + void writeMetadataToFile(TrackInfoObject* pTrack); QSqlDatabase& m_database; CueDAO& m_cueDao; diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index 54c6bef0d99..ae2091da31b 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -122,11 +122,14 @@ class TrackMetadata { // beats / minute static const double BPM_UNDEFINED = 0.0; - static const double BPM_MIN = 0.0; - static const double BPM_MAX = 300.0; + static const double BPM_MIN = 0.0; // exclusive lower bound + static const double BPM_MAX = 300.0; // inclusive upper bound inline double getBpm() const { return m_bpm; } + inline bool isBpmValid() const { + return (BPM_MIN < getBpm()) && (BPM_MAX >= getBpm()); + } inline void setBpm(double bpm) { m_bpm = bpm; } @@ -137,11 +140,14 @@ class TrackMetadata { bool setBpmString(const QString& sBpm); static const double REPLAYGAIN_UNDEFINED = 0.0f; - static const double REPLAYGAIN_MIN = 0.0f; + static const double REPLAYGAIN_MIN = 0.0f; // exclusive lower bound static const double REPLAYGAIN_0DB = 1.0f; inline float getReplayGain() const { return m_replayGain; } + inline bool isReplayGainValid() const { + return REPLAYGAIN_MIN < getReplayGain(); + } inline void setReplayGain(float replayGain) { m_replayGain = replayGain; } diff --git a/src/soundsourceproxy.cpp b/src/soundsourceproxy.cpp index 21016f09252..bf96ef10bfd 100644 --- a/src/soundsourceproxy.cpp +++ b/src/soundsourceproxy.cpp @@ -165,7 +165,11 @@ void SoundSourceProxy::loadPlugins() { // static Mixxx::SoundSourcePointer SoundSourceProxy::initialize(const QString& qFilename) { QString extension(qFilename); - extension = extension.remove(0, (qFilename.lastIndexOf(".") + 1)).toLower(); + extension = extension.remove(0, (qFilename.lastIndexOf(".") + 1)).toLower().trimmed(); + if (extension.isEmpty()) { + qWarning() << "Missing file extension:" << qFilename; + return Mixxx::SoundSourcePointer(); + } #ifdef __FFMPEGFILE__ return Mixxx::SoundSourcePointer(new SoundSourceFFmpeg(qFilename)); diff --git a/src/trackinfoobject.cpp b/src/trackinfoobject.cpp index 18d38fd7fe4..096c2f9d5ab 100644 --- a/src/trackinfoobject.cpp +++ b/src/trackinfoobject.cpp @@ -160,6 +160,87 @@ void TrackInfoObject::setDeleteOnReferenceExpiration(bool deleteOnReferenceExpir m_bDeleteOnReferenceExpiration = deleteOnReferenceExpiration; } +namespace { + // Parses artist/title from the file name and returns the file type. + // Assumes that the file name is written like: "artist - title.xxx" + // or "artist_-_title.xxx", + void parseMetadataFromFileName(Mixxx::TrackMetadata& trackMetadata, QString fileName) { + fileName.replace("_", " "); + QString titleWithFileType; + if (fileName.count('-') == 1) { + const QString artist(fileName.section('-', 0, 0).trimmed()); + if (!artist.isEmpty()) { + trackMetadata.setArtist(artist); + } + titleWithFileType = fileName.section('-', 1, 1).trimmed(); + } else { + titleWithFileType = fileName.trimmed(); + } + const QString title(titleWithFileType.section('.', 0, -2).trimmed()); + if (!title.isEmpty()) { + trackMetadata.setTitle(title); + } + } +} + +void TrackInfoObject::setMetadata(const Mixxx::TrackMetadata& trackMetadata) { + // TODO(XXX): This involves locking the mutex for every setXXX + // method. We should figure out an optimization where there are private + // setters that don't lock the mutex. + setArtist(trackMetadata.getArtist()); + setTitle(trackMetadata.getTitle()); + setAlbum(trackMetadata.getAlbum()); + setAlbumArtist(trackMetadata.getAlbumArtist()); + setYear(trackMetadata.getYear()); + setGenre(trackMetadata.getGenre()); + setComposer(trackMetadata.getComposer()); + setGrouping(trackMetadata.getGrouping()); + setComment(trackMetadata.getComment()); + setTrackNumber(trackMetadata.getTrackNumber()); + setChannels(trackMetadata.getChannels()); + setSampleRate(trackMetadata.getSampleRate()); + setDuration(trackMetadata.getDuration()); + setBitrate(trackMetadata.getBitrate()); + + if (trackMetadata.isReplayGainValid()) { + setReplayGain(trackMetadata.getReplayGain()); + } + + // Need to set BPM after sample rate since beat grid creation depends on + // knowing the sample rate. Bug #1020438. + if (trackMetadata.isBpmValid()) { + setBpm(trackMetadata.getBpm()); + } + + const QString key(trackMetadata.getKey()); + if (!key.isEmpty()) { + setKeyText(key, mixxx::track::io::key::FILE_METADATA); + } +} + +void TrackInfoObject::getMetadata(Mixxx::TrackMetadata* pTrackMetadata) { + // TODO(XXX): This involves locking the mutex for every setXXX + // method. We should figure out an optimization where there are private + // getters that don't lock the mutex. + pTrackMetadata->setArtist(getArtist()); + pTrackMetadata->setTitle(getTitle()); + pTrackMetadata->setAlbum(getAlbum()); + pTrackMetadata->setAlbumArtist(getAlbumArtist()); + pTrackMetadata->setYear(getYear()); + pTrackMetadata->setGenre(getGenre()); + pTrackMetadata->setComposer(getComposer()); + pTrackMetadata->setGrouping(getGrouping()); + pTrackMetadata->setComment(getComment()); + pTrackMetadata->setTrackNumber(getTrackNumber()); + pTrackMetadata->setChannels(getChannels()); + pTrackMetadata->setSampleRate(getSampleRate()); + pTrackMetadata->setDuration(getDuration()); + pTrackMetadata->setBitrate(getBitrate()); + pTrackMetadata->setReplayGain(getReplayGain()); + pTrackMetadata->setBpm(getBpm()); + pTrackMetadata->setKey(getKeyText()); +} + void TrackInfoObject::parse(bool parseCoverArt) { // Log parsing of header information in developer mode. This is useful for // tracking down corrupt files. @@ -171,62 +252,25 @@ void TrackInfoObject::parse(bool parseCoverArt) { SoundSourceProxy proxy(canonicalLocation, m_pSecurityToken); Mixxx::SoundSourcePointer pSoundSource(proxy.getSoundSource()); if (pSoundSource) { + // If we've got a SoundSource then it must have a type! + DEBUG_ASSERT(!pSoundSource->getType().isEmpty()); setType(pSoundSource->getType()); // Parse the information stored in the sound file. Mixxx::TrackMetadata trackMetadata; if (pSoundSource->parseMetadata(&trackMetadata) == OK) { - - // Dump the metadata extracted from the file into the track. - - // TODO(XXX): This involves locking the mutex for every setXXX - // method. We should figure out an optimization where there are private - // setters that don't lock the mutex. - // If Artist, Title and Type fields are not blank, modify them. // Otherwise, keep their current values. // TODO(rryan): Should we re-visit this decision? - if (!(trackMetadata.getArtist().isEmpty())) { - setArtist(trackMetadata.getArtist()); - } else { - parseArtist(); - } - - if (!(trackMetadata.getTitle().isEmpty())) { - setTitle(trackMetadata.getTitle()); - } else { - parseTitle(); - } - - setAlbum(trackMetadata.getAlbum()); - setAlbumArtist(trackMetadata.getAlbumArtist()); - setYear(trackMetadata.getYear()); - setGenre(trackMetadata.getGenre()); - setComposer(trackMetadata.getComposer()); - setGrouping(trackMetadata.getGrouping()); - setComment(trackMetadata.getComment()); - setTrackNumber(trackMetadata.getTrackNumber()); - setChannels(trackMetadata.getChannels()); - setSampleRate(trackMetadata.getSampleRate()); - setDuration(trackMetadata.getDuration()); - setBitrate(trackMetadata.getBitrate()); - - float replayGain = trackMetadata.getReplayGain(); - if (replayGain != 0.0f) { - setReplayGain(replayGain); - } - - // Need to set BPM after sample rate since beat grid creation depends on - // knowing the sample rate. Bug #1020438. - float bpm = trackMetadata.getBpm(); - if (bpm > 0) { - // do not delete beat grid if bpm is not set in file - setBpm(bpm); - } - - QString key = trackMetadata.getKey(); - if (!key.isEmpty()) { - setKeyText(key, mixxx::track::io::key::FILE_METADATA); + if (trackMetadata.getArtist().isEmpty() || trackMetadata.getTitle().isEmpty()) { + Mixxx::TrackMetadata fileNameMetadata; + parseMetadataFromFileName(fileNameMetadata, m_fileInfo.fileName()); + if (trackMetadata.getArtist().isEmpty()) { + trackMetadata.setArtist(fileNameMetadata.getArtist()); + } + if (trackMetadata.getTitle().isEmpty()) { + trackMetadata.setTitle(fileNameMetadata.getTitle()); + } } if (parseCoverArt) { @@ -244,11 +288,14 @@ void TrackInfoObject::parse(bool parseCoverArt) { } else { qDebug() << "TrackInfoObject::parse() error at file" << canonicalLocation; - setHeaderParsed(false); - // Add basic information derived from the filename: - parseFilename(); + // Add basic information derived from the filename + parseMetadataFromFileName(trackMetadata, m_fileInfo.fileName()); + + setHeaderParsed(false); } + // Dump the metadata extracted from the file into the track. + setMetadata(trackMetadata); } else { qDebug() << "TrackInfoObject::parse() error at file" << canonicalLocation; @@ -256,45 +303,6 @@ void TrackInfoObject::parse(bool parseCoverArt) { } } -void TrackInfoObject::parseArtist() { - QMutexLocker lock(&m_qMutex); - QString filename = m_fileInfo.fileName(); - filename = filename.replace("_", " "); - if (filename.count('-') == 1) { - m_sArtist = filename.section('-', 0, 0).trimmed(); - } - setDirty(true); -} - -void TrackInfoObject::parseTitle() { - QMutexLocker lock(&m_qMutex); - QString filename = m_fileInfo.fileName(); - filename = filename.replace("_", " "); - if (filename.count('-') == 1) { - m_sTitle = filename.section('-', 1, 1).trimmed(); - // Remove the file type from m_sTitle - m_sTitle = m_sTitle.section('.', 0, -2).trimmed(); - } else { - m_sTitle = filename.section('.', 0, -2).trimmed(); - } - setDirty(true); -} - -void TrackInfoObject::parseFilename() { - // If the file name has the following form: "Artist - Title.type", extract - // Artist, Title and type fields - parseArtist(); - parseTitle(); - - // Add no comment - m_sComment.clear(); - - // Find the type - QString filename = m_fileInfo.fileName(); - m_sType = filename.section(".",-1).toLower().trimmed(); - setDirty(true); -} - QString TrackInfoObject::getDurationStr() const { QMutexLocker lock(&m_qMutex); int iDuration = m_iDuration; @@ -768,13 +776,13 @@ int TrackInfoObject::getId() const { void TrackInfoObject::setId(int iId) { QMutexLocker lock(&m_qMutex); - // changing the Id does not make the track drity because the Id is always + // changing the Id does not make the track dirty because the Id is always // generated by the Database itself m_iId = iId; } -//TODO (vrince) remove clen-up when new summary is ready +//TODO (vrince) remove clean-up when new summary is ready /* const QByteArray *TrackInfoObject::getWaveSummary() { diff --git a/src/trackinfoobject.h b/src/trackinfoobject.h index 59202e42a52..bb23f31aee6 100644 --- a/src/trackinfoobject.h +++ b/src/trackinfoobject.h @@ -43,6 +43,10 @@ class TrackInfoObject; typedef QSharedPointer TrackPointer; typedef QWeakPointer TrackWeakPointer; +namespace Mixxx { + class TrackMetadata; +} + class TrackInfoObject : public QObject { Q_OBJECT public: @@ -307,11 +311,8 @@ class TrackInfoObject : public QObject { // Common initialization function between all TIO constructors. void initialize(bool parseHeader, bool parseCoverArt); - // Methods for parsing information from knowing only the file name. It - // assumes that the filename is written like: "artist - trackname.xxx" - void parseFilename(); - void parseArtist(); - void parseTitle(); + void setMetadata(const Mixxx::TrackMetadata& trackMetadata); + void getMetadata(Mixxx::TrackMetadata* pTrackMetadata); // Set whether the TIO is dirty not. This should never be called except by // TIO local methods or the TrackDAO. From 8fcd56afffc03a61ceee4e9672a44653e2116c1d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 6 Jan 2015 00:40:25 +0100 Subject: [PATCH 042/481] VorbisComment: Read COMMENT in addition to DESCRIPTION for compatibility --- src/metadata/trackmetadatataglib.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 0aaa814fcf1..a1e4fc18e07 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -250,6 +250,14 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme readTag(pTrackMetadata, tag); + // Some applications (like puddletag up to version 1.0.5) write COMMENT + // instead DESCRIPTION. If the comment field (correctly populated by TagLib + // from DESCRIPTION) is still empty we will additionally read this field. + // Reference: http://www.xiph.org/vorbis/doc/v-comment.html + if (pTrackMetadata->getComment().isEmpty() && tag.fieldListMap().contains("COMMENT")) { + pTrackMetadata->setComment(toQString(tag.fieldListMap()["COMMENT"])); + } + // Some tags use "BPM" so check for that. if (tag.fieldListMap().contains("BPM")) { pTrackMetadata->setBpmString(toQString(tag.fieldListMap()["BPM"])); From 570d6bdbc1d3ceb9d33a815c969e23cfbdbf1f03 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 6 Jan 2015 01:40:32 +0100 Subject: [PATCH 043/481] M4A: Increase prefetch count to avoid audible glitches when seeking --- plugins/soundsourcem4a/audiosourcem4a.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 2f2c3cedc7e..42ff1e3a035 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -32,8 +32,8 @@ const MP4SampleId kMinSampleId = 1; // Decoding will be restarted one or more blocks of samples // before the actual position to avoid audible glitches. -// One block of samples seems to be enough here. -const MP4SampleId kSampleIdPrefetchCount = 1; +// Two blocks of samples seems to be enough here. +const MP4SampleId kSampleIdPrefetchCount = 2; MP4TrackId findAacTrackId(MP4FileHandle hFile) { const MP4TrackId maxTrackId = MP4GetNumberOfTracks(hFile, NULL, 0); @@ -206,9 +206,15 @@ AudioSource::diff_type AudioSourceM4A::seekFrame(diff_type frameIndex) { return m_curFrameIndex; } if ((m_curSampleId != sampleId) || (frameIndex < m_curFrameIndex)) { - // restart decoding one or more blocks of samples - // backwards to avoid audible glitches - m_curSampleId = math_max(sampleId - kSampleIdPrefetchCount, kMinSampleId); + // Restart decoding one or more blocks of samples + // backwards to avoid audible glitches. + // Implementation note: The type MP4SampleId is unsigned so we + // have to be careful when subtracting! + if ((kMinSampleId + kSampleIdPrefetchCount) < sampleId) { + m_curSampleId = sampleId - kSampleIdPrefetchCount; + } else { + m_curSampleId = kMinSampleId; + } m_curFrameIndex = (m_curSampleId - kMinSampleId) * kFramesPerSampleBlock; // rryan 9/2009 -- the documentation is sketchy on this, but I think that // it tells the decoder that you are seeking so it should flush its state From 4df62edf40e0123f21495f15822c2e2e77990d23 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Wed, 7 Jan 2015 15:59:55 +0100 Subject: [PATCH 044/481] MP3: Simplify constant for int->float conversion --- src/sources/audiosourcemp3.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index c2fe73e7173..6f55eb5df1e 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -16,9 +16,7 @@ namespace const AudioSource::diff_type kMaxSkipFrameSamplesWhenSeeking = 2 * kSeekFramePrefetchCount * kMaxSamplesPerMp3Frame; const AudioSource::sample_type kMadScale = - AudioSource::kSampleValuePeak - / AudioSource::sample_type( - mad_fixed_t(1) << MAD_F_FRACBITS); + AudioSource::kSampleValuePeak / AudioSource::sample_type(MAD_F_ONE); inline AudioSource::sample_type madScale(mad_fixed_t sample) { return sample * kMadScale; From 8a0f4263971a6ff5a8382a8df166afd0ef636f6a Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Thu, 8 Jan 2015 23:35:14 +0100 Subject: [PATCH 045/481] Delete typedef Super (review comments) --- plugins/soundsourcem4a/audiosourcem4a.cpp | 2 +- plugins/soundsourcem4a/audiosourcem4a.h | 2 -- plugins/soundsourcem4a/soundsourcem4a.cpp | 2 +- plugins/soundsourcem4a/soundsourcem4a.h | 2 -- .../soundsourcemediafoundation/audiosourcemediafoundation.cpp | 2 +- plugins/soundsourcemediafoundation/audiosourcemediafoundation.h | 2 -- .../soundsourcemediafoundation/soundsourcemediafoundation.cpp | 2 +- plugins/soundsourcemediafoundation/soundsourcemediafoundation.h | 2 -- plugins/soundsourcewv/audiosourcewv.cpp | 2 +- plugins/soundsourcewv/audiosourcewv.h | 2 -- plugins/soundsourcewv/soundsourcewv.cpp | 2 +- plugins/soundsourcewv/soundsourcewv.h | 2 -- src/sources/audiosourcecoreaudio.h | 2 -- src/sources/audiosourceffmpeg.h | 2 -- src/sources/audiosourceflac.cpp | 2 +- src/sources/audiosourceflac.h | 2 -- src/sources/audiosourcemodplug.cpp | 2 +- src/sources/audiosourcemodplug.h | 2 -- src/sources/audiosourcemp3.cpp | 2 +- src/sources/audiosourcemp3.h | 2 -- src/sources/audiosourceoggvorbis.cpp | 2 +- src/sources/audiosourceoggvorbis.h | 2 -- src/sources/audiosourceopus.cpp | 2 +- src/sources/audiosourceopus.h | 2 -- src/sources/audiosourcesndfile.cpp | 2 +- src/sources/audiosourcesndfile.h | 2 -- src/sources/soundsourcecoreaudio.cpp | 2 +- src/sources/soundsourcecoreaudio.h | 2 -- src/sources/soundsourceffmpeg.h | 2 -- src/sources/soundsourceflac.cpp | 2 +- src/sources/soundsourceflac.h | 2 -- src/sources/soundsourcemodplug.cpp | 2 +- src/sources/soundsourcemodplug.h | 2 -- src/sources/soundsourcemp3.cpp | 2 +- src/sources/soundsourcemp3.h | 2 -- src/sources/soundsourceoggvorbis.cpp | 2 +- src/sources/soundsourceoggvorbis.h | 2 -- src/sources/soundsourceopus.cpp | 2 +- src/sources/soundsourceopus.h | 2 -- src/sources/soundsourcesndfile.cpp | 2 +- src/sources/soundsourcesndfile.h | 2 -- 41 files changed, 19 insertions(+), 63 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 42ff1e3a035..0921e168d82 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -191,7 +191,7 @@ void AudioSourceM4A::close() throw() { m_inputBufferOffset = 0; m_inputBufferLength = 0; m_curFrameIndex = 0; - Super::reset(); + reset(); } bool AudioSourceM4A::isValidSampleId(MP4SampleId sampleId) const { diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 007635a9f0d..941ccc03ab3 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -25,8 +25,6 @@ namespace Mixxx { class AudioSourceM4A : public AudioSource { - typedef AudioSource Super; - public: static AudioSourcePointer open(QString fileName); diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 9246c26c43f..e0c49649fbc 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -31,7 +31,7 @@ QList SoundSourceM4A::supportedFileExtensions() { } SoundSourceM4A::SoundSourceM4A(QString fileName) - : Super(fileName, "m4a") { + : SoundSource(fileName, "m4a") { } Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index c4874739a5e..659318fb3de 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -31,8 +31,6 @@ namespace Mixxx { class SoundSourceM4A : public SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index a91024ac893..95e1e012fac 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -158,7 +158,7 @@ void AudioSourceMediaFoundation::close() throw() { m_hrCoInitialize = E_FAIL; } - Super::reset(); + reset(); } Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekFrame( diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index d8f3afd8480..f6b4c5bf1c4 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -19,8 +19,6 @@ class IMFMediaSource; namespace Mixxx { class AudioSourceMediaFoundation : public AudioSource { - typedef AudioSource Super; - public: static AudioSourcePointer open(QString fileName); diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index b9369de6152..90c1a1d15f3 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -38,7 +38,7 @@ QList SoundSourceMediaFoundation::supportedFileExtensions() { } SoundSourceMediaFoundation::SoundSourceMediaFoundation(QString fileName) - : Super(fileName, "m4a") { + : SoundSource(fileName, "m4a") { } Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index b2ec7baf8fe..3b16826bc66 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -30,8 +30,6 @@ #endif class SoundSourceMediaFoundation : public Mixxx::SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 562a8c5a147..52b66d8cc37 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -53,7 +53,7 @@ void AudioSourceWV::close() throw() { WavpackCloseFile(m_wpc); m_wpc = NULL; } - Super::reset(); + reset(); } AudioSource::diff_type AudioSourceWV::seekFrame(diff_type frameIndex) { diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index d9fad4ded83..4aa4a0de248 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -15,8 +15,6 @@ namespace Mixxx { class AudioSourceWV: public AudioSource { - typedef AudioSource Super; - public: static AudioSourcePointer open(QString fileName); diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 60a3e692080..06d0afa7be4 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -14,7 +14,7 @@ QList SoundSourceWV::supportedFileExtensions() { } SoundSourceWV::SoundSourceWV(QString fileName) - : Super(fileName, "wv") { + : SoundSource(fileName, "wv") { } Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 68ddbcf614f..5f50dd41b82 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -13,8 +13,6 @@ namespace Mixxx { class SoundSourceWV: public SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index 34fe431ad51..39134e16078 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -22,8 +22,6 @@ namespace Mixxx { class AudioSourceCoreAudio : public AudioSource { - typedef AudioSource Super; - public: static AudioSourcePointer open(QString fileName); diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index 465b8698261..55a51715794 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -42,8 +42,6 @@ struct ffmpegCacheObject { }; class AudioSourceFFmpeg : public AudioSource { - typedef AudioSource Super; - public: static AudioSourcePointer open(QString fileName); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index bf756a69369..f73d54c6cf5 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -115,7 +115,7 @@ void AudioSourceFLAC::close() throw() { } m_decodeSampleBuffer.clear(); m_file.close(); - Super::reset(); + reset(); } Mixxx::AudioSource::diff_type AudioSourceFLAC::seekFrame(diff_type frameIndex) { diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 04befb5bcb3..5ffa76a4b61 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -14,8 +14,6 @@ namespace Mixxx { class AudioSourceFLAC : public AudioSource { - typedef AudioSource Super; - public: static AudioSourcePointer open(QString fileName); diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index 99a7f5b7b62..c52807458ab 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -131,7 +131,7 @@ void AudioSourceModPlug::close() throw() { ModPlug::ModPlug_Unload(m_pModFile); m_pModFile = NULL; } - Super::reset(); + reset(); } AudioSource::diff_type AudioSourceModPlug::seekFrame( diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index 951c5956625..16c6d55481d 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -17,8 +17,6 @@ namespace Mixxx // The whole file is decoded at once and saved // in RAM to allow seeking and smooth operation in Mixxx. class AudioSourceModPlug: public AudioSource { - typedef AudioSource Super; - public: static const size_type kChannelCount = 2; // always stereo static const size_type kFrameRate = 44100; // always 44.1kHz diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 6f55eb5df1e..cc0678322c7 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -235,7 +235,7 @@ void AudioSourceMp3::close() throw() { m_pFileData = NULL; m_file.close(); - Super::reset(); + reset(); } AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index cb29daa9b9a..29184370ff3 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -20,8 +20,6 @@ namespace Mixxx { class AudioSourceMp3 : public AudioSource { - typedef AudioSource Super; - public: static AudioSourcePointer open(QString fileName); diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 8669cce6ec1..6626f7066ca 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -62,7 +62,7 @@ void AudioSourceOggVorbis::close() throw() { if (0 != clearResult) { qWarning() << "Failed to close OggVorbis file" << clearResult; } - Super::reset(); + reset(); } AudioSource::diff_type AudioSourceOggVorbis::seekFrame( diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index 2f0cc847886..58063e5f294 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -11,8 +11,6 @@ namespace Mixxx { class AudioSourceOggVorbis: public AudioSource { - typedef AudioSource Super; - public: static AudioSourcePointer open(QString fileName); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 04d21ea7cbc..adeaafa6a30 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -58,7 +58,7 @@ void AudioSourceOpus::close() throw() { op_free(m_pOggOpusFile); m_pOggOpusFile = NULL; } - Super::reset(); + reset(); } AudioSource::diff_type AudioSourceOpus::seekFrame(diff_type frameIndex) { diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 50f1dc1d7a3..46ac7c66d4d 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -11,8 +11,6 @@ namespace Mixxx { class AudioSourceOpus: public AudioSource { - typedef AudioSource Super; - public: // All Opus audio is encoded at 48 kHz static const size_type kFrameRate = 48000; diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index 35b68254af6..3c380f56b9a 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -63,7 +63,7 @@ void AudioSourceSndFile::close() throw() { m_pSndFile = NULL; memset(&m_sfInfo, 0, sizeof(m_sfInfo)); } - Super::reset(); + reset(); } AudioSource::diff_type AudioSourceSndFile::seekFrame( diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 1545dce7109..428e7d1d2e2 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -16,8 +16,6 @@ namespace Mixxx { class AudioSourceSndFile: public AudioSource { - typedef AudioSource Super; - public: static AudioSourcePointer open(QString fileName); diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index e9f2f6e934e..d122b9668ec 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -37,7 +37,7 @@ QList SoundSourceCoreAudio::supportedFileExtensions() { } SoundSourceCoreAudio::SoundSourceCoreAudio(QString fileName) - : Super(fileName) { + : SoundSource(fileName) { } Result SoundSourceCoreAudio::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/sources/soundsourcecoreaudio.h b/src/sources/soundsourcecoreaudio.h index abe5cf95ccd..4850cdd0197 100644 --- a/src/sources/soundsourcecoreaudio.h +++ b/src/sources/soundsourcecoreaudio.h @@ -22,8 +22,6 @@ #include "sources/soundsource.h" class SoundSourceCoreAudio : public Mixxx::SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/src/sources/soundsourceffmpeg.h b/src/sources/soundsourceffmpeg.h index 017f08c591f..2056323f62f 100644 --- a/src/sources/soundsourceffmpeg.h +++ b/src/sources/soundsourceffmpeg.h @@ -21,8 +21,6 @@ #include "sources/soundsource.h" class SoundSourceFFmpeg : public Mixxx::SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index cd163800e82..5c883ddd2d1 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -29,7 +29,7 @@ QList SoundSourceFLAC::supportedFileExtensions() { } SoundSourceFLAC::SoundSourceFLAC(QString fileName) - : Super(fileName, "flac") { + : SoundSource(fileName, "flac") { } Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index 7a27997a629..4a1bd3b951f 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -21,8 +21,6 @@ #include "sources/soundsource.h" class SoundSourceFLAC : public Mixxx::SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index fafcb2fa20e..52346d99e90 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -48,7 +48,7 @@ QList SoundSourceModPlug::supportedFileExtensions() { } SoundSourceModPlug::SoundSourceModPlug(QString fileName) - : Super(fileName, getTypeFromFilename(fileName)) { + : SoundSource(fileName, getTypeFromFilename(fileName)) { } Result SoundSourceModPlug::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/sources/soundsourcemodplug.h b/src/sources/soundsourcemodplug.h index 5b66619efc9..0219147d08c 100644 --- a/src/sources/soundsourcemodplug.h +++ b/src/sources/soundsourcemodplug.h @@ -7,8 +7,6 @@ // The whole file is decoded at once and saved // in RAM to allow seeking and smooth operation in Mixxx. class SoundSourceModPlug: public Mixxx::SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 926c161d075..e030a3a82b7 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -28,7 +28,7 @@ QList SoundSourceMp3::supportedFileExtensions() { } SoundSourceMp3::SoundSourceMp3(QString qFilename) - : Super(qFilename, "mp3") { + : SoundSource(qFilename, "mp3") { } Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h index 74efc9ca5b7..33bf0800daf 100644 --- a/src/sources/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -25,8 +25,6 @@ */ class SoundSourceMp3 : public Mixxx::SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index 322e79fffc2..d4554149d2d 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -28,7 +28,7 @@ QList SoundSourceOggVorbis::supportedFileExtensions() { } SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) - : Super(qFilename, "ogg") { + : SoundSource(qFilename, "ogg") { } /* diff --git a/src/sources/soundsourceoggvorbis.h b/src/sources/soundsourceoggvorbis.h index 284a166de92..5a60a89a8d0 100644 --- a/src/sources/soundsourceoggvorbis.h +++ b/src/sources/soundsourceoggvorbis.h @@ -20,8 +20,6 @@ #include "sources/soundsource.h" class SoundSourceOggVorbis: public Mixxx::SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index e1faa6e49a6..048f38eb9df 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -15,7 +15,7 @@ QList SoundSourceOpus::supportedFileExtensions() { } SoundSourceOpus::SoundSourceOpus(QString qFilename) - : Super(qFilename, "opus") { + : SoundSource(qFilename, "opus") { } namespace diff --git a/src/sources/soundsourceopus.h b/src/sources/soundsourceopus.h index 8303dd97147..150eeb8027b 100644 --- a/src/sources/soundsourceopus.h +++ b/src/sources/soundsourceopus.h @@ -4,8 +4,6 @@ #include "sources/soundsource.h" class SoundSourceOpus: public Mixxx::SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 94c63fad90e..2a24c1ab85b 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -18,7 +18,7 @@ QList SoundSourceSndFile::supportedFileExtensions() { } SoundSourceSndFile::SoundSourceSndFile(QString qFilename) - : Super(qFilename) { + : SoundSource(qFilename) { } Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/sources/soundsourcesndfile.h b/src/sources/soundsourcesndfile.h index b2ffa518970..e027bc896a5 100644 --- a/src/sources/soundsourcesndfile.h +++ b/src/sources/soundsourcesndfile.h @@ -4,8 +4,6 @@ #include "sources/soundsource.h" class SoundSourceSndFile: public Mixxx::SoundSource { - typedef SoundSource Super; - public: static QList supportedFileExtensions(); From c539c5d6b06dda4fdf720a45656c68065cdd7977 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 12:33:21 +0100 Subject: [PATCH 046/481] Remove close() from public AudioSource interface (review comments) In sub-classes: - Visibility reduced from public to private - Renamed to preDestroy() --- plugins/soundsourcem4a/audiosourcem4a.cpp | 4 ++-- plugins/soundsourcem4a/audiosourcem4a.h | 3 +-- .../audiosourcemediafoundation.cpp | 4 ++-- .../soundsourcemediafoundation/audiosourcemediafoundation.h | 3 +-- plugins/soundsourcewv/audiosourcewv.cpp | 4 ++-- plugins/soundsourcewv/audiosourcewv.h | 3 +-- src/sources/audiosource.h | 6 ------ src/sources/audiosourcecoreaudio.cpp | 4 ++-- src/sources/audiosourcecoreaudio.h | 3 +-- src/sources/audiosourceffmpeg.cpp | 4 ++-- src/sources/audiosourceffmpeg.h | 3 +-- src/sources/audiosourceflac.cpp | 4 ++-- src/sources/audiosourceflac.h | 3 +-- src/sources/audiosourcemodplug.cpp | 4 ++-- src/sources/audiosourcemodplug.h | 3 +-- src/sources/audiosourcemp3.cpp | 4 ++-- src/sources/audiosourcemp3.h | 3 +-- src/sources/audiosourceoggvorbis.cpp | 4 ++-- src/sources/audiosourceoggvorbis.h | 3 +-- src/sources/audiosourceopus.cpp | 4 ++-- src/sources/audiosourceopus.h | 3 +-- src/sources/audiosourcesndfile.cpp | 4 ++-- src/sources/audiosourcesndfile.h | 3 +-- 23 files changed, 33 insertions(+), 50 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 0921e168d82..84b91468b55 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -73,7 +73,7 @@ AudioSourceM4A::AudioSourceM4A() } AudioSourceM4A::~AudioSourceM4A() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceM4A::open(QString fileName) { @@ -175,7 +175,7 @@ Result AudioSourceM4A::postConstruct(QString fileName) { return OK; } -void AudioSourceM4A::close() throw() { +void AudioSourceM4A::preDestroy() { if (m_hDecoder) { NeAACDecClose(m_hDecoder); m_hDecoder = NULL; diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 941ccc03ab3..c221020d920 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -34,12 +34,11 @@ class AudioSourceM4A : public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /* override*/; - private: AudioSourceM4A(); Result postConstruct(QString fileName); + void preDestroy(); bool isValidSampleId(MP4SampleId sampleId) const; diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index 95e1e012fac..fa29a377dda 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -78,7 +78,7 @@ AudioSourceMediaFoundation::AudioSourceMediaFoundation() } AudioSourceMediaFoundation::~AudioSourceMediaFoundation() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceMediaFoundation::open(QString fileName) { @@ -140,7 +140,7 @@ Result AudioSourceMediaFoundation::postConstruct(QString fileName) { return OK; } -void AudioSourceMediaFoundation::close() throw() { +void AudioSourceMediaFoundation::preDestroy() { delete[] m_wcFilename; m_wcFilename = NULL; delete[] m_leftoverBuffer; diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index f6b4c5bf1c4..d004898571c 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -28,12 +28,11 @@ class AudioSourceMediaFoundation : public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /* override*/; - private: AudioSourceMediaFoundation(); Result postConstruct(QString fileName); + void preDestroy(); bool configureAudioStream(); diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 52b66d8cc37..9bb43eb09d8 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -9,7 +9,7 @@ AudioSourceWV::AudioSourceWV() } AudioSourceWV::~AudioSourceWV() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceWV::open(QString fileName) { @@ -48,7 +48,7 @@ Result AudioSourceWV::postConstruct(QString fileName) { return OK; } -void AudioSourceWV::close() throw() { +void AudioSourceWV::preDestroy() { if (m_wpc) { WavpackCloseFile(m_wpc); m_wpc = NULL; diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index 4aa4a0de248..10b8f76bfeb 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -24,12 +24,11 @@ class AudioSourceWV: public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /* override*/; - private: AudioSourceWV(); Result postConstruct(QString fileName); + void preDestroy(); WavpackContext* m_wpc; diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index ec03547a02b..c21a38a4d67 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -160,12 +160,6 @@ class AudioSource { virtual size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer); - // Explicitly closes the audio sources. It is safe to - // close an audio source repeatedly. Should be invoked - // in the destructor of derived classes and therefore - // must not throw any exceptions - virtual void close() throw() = 0; - protected: AudioSource(); diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index b49708ff6fd..bfc1df4e738 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -16,7 +16,7 @@ AudioSourceCoreAudio::AudioSourceCoreAudio() } AudioSourceCoreAudio::~AudioSourceCoreAudio() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceCoreAudio::open(QString fileName) { @@ -117,7 +117,7 @@ Result AudioSourceCoreAudio::postConstruct(QString fileName) { return OK; } -void AudioSourceCoreAudio::close() throw() { +void AudioSourceCoreAudio::preDestroy() { ExtAudioFileDispose(m_audioFile); } diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index 39134e16078..e735c587ca7 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -31,12 +31,11 @@ class AudioSourceCoreAudio : public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /*override*/; - private: AudioSourceCoreAudio(); Result postConstruct(QString fileName); + void preDestroy(); ExtAudioFileRef m_audioFile; CAStreamBasicDescription m_inputFormat; diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 3f45b66dd66..6918cd02f0d 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -26,7 +26,7 @@ AudioSourceFFmpeg::AudioSourceFFmpeg() } AudioSourceFFmpeg::~AudioSourceFFmpeg() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceFFmpeg::open(QString fileName) { @@ -120,7 +120,7 @@ Result AudioSourceFFmpeg::postConstruct(QString fileName) { return OK; } -void AudioSourceFFmpeg::close() throw() { +void AudioSourceFFmpeg::preDestroy() { clearCache(); if (m_pCodecCtx != NULL) { diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index 55a51715794..9b48beeeb79 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -51,12 +51,11 @@ class AudioSourceFFmpeg : public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /*override*/; - private: AudioSourceFFmpeg(); Result postConstruct(QString fileName); + void preDestroy(); bool readFramesToCache(unsigned int count, qint64 offset); bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index f73d54c6cf5..c65e9d90214 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -63,7 +63,7 @@ AudioSourceFLAC::AudioSourceFLAC(QString fileName) } AudioSourceFLAC::~AudioSourceFLAC() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceFLAC::open(QString fileName) { @@ -107,7 +107,7 @@ Result AudioSourceFLAC::postConstruct() { return OK; } -void AudioSourceFLAC::close() throw() { +void AudioSourceFLAC::preDestroy() { if (m_decoder) { FLAC__stream_decoder_finish(m_decoder); FLAC__stream_decoder_delete(m_decoder); // frees memory diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 5ffa76a4b61..992fd86a1ef 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -24,8 +24,6 @@ class AudioSourceFLAC : public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /* override*/; - // callback methods FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t *bytes); FLAC__StreamDecoderSeekStatus flacSeek(FLAC__uint64 offset); @@ -40,6 +38,7 @@ class AudioSourceFLAC : public AudioSource { explicit AudioSourceFLAC(QString fileName); Result postConstruct(); + void preDestroy(); size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index c52807458ab..a041a5473c6 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -44,7 +44,7 @@ AudioSourceModPlug::AudioSourceModPlug() } AudioSourceModPlug::~AudioSourceModPlug() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceModPlug::open(QString fileName) { @@ -126,7 +126,7 @@ Result AudioSourceModPlug::postConstruct(QString fileName) { return OK; } -void AudioSourceModPlug::close() throw() { +void AudioSourceModPlug::preDestroy() { if (m_pModFile) { ModPlug::ModPlug_Unload(m_pModFile); m_pModFile = NULL; diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index 16c6d55481d..80512fdb8f2 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -33,14 +33,13 @@ class AudioSourceModPlug: public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /* override*/; - private: static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) AudioSourceModPlug(); Result postConstruct(QString fileName); + void preDestroy(); ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor unsigned long m_fileLength; // length of file in samples diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index cc0678322c7..0742355a658 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -52,7 +52,7 @@ AudioSourceMp3::AudioSourceMp3(QString fileName) } AudioSourceMp3::~AudioSourceMp3() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceMp3::open(QString fileName) { @@ -223,7 +223,7 @@ void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { m_curFrameIndex = seekFrame.frameIndex; } -void AudioSourceMp3::close() throw() { +void AudioSourceMp3::preDestroy() { finishDecoding(); m_seekFrameList.clear(); diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index 29184370ff3..12f98ea22a2 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -30,12 +30,11 @@ class AudioSourceMp3 : public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /*override*/; - private: explicit AudioSourceMp3(QString fileName); Result postConstruct(); + void preDestroy(); inline size_type skipFrameSamples(size_type frameCount) { return readFrameSamplesInterleaved(frameCount, NULL); diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 6626f7066ca..d4b01833b56 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -10,7 +10,7 @@ AudioSourceOggVorbis::AudioSourceOggVorbis() { } AudioSourceOggVorbis::~AudioSourceOggVorbis() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceOggVorbis::open(QString fileName) { @@ -57,7 +57,7 @@ Result AudioSourceOggVorbis::postConstruct(QString fileName) { return OK; } -void AudioSourceOggVorbis::close() throw() { +void AudioSourceOggVorbis::preDestroy() { const int clearResult = ov_clear(&m_vf); if (0 != clearResult) { qWarning() << "Failed to close OggVorbis file" << clearResult; diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index 58063e5f294..bd908e32356 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -21,12 +21,11 @@ class AudioSourceOggVorbis: public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /*override*/; - private: AudioSourceOggVorbis(); Result postConstruct(QString fileName); + void preDestroy(); size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index adeaafa6a30..59fb3c9a90f 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -8,7 +8,7 @@ AudioSourceOpus::AudioSourceOpus() } AudioSourceOpus::~AudioSourceOpus() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceOpus::open(QString fileName) { @@ -53,7 +53,7 @@ Result AudioSourceOpus::postConstruct(QString fileName) { return OK; } -void AudioSourceOpus::close() throw() { +void AudioSourceOpus::preDestroy() { if (m_pOggOpusFile) { op_free(m_pOggOpusFile); m_pOggOpusFile = NULL; diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 46ac7c66d4d..95504363921 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -24,12 +24,11 @@ class AudioSourceOpus: public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /*override*/; - private: AudioSourceOpus(); Result postConstruct(QString fileName); + void preDestroy(); OggOpusFile *m_pOggOpusFile; }; diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index 3c380f56b9a..0a825c405e9 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -10,7 +10,7 @@ AudioSourceSndFile::AudioSourceSndFile() } AudioSourceSndFile::~AudioSourceSndFile() { - close(); + preDestroy(); } AudioSourcePointer AudioSourceSndFile::open(QString fileName) { @@ -53,7 +53,7 @@ Result AudioSourceSndFile::postConstruct(QString fileName) { return OK; } -void AudioSourceSndFile::close() throw() { +void AudioSourceSndFile::preDestroy() { if (m_pSndFile) { const int closeResult = sf_close(m_pSndFile); if (0 != closeResult) { diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 428e7d1d2e2..5250cc98b56 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -25,12 +25,11 @@ class AudioSourceSndFile: public AudioSource { size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - void close() throw() /*override*/; - private: AudioSourceSndFile(); Result postConstruct(QString fileName); + void preDestroy(); SNDFILE* m_pSndFile; SF_INFO m_sfInfo; From 61d3b043e0ec7b11d055fe612e0dc40efe66cf10 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 12:34:09 +0100 Subject: [PATCH 047/481] Eliminate unnecessary plain pointer (review comments) --- plugins/soundsourcem4a/audiosourcem4a.cpp | 5 ++--- .../audiosourcemediafoundation.cpp | 5 ++--- plugins/soundsourcewv/audiosourcewv.cpp | 5 ++--- src/sources/audiosourcecoreaudio.cpp | 5 ++--- src/sources/audiosourceffmpeg.cpp | 5 ++--- src/sources/audiosourceflac.cpp | 5 ++--- src/sources/audiosourcemodplug.cpp | 5 ++--- src/sources/audiosourcemp3.cpp | 5 ++--- src/sources/audiosourceoggvorbis.cpp | 5 ++--- src/sources/audiosourceopus.cpp | 5 ++--- src/sources/audiosourcesndfile.cpp | 5 ++--- 11 files changed, 22 insertions(+), 33 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 84b91468b55..8154e24797d 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -77,9 +77,8 @@ AudioSourceM4A::~AudioSourceM4A() { } AudioSourcePointer AudioSourceM4A::open(QString fileName) { - AudioSourceM4A* pAudioSourceM4A(new AudioSourceM4A); - AudioSourcePointer pAudioSource(pAudioSourceM4A); // take ownership - if (OK == pAudioSourceM4A->postConstruct(fileName)) { + QSharedPointer pAudioSource(new AudioSourceM4A); + if (OK == pAudioSource->postConstruct(fileName)) { // success return pAudioSource; } else { diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index fa29a377dda..fd896a55ec4 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -82,9 +82,8 @@ AudioSourceMediaFoundation::~AudioSourceMediaFoundation() { } AudioSourcePointer AudioSourceMediaFoundation::open(QString fileName) { - AudioSourceMediaFoundation* pAudioSourceMediaFoundation(new AudioSourceMediaFoundation); - AudioSourcePointer pAudioSource(pAudioSourceMediaFoundation); // take ownership - if (OK == pAudioSourceMediaFoundation->postConstruct(fileName)) { + QSharedPointer pAudioSource(new AudioSourceMediaFoundation); + if (OK == pAudioSource->postConstruct(fileName)) { // success return pAudioSource; } else { diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 9bb43eb09d8..75b950704fe 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -13,9 +13,8 @@ AudioSourceWV::~AudioSourceWV() { } AudioSourcePointer AudioSourceWV::open(QString fileName) { - AudioSourceWV* pAudioSourceWV(new AudioSourceWV); - AudioSourcePointer pAudioSource(pAudioSourceWV); // take ownership - if (OK == pAudioSourceWV->postConstruct(fileName)) { + QSharedPointer pAudioSource(new AudioSourceWV); + if (OK == pAudioSource->postConstruct(fileName)) { // success return pAudioSource; } else { diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index bfc1df4e738..65d18ef70e9 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -20,9 +20,8 @@ AudioSourceCoreAudio::~AudioSourceCoreAudio() { } AudioSourcePointer AudioSourceCoreAudio::open(QString fileName) { - AudioSourceCoreAudio* pAudioSourceCoreAudio(new AudioSourceCoreAudio); - AudioSourcePointer pAudioSource(pAudioSourceCoreAudio); // take ownership - if (OK == pAudioSourceCoreAudio->postConstruct(fileName)) { + QSharedPointer pAudioSource(new AudioSourceCoreAudio); + if (OK == pAudioSource->postConstruct(fileName)) { // success return pAudioSource; } else { diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 6918cd02f0d..4680efa881f 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -30,9 +30,8 @@ AudioSourceFFmpeg::~AudioSourceFFmpeg() { } AudioSourcePointer AudioSourceFFmpeg::open(QString fileName) { - AudioSourceFFmpeg* pAudioSourceFFmpeg(new AudioSourceFFmpeg); - AudioSourcePointer pAudioSource(pAudioSourceFFmpeg); // take ownership - if (OK == pAudioSourceFFmpeg->postConstruct(fileName)) { + QSharedPointer pAudioSource(new AudioSourceFFmpeg); + if (OK == pAudioSource->postConstruct(fileName)) { // success return pAudioSource; } else { diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index c65e9d90214..11fe5ab7934 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -67,9 +67,8 @@ AudioSourceFLAC::~AudioSourceFLAC() { } AudioSourcePointer AudioSourceFLAC::open(QString fileName) { - AudioSourceFLAC* pAudioSourceFLAC(new AudioSourceFLAC(fileName)); - AudioSourcePointer pAudioSource(pAudioSourceFLAC); // take ownership - if (OK == pAudioSourceFLAC->postConstruct()) { + QSharedPointer pAudioSource(new AudioSourceFLAC(fileName)); + if (OK == pAudioSource->postConstruct()) { // success return pAudioSource; } else { diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index a041a5473c6..6dc2cdfb6d7 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -48,9 +48,8 @@ AudioSourceModPlug::~AudioSourceModPlug() { } AudioSourcePointer AudioSourceModPlug::open(QString fileName) { - AudioSourceModPlug* pAudioSourceModPlug(new AudioSourceModPlug); - AudioSourcePointer pAudioSource(pAudioSourceModPlug); // take ownership - if (OK == pAudioSourceModPlug->postConstruct(fileName)) { + QSharedPointer pAudioSource(new AudioSourceModPlug); + if (OK == pAudioSource->postConstruct(fileName)) { // success return pAudioSource; } else { diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 0742355a658..a2735862134 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -56,9 +56,8 @@ AudioSourceMp3::~AudioSourceMp3() { } AudioSourcePointer AudioSourceMp3::open(QString fileName) { - AudioSourceMp3* pAudioSourceMp3(new AudioSourceMp3(fileName)); - AudioSourcePointer pAudioSource(pAudioSourceMp3); // take ownership - if (OK == pAudioSourceMp3->postConstruct()) { + QSharedPointer pAudioSource(new AudioSourceMp3(fileName)); + if (OK == pAudioSource->postConstruct()) { // success return pAudioSource; } else { diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index d4b01833b56..a86bd4343b3 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -14,9 +14,8 @@ AudioSourceOggVorbis::~AudioSourceOggVorbis() { } AudioSourcePointer AudioSourceOggVorbis::open(QString fileName) { - AudioSourceOggVorbis* pAudioSourceOggVorbis(new AudioSourceOggVorbis); - AudioSourcePointer pAudioSource(pAudioSourceOggVorbis); // take ownership - if (OK == pAudioSourceOggVorbis->postConstruct(fileName)) { + QSharedPointer pAudioSource(new AudioSourceOggVorbis); + if (OK == pAudioSource->postConstruct(fileName)) { // success return pAudioSource; } else { diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 59fb3c9a90f..201e7f53f8c 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -12,9 +12,8 @@ AudioSourceOpus::~AudioSourceOpus() { } AudioSourcePointer AudioSourceOpus::open(QString fileName) { - AudioSourceOpus* pAudioSourceOpus(new AudioSourceOpus); - AudioSourcePointer pAudioSource(pAudioSourceOpus); // take ownership - if (OK == pAudioSourceOpus->postConstruct(fileName)) { + QSharedPointer pAudioSource(new AudioSourceOpus); + if (OK == pAudioSource->postConstruct(fileName)) { // success return pAudioSource; } else { diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index 0a825c405e9..ec54df6d2c8 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -14,9 +14,8 @@ AudioSourceSndFile::~AudioSourceSndFile() { } AudioSourcePointer AudioSourceSndFile::open(QString fileName) { - AudioSourceSndFile* pAudioSourceSndFile(new AudioSourceSndFile); - AudioSourcePointer pAudioSource(pAudioSourceSndFile); // take ownership - if (OK == pAudioSourceSndFile->postConstruct(fileName)) { + QSharedPointer pAudioSource(new AudioSourceSndFile); + if (OK == pAudioSource->postConstruct(fileName)) { // success return pAudioSource; } else { From dd76d6745326b92978fe02ce479cf4771b9c00cf Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 12:34:40 +0100 Subject: [PATCH 048/481] Rename static factory method of AudioSources: open() -> create() --- plugins/soundsourcem4a/audiosourcem4a.cpp | 2 +- plugins/soundsourcem4a/audiosourcem4a.h | 2 +- plugins/soundsourcem4a/soundsourcem4a.cpp | 2 +- .../soundsourcemediafoundation/audiosourcemediafoundation.cpp | 2 +- .../soundsourcemediafoundation/audiosourcemediafoundation.h | 2 +- .../soundsourcemediafoundation/soundsourcemediafoundation.cpp | 2 +- plugins/soundsourcewv/audiosourcewv.cpp | 2 +- plugins/soundsourcewv/audiosourcewv.h | 2 +- plugins/soundsourcewv/soundsourcewv.cpp | 2 +- src/sources/audiosourcecoreaudio.cpp | 2 +- src/sources/audiosourcecoreaudio.h | 2 +- src/sources/audiosourceffmpeg.cpp | 2 +- src/sources/audiosourceffmpeg.h | 2 +- src/sources/audiosourceflac.cpp | 2 +- src/sources/audiosourceflac.h | 2 +- src/sources/audiosourcemodplug.cpp | 4 ++-- src/sources/audiosourcemodplug.h | 2 +- src/sources/audiosourcemp3.cpp | 2 +- src/sources/audiosourcemp3.h | 2 +- src/sources/audiosourceoggvorbis.cpp | 2 +- src/sources/audiosourceoggvorbis.h | 2 +- src/sources/audiosourceopus.cpp | 2 +- src/sources/audiosourceopus.h | 2 +- src/sources/audiosourcesndfile.cpp | 2 +- src/sources/audiosourcesndfile.h | 2 +- src/sources/soundsourcecoreaudio.cpp | 2 +- src/sources/soundsourceffmpeg.cpp | 2 +- src/sources/soundsourceflac.cpp | 2 +- src/sources/soundsourcemodplug.cpp | 2 +- src/sources/soundsourcemp3.cpp | 2 +- src/sources/soundsourceoggvorbis.cpp | 2 +- src/sources/soundsourceopus.cpp | 2 +- src/sources/soundsourcesndfile.cpp | 2 +- 33 files changed, 34 insertions(+), 34 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 8154e24797d..460138a2439 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -76,7 +76,7 @@ AudioSourceM4A::~AudioSourceM4A() { preDestroy(); } -AudioSourcePointer AudioSourceM4A::open(QString fileName) { +AudioSourcePointer AudioSourceM4A::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceM4A); if (OK == pAudioSource->postConstruct(fileName)) { // success diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index c221020d920..101009cb916 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -26,7 +26,7 @@ namespace Mixxx { class AudioSourceM4A : public AudioSource { public: - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceM4A(); diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index e0c49649fbc..72a0c6b35f5 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -68,7 +68,7 @@ QImage SoundSourceM4A::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceM4A::open() const { - return Mixxx::AudioSourceM4A::open(getFilename()); + return Mixxx::AudioSourceM4A::create(getFilename()); } } // namespace Mixxx diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index fd896a55ec4..704397ace16 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -81,7 +81,7 @@ AudioSourceMediaFoundation::~AudioSourceMediaFoundation() { preDestroy(); } -AudioSourcePointer AudioSourceMediaFoundation::open(QString fileName) { +AudioSourcePointer AudioSourceMediaFoundation::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceMediaFoundation); if (OK == pAudioSource->postConstruct(fileName)) { // success diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index d004898571c..9d90416f751 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -20,7 +20,7 @@ namespace Mixxx { class AudioSourceMediaFoundation : public AudioSource { public: - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceMediaFoundation(); diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index 90c1a1d15f3..fc60d8d614e 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -76,5 +76,5 @@ QImage SoundSourceMediaFoundation::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceMediaFoundation::open() const { - return Mixxx::AudioSourceMediaFoundation::open(getFilename()); + return Mixxx::AudioSourceMediaFoundation::create(getFilename()); } diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 75b950704fe..7c39f24da72 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -12,7 +12,7 @@ AudioSourceWV::~AudioSourceWV() { preDestroy(); } -AudioSourcePointer AudioSourceWV::open(QString fileName) { +AudioSourcePointer AudioSourceWV::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceWV); if (OK == pAudioSource->postConstruct(fileName)) { // success diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index 10b8f76bfeb..2cc4ca5f2d6 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -16,7 +16,7 @@ namespace Mixxx { class AudioSourceWV: public AudioSource { public: - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceWV(); diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 06d0afa7be4..9cc424c647c 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -51,7 +51,7 @@ QImage SoundSourceWV::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceWV::open() const { - return Mixxx::AudioSourceWV::open(getFilename()); + return Mixxx::AudioSourceWV::create(getFilename()); } } // namespace Mixxx diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index 65d18ef70e9..aea968b22e8 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -19,7 +19,7 @@ AudioSourceCoreAudio::~AudioSourceCoreAudio() { preDestroy(); } -AudioSourcePointer AudioSourceCoreAudio::open(QString fileName) { +AudioSourcePointer AudioSourceCoreAudio::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceCoreAudio); if (OK == pAudioSource->postConstruct(fileName)) { // success diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index e735c587ca7..de077af5a58 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -23,7 +23,7 @@ namespace Mixxx { class AudioSourceCoreAudio : public AudioSource { public: - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceCoreAudio(); diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 4680efa881f..2402dbc9384 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -29,7 +29,7 @@ AudioSourceFFmpeg::~AudioSourceFFmpeg() { preDestroy(); } -AudioSourcePointer AudioSourceFFmpeg::open(QString fileName) { +AudioSourcePointer AudioSourceFFmpeg::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceFFmpeg); if (OK == pAudioSource->postConstruct(fileName)) { // success diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index 9b48beeeb79..29069fe0858 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -43,7 +43,7 @@ struct ffmpegCacheObject { class AudioSourceFFmpeg : public AudioSource { public: - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceFFmpeg(); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 11fe5ab7934..bdca779feab 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -66,7 +66,7 @@ AudioSourceFLAC::~AudioSourceFLAC() { preDestroy(); } -AudioSourcePointer AudioSourceFLAC::open(QString fileName) { +AudioSourcePointer AudioSourceFLAC::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceFLAC(fileName)); if (OK == pAudioSource->postConstruct()) { // success diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 992fd86a1ef..fb3bc1c4566 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -15,7 +15,7 @@ namespace Mixxx class AudioSourceFLAC : public AudioSource { public: - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceFLAC(); diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index 6dc2cdfb6d7..83219a85221 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -47,7 +47,7 @@ AudioSourceModPlug::~AudioSourceModPlug() { preDestroy(); } -AudioSourcePointer AudioSourceModPlug::open(QString fileName) { +AudioSourcePointer AudioSourceModPlug::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceModPlug); if (OK == pAudioSource->postConstruct(fileName)) { // success @@ -59,7 +59,7 @@ AudioSourcePointer AudioSourceModPlug::open(QString fileName) { } Result AudioSourceModPlug::postConstruct(QString fileName) { - ScopedTimer t("AudioSourceModPlug::open()"); + ScopedTimer t("AudioSourceModPlug::postConstruct()"); qDebug() << "[ModPlug] Loading ModPlug module " << fileName; diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index 80512fdb8f2..0e21aaca64f 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -25,7 +25,7 @@ class AudioSourceModPlug: public AudioSource { static void configure(unsigned int bufferSizeLimit, const ModPlug::ModPlug_Settings &settings); - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceModPlug(); diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index a2735862134..7c02678c279 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -55,7 +55,7 @@ AudioSourceMp3::~AudioSourceMp3() { preDestroy(); } -AudioSourcePointer AudioSourceMp3::open(QString fileName) { +AudioSourcePointer AudioSourceMp3::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceMp3(fileName)); if (OK == pAudioSource->postConstruct()) { // success diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index 12f98ea22a2..dbae70e51a7 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -21,7 +21,7 @@ namespace Mixxx class AudioSourceMp3 : public AudioSource { public: - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceMp3(); diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index a86bd4343b3..adb654030cf 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -13,7 +13,7 @@ AudioSourceOggVorbis::~AudioSourceOggVorbis() { preDestroy(); } -AudioSourcePointer AudioSourceOggVorbis::open(QString fileName) { +AudioSourcePointer AudioSourceOggVorbis::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceOggVorbis); if (OK == pAudioSource->postConstruct(fileName)) { // success diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index bd908e32356..90c68ed7ed1 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -12,7 +12,7 @@ namespace Mixxx class AudioSourceOggVorbis: public AudioSource { public: - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceOggVorbis(); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 201e7f53f8c..7aaeddee614 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -11,7 +11,7 @@ AudioSourceOpus::~AudioSourceOpus() { preDestroy(); } -AudioSourcePointer AudioSourceOpus::open(QString fileName) { +AudioSourcePointer AudioSourceOpus::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceOpus); if (OK == pAudioSource->postConstruct(fileName)) { // success diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 95504363921..c941caa5e29 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -15,7 +15,7 @@ class AudioSourceOpus: public AudioSource { // All Opus audio is encoded at 48 kHz static const size_type kFrameRate = 48000; - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceOpus(); diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index ec54df6d2c8..d16c0608bd7 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -13,7 +13,7 @@ AudioSourceSndFile::~AudioSourceSndFile() { preDestroy(); } -AudioSourcePointer AudioSourceSndFile::open(QString fileName) { +AudioSourcePointer AudioSourceSndFile::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceSndFile); if (OK == pAudioSource->postConstruct(fileName)) { // success diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 5250cc98b56..499ff0a46e3 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -17,7 +17,7 @@ namespace Mixxx class AudioSourceSndFile: public AudioSource { public: - static AudioSourcePointer open(QString fileName); + static AudioSourcePointer create(QString fileName); ~AudioSourceSndFile(); diff --git a/src/sources/soundsourcecoreaudio.cpp b/src/sources/soundsourcecoreaudio.cpp index d122b9668ec..c6a329452c0 100644 --- a/src/sources/soundsourcecoreaudio.cpp +++ b/src/sources/soundsourcecoreaudio.cpp @@ -117,5 +117,5 @@ QImage SoundSourceCoreAudio::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceCoreAudio::open() const { - return Mixxx::AudioSourceCoreAudio::open(getFilename()); + return Mixxx::AudioSourceCoreAudio::create(getFilename()); } diff --git a/src/sources/soundsourceffmpeg.cpp b/src/sources/soundsourceffmpeg.cpp index f42f7e14e2e..a5ed7c930df 100644 --- a/src/sources/soundsourceffmpeg.cpp +++ b/src/sources/soundsourceffmpeg.cpp @@ -189,5 +189,5 @@ QImage SoundSourceFFmpeg::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceFFmpeg::open() const { - return Mixxx::AudioSourceFFmpeg::open(getFilename()); + return Mixxx::AudioSourceFFmpeg::create(getFilename()); } diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 5c883ddd2d1..4efffe8423a 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -86,5 +86,5 @@ QImage SoundSourceFLAC::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceFLAC::open() const { - return Mixxx::AudioSourceFLAC::open(getFilename()); + return Mixxx::AudioSourceFLAC::create(getFilename()); } diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index 52346d99e90..f817e6b2b94 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -76,5 +76,5 @@ QImage SoundSourceModPlug::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceModPlug::open() const { - return Mixxx::AudioSourceModPlug::open(getFilename()); + return Mixxx::AudioSourceModPlug::create(getFilename()); } diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index e030a3a82b7..d0341360397 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -77,5 +77,5 @@ QImage SoundSourceMp3::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceMp3::open() const { - return Mixxx::AudioSourceMp3::open(getFilename()); + return Mixxx::AudioSourceMp3::create(getFilename()); } diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index d4554149d2d..0853221c6d2 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -68,5 +68,5 @@ QImage SoundSourceOggVorbis::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceOggVorbis::open() const { - return Mixxx::AudioSourceOggVorbis::open(getFilename()); + return Mixxx::AudioSourceOggVorbis::create(getFilename()); } diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 048f38eb9df..0af4641c91d 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -129,5 +129,5 @@ QImage SoundSourceOpus::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceOpus::open() const { - return Mixxx::AudioSourceOpus::open(getFilename()); + return Mixxx::AudioSourceOpus::create(getFilename()); } diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 2a24c1ab85b..88db535a2fe 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -123,5 +123,5 @@ QImage SoundSourceSndFile::parseCoverArt() const { } Mixxx::AudioSourcePointer SoundSourceSndFile::open() const { - return Mixxx::AudioSourceSndFile::open(getFilename()); + return Mixxx::AudioSourceSndFile::create(getFilename()); } From d81fb59ccf3ee137022df2f3e1999d623c75754b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 12:35:14 +0100 Subject: [PATCH 049/481] Rename postConstruct() and preDestroy( ) methods --- plugins/soundsourcem4a/audiosourcem4a.cpp | 8 ++++---- plugins/soundsourcem4a/audiosourcem4a.h | 5 +++-- .../audiosourcemediafoundation.cpp | 8 ++++---- .../audiosourcemediafoundation.h | 5 +++-- plugins/soundsourcewv/audiosourcewv.cpp | 8 ++++---- plugins/soundsourcewv/audiosourcewv.h | 5 +++-- src/sources/audiosourcecoreaudio.cpp | 8 ++++---- src/sources/audiosourcecoreaudio.h | 5 +++-- src/sources/audiosourceffmpeg.cpp | 8 ++++---- src/sources/audiosourceffmpeg.h | 5 +++-- src/sources/audiosourceflac.cpp | 8 ++++---- src/sources/audiosourceflac.h | 5 +++-- src/sources/audiosourcemodplug.cpp | 10 +++++----- src/sources/audiosourcemodplug.h | 5 +++-- src/sources/audiosourcemp3.cpp | 8 ++++---- src/sources/audiosourcemp3.h | 5 +++-- src/sources/audiosourceoggvorbis.cpp | 8 ++++---- src/sources/audiosourceoggvorbis.h | 5 +++-- src/sources/audiosourceopus.cpp | 8 ++++---- src/sources/audiosourceopus.h | 5 +++-- src/sources/audiosourcesndfile.cpp | 8 ++++---- src/sources/audiosourcesndfile.h | 5 +++-- 22 files changed, 78 insertions(+), 67 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 460138a2439..f2d58793ed7 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -73,12 +73,12 @@ AudioSourceM4A::AudioSourceM4A() } AudioSourceM4A::~AudioSourceM4A() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceM4A::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceM4A); - if (OK == pAudioSource->postConstruct(fileName)) { + if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; } else { @@ -87,7 +87,7 @@ AudioSourcePointer AudioSourceM4A::create(QString fileName) { } } -Result AudioSourceM4A::postConstruct(QString fileName) { +Result AudioSourceM4A::open(QString fileName) { /* open MP4 file, check for >= ver 1.9.1 */ #if MP4V2_PROJECT_version_hex <= 0x00010901 m_hFile = MP4Read(fileName.toLocal8Bit().constData(), 0); @@ -174,7 +174,7 @@ Result AudioSourceM4A::postConstruct(QString fileName) { return OK; } -void AudioSourceM4A::preDestroy() { +void AudioSourceM4A::close() { if (m_hDecoder) { NeAACDecClose(m_hDecoder); m_hDecoder = NULL; diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 101009cb916..83a554ba047 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -37,8 +37,9 @@ class AudioSourceM4A : public AudioSource { private: AudioSourceM4A(); - Result postConstruct(QString fileName); - void preDestroy(); + Result open(QString fileName); + + void close(); bool isValidSampleId(MP4SampleId sampleId) const; diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index 704397ace16..3f9560c0c9d 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -78,12 +78,12 @@ AudioSourceMediaFoundation::AudioSourceMediaFoundation() } AudioSourceMediaFoundation::~AudioSourceMediaFoundation() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceMediaFoundation::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceMediaFoundation); - if (OK == pAudioSource->postConstruct(fileName)) { + if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; } else { @@ -92,7 +92,7 @@ AudioSourcePointer AudioSourceMediaFoundation::create(QString fileName) { } } -Result AudioSourceMediaFoundation::postConstruct(QString fileName) { +Result AudioSourceMediaFoundation::open(QString fileName) { if (sDebug) { qDebug() << "open()" << fileName; } @@ -139,7 +139,7 @@ Result AudioSourceMediaFoundation::postConstruct(QString fileName) { return OK; } -void AudioSourceMediaFoundation::preDestroy() { +void AudioSourceMediaFoundation::close() { delete[] m_wcFilename; m_wcFilename = NULL; delete[] m_leftoverBuffer; diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index 9d90416f751..761e96e293e 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -31,8 +31,9 @@ class AudioSourceMediaFoundation : public AudioSource { private: AudioSourceMediaFoundation(); - Result postConstruct(QString fileName); - void preDestroy(); + Result open(QString fileName); + + void close(); bool configureAudioStream(); diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 7c39f24da72..96fff3f045f 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -9,12 +9,12 @@ AudioSourceWV::AudioSourceWV() } AudioSourceWV::~AudioSourceWV() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceWV::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceWV); - if (OK == pAudioSource->postConstruct(fileName)) { + if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; } else { @@ -23,7 +23,7 @@ AudioSourcePointer AudioSourceWV::create(QString fileName) { } } -Result AudioSourceWV::postConstruct(QString fileName) { +Result AudioSourceWV::open(QString fileName) { char msg[80]; // hold possible error message m_wpc = WavpackOpenFileInput(fileName.toLocal8Bit().constData(), msg, OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); @@ -47,7 +47,7 @@ Result AudioSourceWV::postConstruct(QString fileName) { return OK; } -void AudioSourceWV::preDestroy() { +void AudioSourceWV::close() { if (m_wpc) { WavpackCloseFile(m_wpc); m_wpc = NULL; diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index 2cc4ca5f2d6..0181f87349b 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -27,8 +27,9 @@ class AudioSourceWV: public AudioSource { private: AudioSourceWV(); - Result postConstruct(QString fileName); - void preDestroy(); + Result open(QString fileName); + + void close(); WavpackContext* m_wpc; diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index aea968b22e8..1bca68ee438 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -16,12 +16,12 @@ AudioSourceCoreAudio::AudioSourceCoreAudio() } AudioSourceCoreAudio::~AudioSourceCoreAudio() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceCoreAudio::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceCoreAudio); - if (OK == pAudioSource->postConstruct(fileName)) { + if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; } else { @@ -31,7 +31,7 @@ AudioSourcePointer AudioSourceCoreAudio::create(QString fileName) { } // soundsource overrides -Result AudioSourceCoreAudio::postConstruct(QString fileName) { +Result AudioSourceCoreAudio::open(QString fileName) { //Open the audio file. OSStatus err; @@ -116,7 +116,7 @@ Result AudioSourceCoreAudio::postConstruct(QString fileName) { return OK; } -void AudioSourceCoreAudio::preDestroy() { +void AudioSourceCoreAudio::close() { ExtAudioFileDispose(m_audioFile); } diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index de077af5a58..7bf24fc74cc 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -34,8 +34,9 @@ class AudioSourceCoreAudio : public AudioSource { private: AudioSourceCoreAudio(); - Result postConstruct(QString fileName); - void preDestroy(); + Result open(QString fileName); + + void close(); ExtAudioFileRef m_audioFile; CAStreamBasicDescription m_inputFormat; diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 2402dbc9384..33f71ebd00c 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -26,12 +26,12 @@ AudioSourceFFmpeg::AudioSourceFFmpeg() } AudioSourceFFmpeg::~AudioSourceFFmpeg() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceFFmpeg::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceFFmpeg); - if (OK == pAudioSource->postConstruct(fileName)) { + if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; } else { @@ -40,7 +40,7 @@ AudioSourcePointer AudioSourceFFmpeg::create(QString fileName) { } } -Result AudioSourceFFmpeg::postConstruct(QString fileName) { +Result AudioSourceFFmpeg::open(QString fileName) { unsigned int i; AVDictionary *l_iFormatOpts = NULL; @@ -119,7 +119,7 @@ Result AudioSourceFFmpeg::postConstruct(QString fileName) { return OK; } -void AudioSourceFFmpeg::preDestroy() { +void AudioSourceFFmpeg::close() { clearCache(); if (m_pCodecCtx != NULL) { diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index 29069fe0858..afac0395e6a 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -54,8 +54,9 @@ class AudioSourceFFmpeg : public AudioSource { private: AudioSourceFFmpeg(); - Result postConstruct(QString fileName); - void preDestroy(); + Result open(QString fileName); + + void close(); bool readFramesToCache(unsigned int count, qint64 offset); bool getBytesFromCache(char *buffer, quint64 offset, quint64 size); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index bdca779feab..88cec79f269 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -63,12 +63,12 @@ AudioSourceFLAC::AudioSourceFLAC(QString fileName) } AudioSourceFLAC::~AudioSourceFLAC() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceFLAC::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceFLAC(fileName)); - if (OK == pAudioSource->postConstruct()) { + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -77,7 +77,7 @@ AudioSourcePointer AudioSourceFLAC::create(QString fileName) { } } -Result AudioSourceFLAC::postConstruct() { +Result AudioSourceFLAC::open() { if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "SSFLAC: Could not read file!"; return ERR; @@ -106,7 +106,7 @@ Result AudioSourceFLAC::postConstruct() { return OK; } -void AudioSourceFLAC::preDestroy() { +void AudioSourceFLAC::close() { if (m_decoder) { FLAC__stream_decoder_finish(m_decoder); FLAC__stream_decoder_delete(m_decoder); // frees memory diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index fb3bc1c4566..98bf11074f4 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -37,8 +37,9 @@ class AudioSourceFLAC : public AudioSource { private: explicit AudioSourceFLAC(QString fileName); - Result postConstruct(); - void preDestroy(); + Result open(); + + void close(); size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index 83219a85221..d3b4c2b00aa 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -44,12 +44,12 @@ AudioSourceModPlug::AudioSourceModPlug() } AudioSourceModPlug::~AudioSourceModPlug() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceModPlug::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceModPlug); - if (OK == pAudioSource->postConstruct(fileName)) { + if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; } else { @@ -58,8 +58,8 @@ AudioSourcePointer AudioSourceModPlug::create(QString fileName) { } } -Result AudioSourceModPlug::postConstruct(QString fileName) { - ScopedTimer t("AudioSourceModPlug::postConstruct()"); +Result AudioSourceModPlug::open(QString fileName) { + ScopedTimer t("AudioSourceModPlug::open()"); qDebug() << "[ModPlug] Loading ModPlug module " << fileName; @@ -125,7 +125,7 @@ Result AudioSourceModPlug::postConstruct(QString fileName) { return OK; } -void AudioSourceModPlug::preDestroy() { +void AudioSourceModPlug::close() { if (m_pModFile) { ModPlug::ModPlug_Unload(m_pModFile); m_pModFile = NULL; diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index 0e21aaca64f..359ca7cef25 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -38,8 +38,9 @@ class AudioSourceModPlug: public AudioSource { AudioSourceModPlug(); - Result postConstruct(QString fileName); - void preDestroy(); + Result open(QString fileName); + + void close(); ModPlug::ModPlugFile *m_pModFile; // modplug file descriptor unsigned long m_fileLength; // length of file in samples diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 7c02678c279..43c2b49d91e 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -52,12 +52,12 @@ AudioSourceMp3::AudioSourceMp3(QString fileName) } AudioSourceMp3::~AudioSourceMp3() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceMp3::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceMp3(fileName)); - if (OK == pAudioSource->postConstruct()) { + if (OK == pAudioSource->open()) { // success return pAudioSource; } else { @@ -66,7 +66,7 @@ AudioSourcePointer AudioSourceMp3::create(QString fileName) { } } -Result AudioSourceMp3::postConstruct() { +Result AudioSourceMp3::open() { if (!m_file.open(QIODevice::ReadOnly)) { qWarning() << "Failed to open file:" << m_file.fileName(); return ERR; @@ -222,7 +222,7 @@ void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { m_curFrameIndex = seekFrame.frameIndex; } -void AudioSourceMp3::preDestroy() { +void AudioSourceMp3::close() { finishDecoding(); m_seekFrameList.clear(); diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index dbae70e51a7..c3cbca387cc 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -33,8 +33,9 @@ class AudioSourceMp3 : public AudioSource { private: explicit AudioSourceMp3(QString fileName); - Result postConstruct(); - void preDestroy(); + Result open(); + + void close(); inline size_type skipFrameSamples(size_type frameCount) { return readFrameSamplesInterleaved(frameCount, NULL); diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index adb654030cf..e0eb160461e 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -10,12 +10,12 @@ AudioSourceOggVorbis::AudioSourceOggVorbis() { } AudioSourceOggVorbis::~AudioSourceOggVorbis() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceOggVorbis::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceOggVorbis); - if (OK == pAudioSource->postConstruct(fileName)) { + if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; } else { @@ -24,7 +24,7 @@ AudioSourcePointer AudioSourceOggVorbis::create(QString fileName) { } } -Result AudioSourceOggVorbis::postConstruct(QString fileName) { +Result AudioSourceOggVorbis::open(QString fileName) { const QByteArray qbaFilename(fileName.toLocal8Bit()); if (0 != ov_fopen(qbaFilename.constData(), &m_vf)) { qWarning() << "Failed to open OggVorbis file:" << fileName; @@ -56,7 +56,7 @@ Result AudioSourceOggVorbis::postConstruct(QString fileName) { return OK; } -void AudioSourceOggVorbis::preDestroy() { +void AudioSourceOggVorbis::close() { const int clearResult = ov_clear(&m_vf); if (0 != clearResult) { qWarning() << "Failed to close OggVorbis file" << clearResult; diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index 90c68ed7ed1..374e0837948 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -24,8 +24,9 @@ class AudioSourceOggVorbis: public AudioSource { private: AudioSourceOggVorbis(); - Result postConstruct(QString fileName); - void preDestroy(); + Result open(QString fileName); + + void close(); size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 7aaeddee614..dc51f210e14 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -8,12 +8,12 @@ AudioSourceOpus::AudioSourceOpus() } AudioSourceOpus::~AudioSourceOpus() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceOpus::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceOpus); - if (OK == pAudioSource->postConstruct(fileName)) { + if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; } else { @@ -22,7 +22,7 @@ AudioSourcePointer AudioSourceOpus::create(QString fileName) { } } -Result AudioSourceOpus::postConstruct(QString fileName) { +Result AudioSourceOpus::open(QString fileName) { int errorCode = 0; const QByteArray qbaFilename(fileName.toLocal8Bit()); @@ -52,7 +52,7 @@ Result AudioSourceOpus::postConstruct(QString fileName) { return OK; } -void AudioSourceOpus::preDestroy() { +void AudioSourceOpus::close() { if (m_pOggOpusFile) { op_free(m_pOggOpusFile); m_pOggOpusFile = NULL; diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index c941caa5e29..11b16144d22 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -27,8 +27,9 @@ class AudioSourceOpus: public AudioSource { private: AudioSourceOpus(); - Result postConstruct(QString fileName); - void preDestroy(); + Result open(QString fileName); + + void close(); OggOpusFile *m_pOggOpusFile; }; diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index d16c0608bd7..e39c87fb761 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -10,12 +10,12 @@ AudioSourceSndFile::AudioSourceSndFile() } AudioSourceSndFile::~AudioSourceSndFile() { - preDestroy(); + close(); } AudioSourcePointer AudioSourceSndFile::create(QString fileName) { QSharedPointer pAudioSource(new AudioSourceSndFile); - if (OK == pAudioSource->postConstruct(fileName)) { + if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; } else { @@ -24,7 +24,7 @@ AudioSourcePointer AudioSourceSndFile::create(QString fileName) { } } -Result AudioSourceSndFile::postConstruct(QString fileName) { +Result AudioSourceSndFile::open(QString fileName) { #ifdef __WINDOWS__ // Pointer valid until string changed LPCWSTR lpcwFilename = (LPCWSTR)fileName.utf16(); @@ -52,7 +52,7 @@ Result AudioSourceSndFile::postConstruct(QString fileName) { return OK; } -void AudioSourceSndFile::preDestroy() { +void AudioSourceSndFile::close() { if (m_pSndFile) { const int closeResult = sf_close(m_pSndFile); if (0 != closeResult) { diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 499ff0a46e3..23995a01b5e 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -28,8 +28,9 @@ class AudioSourceSndFile: public AudioSource { private: AudioSourceSndFile(); - Result postConstruct(QString fileName); - void preDestroy(); + Result open(QString fileName); + + void close(); SNDFILE* m_pSndFile; SF_INFO m_sfInfo; From 4998f1833892fe73a2f164c76dfd56c19bdf3bd4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 12:35:49 +0100 Subject: [PATCH 050/481] Improve interface and documentation of AudioSource (review comments) --- plugins/soundsourcem4a/audiosourcem4a.cpp | 14 +++--- plugins/soundsourcem4a/audiosourcem4a.h | 4 +- .../audiosourcemediafoundation.cpp | 24 +++++------ .../audiosourcemediafoundation.h | 4 +- plugins/soundsourcewv/audiosourcewv.cpp | 8 ++-- plugins/soundsourcewv/audiosourcewv.h | 4 +- src/analyserqueue.cpp | 2 +- src/cachingreaderworker.cpp | 4 +- src/musicbrainz/chromaprinter.cpp | 2 +- src/sources/audiosource.cpp | 16 +++---- src/sources/audiosource.h | 43 ++++++++++++------- src/sources/audiosourcecoreaudio.cpp | 10 ++--- src/sources/audiosourcecoreaudio.h | 4 +- src/sources/audiosourceffmpeg.cpp | 8 ++-- src/sources/audiosourceffmpeg.h | 4 +- src/sources/audiosourceflac.cpp | 22 +++++----- src/sources/audiosourceflac.h | 8 ++-- src/sources/audiosourcemodplug.cpp | 8 ++-- src/sources/audiosourcemodplug.h | 4 +- src/sources/audiosourcemp3.cpp | 24 +++++------ src/sources/audiosourcemp3.h | 12 +++--- src/sources/audiosourceoggvorbis.cpp | 22 +++++----- src/sources/audiosourceoggvorbis.h | 8 ++-- src/sources/audiosourceopus.cpp | 18 ++++---- src/sources/audiosourceopus.h | 6 +-- src/sources/audiosourcesndfile.cpp | 8 ++-- src/sources/audiosourcesndfile.h | 4 +- 27 files changed, 153 insertions(+), 142 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index f2d58793ed7..dea7ef2bd86 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -166,7 +166,7 @@ Result AudioSourceM4A::open(QString fileName) { // invalidate current frame index m_curFrameIndex = getFrameCount(); // seek to beginning of file - if (0 != seekFrame(0)) { + if (0 != seekSampleFrame(0)) { qWarning() << "Failed to seek to the beginning of the file!"; return ERR; } @@ -197,7 +197,7 @@ bool AudioSourceM4A::isValidSampleId(MP4SampleId sampleId) const { return (sampleId >= kMinSampleId) && (sampleId <= m_maxSampleId); } -AudioSource::diff_type AudioSourceM4A::seekFrame(diff_type frameIndex) { +AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { if (m_curFrameIndex != frameIndex) { const MP4SampleId sampleId = kMinSampleId + (frameIndex / kFramesPerSampleBlock); @@ -227,18 +227,18 @@ AudioSource::diff_type AudioSourceM4A::seekFrame(diff_type frameIndex) { const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; // prefetch (decode and discard) all samples up to the target position DEBUG_ASSERT(frames2samples(prefetchFrameCount) <= m_prefetchSampleBuffer.size()); - readFrameSamplesInterleaved(prefetchFrameCount, &m_prefetchSampleBuffer[0]); + readSampleFrames(prefetchFrameCount, &m_prefetchSampleBuffer[0]); } DEBUG_ASSERT(m_curFrameIndex == frameIndex); return frameIndex; } -AudioSource::size_type AudioSourceM4A::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { +AudioSource::size_type AudioSourceM4A::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { sample_type* pSampleBuffer = sampleBuffer; const diff_type readFrameIndex = m_curFrameIndex; size_type readFrameCount = m_curFrameIndex - readFrameIndex; - while ((readFrameCount = m_curFrameIndex - readFrameIndex) < frameCount) { + while ((readFrameCount = m_curFrameIndex - readFrameIndex) < numberOfFrames) { if (isValidSampleId(m_curSampleId) && (0 == m_inputBufferLength)) { // fill input buffer with block of samples InputBuffer::value_type* pInputBuffer = &m_inputBuffer[0]; @@ -261,7 +261,7 @@ AudioSource::size_type AudioSourceM4A::readFrameSamplesInterleaved( // decode samples into sampleBuffer const size_type readFrameCount = m_curFrameIndex - readFrameIndex; const size_type decodeBufferCapacityInBytes = frames2samples( - frameCount - readFrameCount) * sizeof(*sampleBuffer); + numberOfFrames - readFrameCount) * sizeof(*sampleBuffer); DEBUG_ASSERT(0 < decodeBufferCapacityInBytes); void* pDecodeBuffer = pSampleBuffer; NeAACDecDecode2(m_hDecoder, &decFrameInfo, diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 83a554ba047..c236a91e2eb 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -30,9 +30,9 @@ class AudioSourceM4A : public AudioSource { ~AudioSourceM4A(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: AudioSourceM4A(); diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index 3f9560c0c9d..95f0c5aea82 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -134,7 +134,7 @@ Result AudioSourceMediaFoundation::open(QString fileName) { //Seek to position 0, which forces us to skip over all the header frames. //This makes sure we're ready to just let the Analyser rip and it'll //get the number of samples it expects (ie. no header frames). - seekFrame(0); + seekSampleFrame(0); return OK; } @@ -160,10 +160,10 @@ void AudioSourceMediaFoundation::close() { reset(); } -Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekFrame( +Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekSampleFrame( diff_type frameIndex) { if (sDebug) { - qDebug() << "seekFrame()" << frameIndex; + qDebug() << "seekSampleFrame()" << frameIndex; } qint64 mfSeekTarget(mfFromFrame(frameIndex) - 1); // minus 1 here seems to make our seeking work properly, otherwise we will @@ -208,12 +208,12 @@ Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekFrame( return result; } -Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { +Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { if (sDebug) { - qDebug() << "read()" << frameCount; + qDebug() << "read()" << numberOfFrames; } - size_type framesNeeded(frameCount); + size_type framesNeeded(numberOfFrames); // first, copy frames from leftover buffer IF the leftover buffer is at // the correct frame @@ -226,7 +226,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readFrameSamplesInterl << "WARNING: Expected frames needed to be 0. Abandoning this file."; m_dead = true; } - m_leftoverBufferPosition += frameCount; + m_leftoverBufferPosition += numberOfFrames; } } else { // leftoverBuffer already empty or in the wrong position, clear it @@ -303,7 +303,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readFrameSamplesInterl // Uh oh. We are farther forward than our seek target. Emit // silence? We can't seek backwards here. sample_type* pBufferCurpos = sampleBuffer - + frames2samples(frameCount - framesNeeded); + + frames2samples(numberOfFrames - framesNeeded); qint64 offshootFrames = bufferPosition - m_nextFrame; // If we can correct this immediately, write zeros and adjust @@ -359,7 +359,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readFrameSamplesInterl m_leftoverBufferSize = newSize; } copyFrames( - sampleBuffer + frames2samples(frameCount - framesNeeded), + sampleBuffer + frames2samples(numberOfFrames - framesNeeded), &framesNeeded, buffer, bufferLength); @@ -374,7 +374,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readFrameSamplesInterl break; } - size_type framesRead = frameCount - framesNeeded; + size_type framesRead = numberOfFrames - framesNeeded; m_iCurrentPosition += framesRead; m_nextFrame += framesRead; if (m_leftoverBufferLength > 0) { @@ -386,7 +386,7 @@ Mixxx::AudioSource::size_type AudioSourceMediaFoundation::readFrameSamplesInterl m_leftoverBufferPosition = m_nextFrame; } if (sDebug) { - qDebug() << "read()" << frameCount << "returning" << framesRead; + qDebug() << "read()" << numberOfFrames << "returning" << framesRead; } return framesRead; } diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index 761e96e293e..870e58fede2 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -24,9 +24,9 @@ class AudioSourceMediaFoundation : public AudioSource { ~AudioSourceMediaFoundation(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: AudioSourceMediaFoundation(); diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 96fff3f045f..05029d918b3 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -55,7 +55,7 @@ void AudioSourceWV::close() { reset(); } -AudioSource::diff_type AudioSourceWV::seekFrame(diff_type frameIndex) { +AudioSource::diff_type AudioSourceWV::seekSampleFrame(diff_type frameIndex) { if (WavpackSeekSample(m_wpc, frameIndex) == TRUE) { return frameIndex; } else { @@ -64,11 +64,11 @@ AudioSource::diff_type AudioSourceWV::seekFrame(diff_type frameIndex) { } } -AudioSource::size_type AudioSourceWV::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { +AudioSource::size_type AudioSourceWV::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { // static assert: sizeof(sample_type) == sizeof(int32_t) size_type unpackCount = WavpackUnpackSamples(m_wpc, - reinterpret_cast(sampleBuffer), frameCount); + reinterpret_cast(sampleBuffer), numberOfFrames); if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { // signed integer -> float const size_type sampleCount = frames2samples(unpackCount); diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index 0181f87349b..a3cee18e8a4 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -20,9 +20,9 @@ class AudioSourceWV: public AudioSource { ~AudioSourceWV(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: AudioSourceWV(); diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 4a1146e94f1..8d2c84fe26e 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -173,7 +173,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi do { ScopedTimer t("AnalyserQueue::doAnalysis block"); - const Mixxx::AudioSource::size_type readFrameCount = pAudioSource->readStereoFrameSamplesInterleaved(kAnalysisFrameCount, &m_sampleBuffer[0]); + const Mixxx::AudioSource::size_type readFrameCount = pAudioSource->readSampleFramesStereo(kAnalysisFrameCount, &m_sampleBuffer[0]); // To compare apples to apples, let's only look at blocks that are the // full block size. diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index ded9922d59a..ae6144c029c 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -62,9 +62,9 @@ void CachingReaderWorker::processChunkReadRequest(ChunkReadRequest* request, return; } - frame_position = m_pAudioSource->seekFrame(frame_position); + frame_position = m_pAudioSource->seekSampleFrame(frame_position); - const Mixxx::AudioSource::size_type frames_read = m_pAudioSource->readStereoFrameSamplesInterleaved(frames_to_read, request->chunk->stereoSamples); + const Mixxx::AudioSource::size_type frames_read = m_pAudioSource->readSampleFramesStereo(frames_to_read, request->chunk->stereoSamples); // If we've run out of music, the AudioSource can return 0 frames/samples. // Remember that AudioSource->getFrameCount() can lie to us about diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index aae5e70c751..296773bc217 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -31,7 +31,7 @@ namespace const Mixxx::AudioSource::size_type numSamples = numFrames * kFingerprintChannels; Mixxx::AudioSource::sample_type* sampleBuffer = new Mixxx::AudioSource::sample_type[numSamples]; - const Mixxx::AudioSource::size_type readFrames = pAudioSource->readStereoFrameSamplesInterleaved(numFrames, sampleBuffer); + const Mixxx::AudioSource::size_type readFrames = pAudioSource->readSampleFramesStereo(numFrames, sampleBuffer); const Mixxx::AudioSource::size_type readSamples = readFrames * kFingerprintChannels; SAMPLE *pData = new SAMPLE[readSamples]; diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index 2443b7dbb5d..38c19754527 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -36,26 +36,26 @@ void AudioSource::reset() { m_bitrate = kBitrateDefault; } -AudioSource::size_type AudioSource::readStereoFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { +AudioSource::size_type AudioSource::readSampleFramesStereo( + size_type numberOfFrames, sample_type* sampleBuffer) { switch (getChannelCount()) { case 1: // mono channel { - const AudioSource::size_type readCount = readFrameSamplesInterleaved( - frameCount, sampleBuffer); + const AudioSource::size_type readCount = readSampleFrames( + numberOfFrames, sampleBuffer); SampleUtil::doubleMonoToDualMono(sampleBuffer, readCount); return readCount; } case 2: // stereo channel(s) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer); + return readSampleFrames(numberOfFrames, sampleBuffer); } default: // multiple channels { typedef std::vector SampleBuffer; - SampleBuffer tempBuffer(frames2samples(frameCount)); - const AudioSource::size_type readCount = readFrameSamplesInterleaved( - frameCount, &tempBuffer[0]); + SampleBuffer tempBuffer(frames2samples(numberOfFrames)); + const AudioSource::size_type readCount = readSampleFrames( + numberOfFrames, &tempBuffer[0]); SampleUtil::copyMultiToStereo(sampleBuffer, &tempBuffer[0], readCount, getChannelCount()); return readCount; diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index c21a38a4d67..7d50f021b32 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -16,7 +16,16 @@ namespace Mixxx { // be constant and are not allowed to change over time. // // The length of audio data is measured in frames. A frame -// is a tuple that contains one sample for each channel. +// is a tuple containing samples from each channel that are +// coincident in time. A frame for a mono signal contains a +// single sample. A frame for a stereo signal contains a pair +// of samples, one for the left and right channel respectively. +// +// Samples in a sample buffer are stored as consecutive frames, +// i.e. the samples of the channels are interleaved. +// +// Audio sources are implicitly opened upon creation and +// closed upon destruction. class AudioSource { public: typedef std::size_t size_type; @@ -96,7 +105,7 @@ class AudioSource { } // The actual duration in seconds. - // Only avalailable for valid files! + // Only available for valid files! inline bool hasDuration() const { return isValid(); } @@ -105,14 +114,14 @@ class AudioSource { return getFrameCount() / getFrameRate(); } - // #frames -> #samples + // Conversion: #frames -> #samples template inline T frames2samples(T frameCount) const { DEBUG_ASSERT(isChannelCountValid()); return frameCount * getChannelCount(); } - // #samples -> #frames + // Conversion: #samples -> #frames template inline T samples2frames(T sampleCount) const { DEBUG_ASSERT(isChannelCountValid()); @@ -126,38 +135,40 @@ class AudioSource { // - The seek position in seconds is frameIndex / frameRate() // Returns the actual current frame index which may differ from the // requested index if the source does not support accurate seeking. - virtual diff_type seekFrame(diff_type frameIndex) = 0; + virtual diff_type seekSampleFrame(diff_type frameIndex) = 0; // Fills the buffer with samples from each channel starting // at the current frame seek position. // - // The required size of the sampleBuffer is sampleCount = - // frames2samples(frameCount). The samples in the sampleBuffer - // are stored as consecutive frames with the samples for each - // channel interleaved. + // The required size of the sampleBuffer is numberOfSamples = + // frames2samples(numberOfFrames). The samples in the sampleBuffer + // are stored as consecutive sample frames with samples from + // each channel interleaved. // // Returns the actual number of frames that have been read which // may be lower than the requested number of frames. The current // frame seek position is moved forward to the next unread frame. - virtual size_type readFrameSamplesInterleaved(size_type frameCount, + virtual size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) = 0; - // Utility function for explicitly reading stereo (= 2 channels) + // Specialized function for explicitly reading stereo (= 2 channels) // frames from an AudioSource. This is commonly used in Mixxx! // - // If this source provides only a single channel (mono) the samples + // If the source provides only a single channel (mono) the samples // of that channel will be doubled. If the source provides more // than 2 channels only the first 2 channels will be read. The - // minimum required capacity of the sampleBuffer is frameCount * 2. + // minimum required capacity of the sampleBuffer is numberOfFrames * 2. // // Returns the actual number of frames that have been read which // may be lower than the requested number of frames. The current - // frame seek position is moved forward to the next unread frame. + // frame seek position is moved forward towards the next unread + // frame. // // Derived classes may provide an optimized version that doesn't // require any post-processing as done by this default implementation. - // Especially down-mixing multiple channels to stereo is inefficient! - virtual size_type readStereoFrameSamplesInterleaved(size_type frameCount, + // Please note that the default implementation performs poorly when + // reducing more than 2 channels to stereo! + virtual size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer); protected: diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index 1bca68ee438..a7d790194b0 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -111,7 +111,7 @@ Result AudioSourceCoreAudio::open(QString fileName) { //Seek to position 0, which forces us to skip over all the header frames. //This makes sure we're ready to just let the Analyser rip and it'll //get the number of samples it expects (ie. no header frames). - seekFrame(0); + seekSampleFrame(0); return OK; } @@ -120,7 +120,7 @@ void AudioSourceCoreAudio::close() { ExtAudioFileDispose(m_audioFile); } -AudioSource::diff_type AudioSourceCoreAudio::seekFrame(diff_type frameIndex) { +AudioSource::diff_type AudioSourceCoreAudio::seekSampleFrame(diff_type frameIndex) { OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); //_ThrowExceptionIfErr(@"ExtAudioFileSeek", err); //qDebug() << "SSCA: Seeking to" << frameIndex; @@ -130,13 +130,13 @@ AudioSource::diff_type AudioSourceCoreAudio::seekFrame(diff_type frameIndex) { return frameIndex; } -AudioSource::size_type AudioSourceCoreAudio::readFrameSamplesInterleaved(size_type frameCount, +AudioSource::size_type AudioSourceCoreAudio::readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) { //if (!m_decoder) return 0; size_type numFramesRead = 0; - while (numFramesRead < frameCount) { - size_type numFramesToRead = frameCount - numFramesRead; + while (numFramesRead < numberOfFrames) { + size_type numFramesToRead = numberOfFrames - numFramesRead; AudioBufferList fillBufList; fillBufList.mNumberBuffers = 1; diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index 7bf24fc74cc..9b65007d2f6 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -27,8 +27,8 @@ class AudioSourceCoreAudio : public AudioSource { ~AudioSourceCoreAudio(); - diff_type seekFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 33f71ebd00c..12aaf64500d 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -403,7 +403,7 @@ bool AudioSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, return false; } -AudioSource::diff_type AudioSourceFFmpeg::seekFrame(diff_type frameIndex) { +AudioSource::diff_type AudioSourceFFmpeg::seekSampleFrame(diff_type frameIndex) { const diff_type filepos = frames2samples(frameIndex); int ret = 0; @@ -468,7 +468,7 @@ unsigned int AudioSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { if (m_SCache.size() == 0) { // Make sure we allways start at begining and cache have some // material that we can consume. - seekFrame(0); + seekSampleFrame(0); m_bIsSeeked = FALSE; } @@ -485,14 +485,14 @@ unsigned int AudioSourceFFmpeg::read(unsigned long size, SAMPLE* destination) { return size; } -AudioSource::size_type AudioSourceFFmpeg::readFrameSamplesInterleaved(size_type frameCount, +AudioSource::size_type AudioSourceFFmpeg::readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) { // This is just a hack that simply reuses existing // functionality. Sample data should be resampled // directly into AV_SAMPLE_FMT_FLT instead of // AV_SAMPLE_FMT_S16! typedef std::vector TempBuffer; - TempBuffer tempBuffer(frames2samples(frameCount)); + TempBuffer tempBuffer(frames2samples(numberOfFrames)); const size_type readSamples = read(tempBuffer.size(), &tempBuffer[0]); for (size_type i = 0; i < readSamples; ++i) { sampleBuffer[i] = SAMPLE_clampSymmetric(tempBuffer[i]) / sample_type(SAMPLE_MAX); diff --git a/src/sources/audiosourceffmpeg.h b/src/sources/audiosourceffmpeg.h index afac0395e6a..ab85b792999 100644 --- a/src/sources/audiosourceffmpeg.h +++ b/src/sources/audiosourceffmpeg.h @@ -47,9 +47,9 @@ class AudioSourceFFmpeg : public AudioSource { ~AudioSourceFFmpeg(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: AudioSourceFFmpeg(); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 88cec79f269..05f139e7045 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -117,7 +117,7 @@ void AudioSourceFLAC::close() { reset(); } -Mixxx::AudioSource::diff_type AudioSourceFLAC::seekFrame(diff_type frameIndex) { +Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame(diff_type frameIndex) { // clear decode buffer before seeking m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; @@ -128,21 +128,21 @@ Mixxx::AudioSource::diff_type AudioSourceFLAC::seekFrame(diff_type frameIndex) { return frameIndex; } -Mixxx::AudioSource::size_type AudioSourceFLAC::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); +Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, false); } -Mixxx::AudioSource::size_type AudioSourceFLAC::readStereoFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); +Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFramesStereo( + size_type numberOfFrames, sample_type* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, true); } -Mixxx::AudioSource::size_type AudioSourceFLAC::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer, +Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer, bool readStereoSamples) { sample_type* outBuffer = sampleBuffer; - size_type framesRemaining = frameCount; + size_type framesRemaining = numberOfFrames; while (0 < framesRemaining) { DEBUG_ASSERT( m_decodeSampleBufferReadOffset @@ -194,7 +194,7 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readFrameSamplesInterleaved( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); } - return frameCount - framesRemaining; + return numberOfFrames - framesRemaining; } // flac callback methods diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 98bf11074f4..f878b4b1340 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -19,10 +19,10 @@ class AudioSourceFLAC : public AudioSource { ~AudioSourceFLAC(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; // callback methods FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t *bytes); @@ -41,7 +41,7 @@ class AudioSourceFLAC : public AudioSource { void close(); - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, bool readStereoSamples); QFile m_file; diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index d3b4c2b00aa..5e8f9a47831 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -133,15 +133,15 @@ void AudioSourceModPlug::close() { reset(); } -AudioSource::diff_type AudioSourceModPlug::seekFrame( +AudioSource::diff_type AudioSourceModPlug::seekSampleFrame( diff_type frameIndex) { return m_seekPos = frameIndex; } -AudioSource::size_type AudioSourceModPlug::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { +AudioSource::size_type AudioSourceModPlug::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { const size_type maxFrames = samples2frames(m_sampleBuf.size()); - const size_type readFrames = math_min(maxFrames - m_seekPos, frameCount); + const size_type readFrames = math_min(maxFrames - m_seekPos, numberOfFrames); const size_type readSamples = frames2samples(readFrames); const size_type readOffset = frames2samples(m_seekPos); diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index 359ca7cef25..fef79b7d113 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -29,9 +29,9 @@ class AudioSourceModPlug: public AudioSource { ~AudioSourceModPlug(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 43c2b49d91e..f36476f1a99 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -265,12 +265,12 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff return seekFrameIndex; } -AudioSource::diff_type AudioSourceMp3::seekFrame(diff_type frameIndex) { +AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { if (m_curFrameIndex == frameIndex) { return m_curFrameIndex; } if (0 > frameIndex) { - return seekFrame(0); + return seekSampleFrame(0); } // simply skip frames when jumping no more than kMaxSkipFrameSamplesWhenSeeking frames forward if ((frameIndex < m_curFrameIndex) || ((frameIndex - m_curFrameIndex) > kMaxSkipFrameSamplesWhenSeeking)) { @@ -290,20 +290,20 @@ AudioSource::diff_type AudioSourceMp3::seekFrame(diff_type frameIndex) { return m_curFrameIndex; } -AudioSource::size_type AudioSourceMp3::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); +AudioSource::size_type AudioSourceMp3::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, false); } -AudioSource::size_type AudioSourceMp3::readStereoFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); +AudioSource::size_type AudioSourceMp3::readSampleFramesStereo( + size_type numberOfFrames, sample_type* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, true); } -AudioSource::size_type AudioSourceMp3::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer, +AudioSource::size_type AudioSourceMp3::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer, bool readStereoSamples) { - size_type framesRemaining = frameCount; + size_type framesRemaining = numberOfFrames; sample_type* pSampleBuffer = sampleBuffer; while (0 < framesRemaining) { if (0 >= m_madSynthCount) { @@ -361,7 +361,7 @@ AudioSource::size_type AudioSourceMp3::readFrameSamplesInterleaved( } } } - return frameCount - framesRemaining; + return numberOfFrames - framesRemaining; } } // namespace Mixxx diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index c3cbca387cc..b2c00ea6d90 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -25,10 +25,10 @@ class AudioSourceMp3 : public AudioSource { ~AudioSourceMp3(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: explicit AudioSourceMp3(QString fileName); @@ -37,10 +37,10 @@ class AudioSourceMp3 : public AudioSource { void close(); - inline size_type skipFrameSamples(size_type frameCount) { - return readFrameSamplesInterleaved(frameCount, NULL); + inline size_type skipFrameSamples(size_type numberOfFrames) { + return readSampleFrames(numberOfFrames, NULL); } - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer, bool readStereoSamples); + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, bool readStereoSamples); QFile m_file; quint64 m_fileSize; diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index e0eb160461e..1f39c7e64f8 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -64,7 +64,7 @@ void AudioSourceOggVorbis::close() { reset(); } -AudioSource::diff_type AudioSourceOggVorbis::seekFrame( +AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( diff_type frameIndex) { const int seekResult = ov_pcm_seek(&m_vf, frameIndex); if (0 != seekResult) { @@ -73,26 +73,26 @@ AudioSource::diff_type AudioSourceOggVorbis::seekFrame( return ov_pcm_tell(&m_vf); } -AudioSource::size_type AudioSourceOggVorbis::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, false); +AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, false); } -AudioSource::size_type AudioSourceOggVorbis::readStereoFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - return readFrameSamplesInterleaved(frameCount, sampleBuffer, true); +AudioSource::size_type AudioSourceOggVorbis::readSampleFramesStereo( + size_type numberOfFrames, sample_type* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, true); } -AudioSource::size_type AudioSourceOggVorbis::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer, +AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer, bool readStereoSamples) { size_type readCount = 0; sample_type* nextSample = sampleBuffer; - while (readCount < frameCount) { + while (readCount < numberOfFrames) { float** pcmChannels; int currentSection; const long readResult = ov_read_float(&m_vf, &pcmChannels, - frameCount - readCount, ¤tSection); + numberOfFrames - readCount, ¤tSection); if (0 == readResult) { // EOF break; // done diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index 374e0837948..f524172e002 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -16,10 +16,10 @@ class AudioSourceOggVorbis: public AudioSource { ~AudioSourceOggVorbis(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: AudioSourceOggVorbis(); @@ -28,7 +28,7 @@ class AudioSourceOggVorbis: public AudioSource { void close(); - size_type readFrameSamplesInterleaved(size_type frameCount, + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, bool readStereoSamples); OggVorbis_File m_vf; diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index dc51f210e14..d1b7acdb2d7 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -60,7 +60,7 @@ void AudioSourceOpus::close() { reset(); } -AudioSource::diff_type AudioSourceOpus::seekFrame(diff_type frameIndex) { +AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggOpus file:" << seekResult; @@ -68,13 +68,13 @@ AudioSource::diff_type AudioSourceOpus::seekFrame(diff_type frameIndex) { return op_pcm_tell(m_pOggOpusFile); } -AudioSource::size_type AudioSourceOpus::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { +AudioSource::size_type AudioSourceOpus::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { size_type readCount = 0; - while (readCount < frameCount) { + while (readCount < numberOfFrames) { int readResult = op_read_float(m_pOggOpusFile, sampleBuffer + frames2samples(readCount), - frames2samples(frameCount - readCount), NULL); + frames2samples(numberOfFrames - readCount), NULL); if (0 == readResult) { // EOF break; // done @@ -90,13 +90,13 @@ AudioSource::size_type AudioSourceOpus::readFrameSamplesInterleaved( return readCount; } -AudioSource::size_type AudioSourceOpus::readStereoFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { +AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( + size_type numberOfFrames, sample_type* sampleBuffer) { size_type readCount = 0; - while (readCount < frameCount) { + while (readCount < numberOfFrames) { int readResult = op_read_float_stereo(m_pOggOpusFile, sampleBuffer + (readCount * 2), - (frameCount - readCount) * 2); + (numberOfFrames - readCount) * 2); if (0 == readResult) { // EOF break; // done diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 11b16144d22..6b0c5d08caa 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -19,10 +19,10 @@ class AudioSourceOpus: public AudioSource { ~AudioSourceOpus(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; - size_type readStereoFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: AudioSourceOpus(); diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index e39c87fb761..ba6a3f5b9da 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -65,7 +65,7 @@ void AudioSourceSndFile::close() { reset(); } -AudioSource::diff_type AudioSourceSndFile::seekFrame( +AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( diff_type frameIndex) { const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); if (0 <= seekResult) { @@ -77,9 +77,9 @@ AudioSource::diff_type AudioSourceSndFile::seekFrame( } } -AudioSource::size_type AudioSourceSndFile::readFrameSamplesInterleaved( - size_type frameCount, sample_type* sampleBuffer) { - const sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, frameCount); +AudioSource::size_type AudioSourceSndFile::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { + const sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); if (0 <= readCount) { return readCount; } else { diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 23995a01b5e..08d8e6da8f0 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -21,9 +21,9 @@ class AudioSourceSndFile: public AudioSource { ~AudioSourceSndFile(); - diff_type seekFrame(diff_type frameIndex) /*override*/; + diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readFrameSamplesInterleaved(size_type frameCount, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; private: AudioSourceSndFile(); From d76a786c53e4b30a4f5283426347caa9acd139a8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 12:36:27 +0100 Subject: [PATCH 051/481] Improve readability of AudioSourceM4A (review comments) --- plugins/soundsourcem4a/audiosourcem4a.cpp | 106 +++++++++++++--------- plugins/soundsourcem4a/audiosourcem4a.h | 8 +- 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index dea7ef2bd86..2ee8515fd15 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -13,6 +13,7 @@ #define strncasecmp strnicmp #endif +// TODO(XXX): Do we still need this "hack" on the supported platforms? #ifdef __M4AHACK__ typedef uint32_t SAMPLERATE_TYPE; #else @@ -28,12 +29,12 @@ namespace const AudioSource::size_type kFramesPerSampleBlock = 1024; // MP4SampleId is 1-based -const MP4SampleId kMinSampleId = 1; +const MP4SampleId kMinSampleBlockId = 1; // Decoding will be restarted one or more blocks of samples // before the actual position to avoid audible glitches. // Two blocks of samples seems to be enough here. -const MP4SampleId kSampleIdPrefetchCount = 2; +const MP4SampleId kNumberOfPrefetchSampleBlocks = 2; MP4TrackId findAacTrackId(MP4FileHandle hFile) { const MP4TrackId maxTrackId = MP4GetNumberOfTracks(hFile, NULL, 0); @@ -66,10 +67,14 @@ MP4TrackId findAacTrackId(MP4FileHandle hFile) { } AudioSourceM4A::AudioSourceM4A() - : m_hFile(MP4_INVALID_FILE_HANDLE), m_trackId( - MP4_INVALID_TRACK_ID), m_maxSampleId(MP4_INVALID_SAMPLE_ID), m_curSampleId( - MP4_INVALID_SAMPLE_ID), m_inputBufferOffset(0), m_inputBufferLength( - 0), m_hDecoder(NULL), m_curFrameIndex(0) { + : m_hFile(MP4_INVALID_FILE_HANDLE), + m_trackId(MP4_INVALID_TRACK_ID), + m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_inputBufferOffset(0), + m_inputBufferLength(0), + m_hDecoder(NULL), + m_curFrameIndex(0) { } AudioSourceM4A::~AudioSourceM4A() { @@ -105,12 +110,12 @@ Result AudioSourceM4A::open(QString fileName) { return ERR; } - m_maxSampleId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); - if (MP4_INVALID_SAMPLE_ID == m_maxSampleId) { + m_maxSampleBlockId = MP4GetTrackNumberOfSamples(m_hFile, m_trackId); + if (MP4_INVALID_SAMPLE_ID == m_maxSampleBlockId) { qWarning() << "Failed to read file structure:" << fileName; return ERR; } - m_curSampleId = MP4_INVALID_SAMPLE_ID; + m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; m_inputBuffer.resize(MP4GetTrackMaxSampleSize(m_hFile, m_trackId), 0); m_inputBufferOffset = 0; @@ -158,10 +163,12 @@ Result AudioSourceM4A::open(QString fileName) { setChannelCount(channelCount); setFrameRate(sampleRate); - setFrameCount(m_maxSampleId * kFramesPerSampleBlock); + setFrameCount(m_maxSampleBlockId * kFramesPerSampleBlock); - const SampleBuffer::size_type prefetchSampleBufferSize = (kSampleIdPrefetchCount + 1) * frames2samples(kFramesPerSampleBlock); - SampleBuffer(prefetchSampleBufferSize).swap(m_prefetchSampleBuffer); + // Allocate one block more than the number of sample blocks + // that are prefetched + const SampleBuffer::size_type prefetchSampleBufferSize = (kNumberOfPrefetchSampleBlocks + 1) * frames2samples(kFramesPerSampleBlock); + m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); // invalidate current frame index m_curFrameIndex = getFrameCount(); @@ -184,8 +191,8 @@ void AudioSourceM4A::close() { m_hFile = MP4_INVALID_FILE_HANDLE; } m_trackId = MP4_INVALID_TRACK_ID; - m_maxSampleId = MP4_INVALID_SAMPLE_ID; - m_curSampleId = MP4_INVALID_SAMPLE_ID; + m_maxSampleBlockId = MP4_INVALID_SAMPLE_ID; + m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; m_inputBuffer.clear(); m_inputBufferOffset = 0; m_inputBufferLength = 0; @@ -193,31 +200,31 @@ void AudioSourceM4A::close() { reset(); } -bool AudioSourceM4A::isValidSampleId(MP4SampleId sampleId) const { - return (sampleId >= kMinSampleId) && (sampleId <= m_maxSampleId); +bool AudioSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { + return (sampleBlockId >= kMinSampleBlockId) && (sampleBlockId <= m_maxSampleBlockId); } AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { if (m_curFrameIndex != frameIndex) { - const MP4SampleId sampleId = kMinSampleId + const MP4SampleId sampleBlockId = kMinSampleBlockId + (frameIndex / kFramesPerSampleBlock); - if (!isValidSampleId(sampleId)) { + if (!isValidSampleBlockId(sampleBlockId)) { return m_curFrameIndex; } - if ((m_curSampleId != sampleId) || (frameIndex < m_curFrameIndex)) { - // Restart decoding one or more blocks of samples - // backwards to avoid audible glitches. + if ((m_curSampleBlockId != sampleBlockId) || (frameIndex < m_curFrameIndex)) { + // Restart decoding one or more blocks of samples backwards to + // avoid audible glitches. // Implementation note: The type MP4SampleId is unsigned so we - // have to be careful when subtracting! - if ((kMinSampleId + kSampleIdPrefetchCount) < sampleId) { - m_curSampleId = sampleId - kSampleIdPrefetchCount; + // need to be careful when subtracting! + if ((kMinSampleBlockId + kNumberOfPrefetchSampleBlocks) < sampleBlockId) { + m_curSampleBlockId = sampleBlockId - kNumberOfPrefetchSampleBlocks; } else { - m_curSampleId = kMinSampleId; + m_curSampleBlockId = kMinSampleBlockId; } - m_curFrameIndex = (m_curSampleId - kMinSampleId) * kFramesPerSampleBlock; + m_curFrameIndex = (m_curSampleBlockId - kMinSampleBlockId) * kFramesPerSampleBlock; // rryan 9/2009 -- the documentation is sketchy on this, but I think that // it tells the decoder that you are seeking so it should flush its state - faacDecPostSeekReset(m_hDecoder, m_curSampleId); + faacDecPostSeekReset(m_hDecoder, m_curSampleBlockId); // discard input buffer m_inputBufferOffset = 0; m_inputBufferLength = 0; @@ -235,23 +242,35 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { AudioSource::size_type AudioSourceM4A::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { + if (!isValidSampleBlockId(m_curSampleBlockId)) { + qWarning() << "Invalid MP4 sample block" << m_curSampleBlockId; + return 0; + } sample_type* pSampleBuffer = sampleBuffer; - const diff_type readFrameIndex = m_curFrameIndex; - size_type readFrameCount = m_curFrameIndex - readFrameIndex; - while ((readFrameCount = m_curFrameIndex - readFrameIndex) < numberOfFrames) { - if (isValidSampleId(m_curSampleId) && (0 == m_inputBufferLength)) { - // fill input buffer with block of samples - InputBuffer::value_type* pInputBuffer = &m_inputBuffer[0]; + size_type numberOfFramesRemaining = numberOfFrames; + while (0 < numberOfFramesRemaining) { + DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); + if (m_inputBufferOffset >= m_inputBufferLength) { + // reset input buffer m_inputBufferOffset = 0; - m_inputBufferLength = m_inputBuffer.size(); // in/out parameter - if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleId++, - &pInputBuffer, &m_inputBufferLength, - NULL, NULL, NULL, NULL)) { - qWarning() << "Failed to read MP4 sample data" << m_curSampleId; - break; // abort + m_inputBufferLength = 0; + // fill input buffer with next block of samples + const MP4SampleId nextSampleBlockId = m_curSampleBlockId + 1; + if (isValidSampleBlockId(nextSampleBlockId)) { + u_int8_t* pInputBuffer = &m_inputBuffer[0]; + u_int32_t inputBufferLength = m_inputBuffer.size(); // in/out parameter + if (!MP4ReadSample(m_hFile, m_trackId, nextSampleBlockId, + &pInputBuffer, &inputBufferLength, + NULL, NULL, NULL, NULL)) { + qWarning() << "Failed to read MP4 input data for sample block" << m_curSampleBlockId; + break; // abort + } + m_curSampleBlockId = nextSampleBlockId; + m_inputBufferLength = inputBufferLength; } } - if (0 == m_inputBufferLength) { + DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); + if (m_inputBufferOffset >= m_inputBufferLength) { // EOF break; // done } @@ -259,14 +278,13 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( decFrameInfo.bytesconsumed = 0; decFrameInfo.samples = 0; // decode samples into sampleBuffer - const size_type readFrameCount = m_curFrameIndex - readFrameIndex; const size_type decodeBufferCapacityInBytes = frames2samples( - numberOfFrames - readFrameCount) * sizeof(*sampleBuffer); + numberOfFrames - numberOfFramesRemaining) * sizeof(*sampleBuffer); DEBUG_ASSERT(0 < decodeBufferCapacityInBytes); void* pDecodeBuffer = pSampleBuffer; NeAACDecDecode2(m_hDecoder, &decFrameInfo, &m_inputBuffer[m_inputBufferOffset], - m_inputBufferLength / sizeof(m_inputBuffer[0]), + m_inputBufferLength, &pDecodeBuffer, decodeBufferCapacityInBytes); if (0 != decFrameInfo.error) { qWarning() << "AAC decoding error:" @@ -300,7 +318,7 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( break; // abort } } - return readFrameCount; + return numberOfFrames - numberOfFramesRemaining; } } // namespace Mixxx diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index c236a91e2eb..d8a9cc457c4 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -41,17 +41,17 @@ class AudioSourceM4A : public AudioSource { void close(); - bool isValidSampleId(MP4SampleId sampleId) const; + bool isValidSampleBlockId(MP4SampleId sampleBlockId) const; MP4FileHandle m_hFile; MP4TrackId m_trackId; - MP4SampleId m_maxSampleId; - MP4SampleId m_curSampleId; + MP4SampleId m_maxSampleBlockId; + MP4SampleId m_curSampleBlockId; typedef std::vector InputBuffer; InputBuffer m_inputBuffer; InputBuffer::size_type m_inputBufferOffset; - u_int32_t m_inputBufferLength; + InputBuffer::size_type m_inputBufferLength; faacDecHandle m_hDecoder; From 818c43ecc604846d6339a63b08451e24f4eac779 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 12:37:00 +0100 Subject: [PATCH 052/481] FIXUP for "New SoundSource/AudioSource API" commit: Revert changes to EngineBufferScaleST --- src/engine/enginebufferscalest.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/enginebufferscalest.cpp b/src/engine/enginebufferscalest.cpp index 6068d1103e4..bf904c95ba8 100644 --- a/src/engine/enginebufferscalest.cpp +++ b/src/engine/enginebufferscalest.cpp @@ -38,6 +38,7 @@ EngineBufferScaleST::EngineBufferScaleST(ReadAheadManager *pReadAheadManager) m_dTempoOld(1.0), m_pReadAheadManager(pReadAheadManager) { m_pSoundTouch = new soundtouch::SoundTouch(); + m_pSoundTouch->setChannels(2); m_pSoundTouch->setRate(m_dRateOld); m_pSoundTouch->setTempo(m_dTempoOld); m_pSoundTouch->setPitch(1.0); From 48ee08a8992eeee9d2083c941c041531fe8d0e1c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 12:37:36 +0100 Subject: [PATCH 053/481] Fix WavPack decoding issues (review comments) --- plugins/soundsourcewv/SConscript | 3 +-- plugins/soundsourcewv/audiosourcewv.cpp | 11 ++++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/soundsourcewv/SConscript b/plugins/soundsourcewv/SConscript index 4c996c35220..787af35d352 100644 --- a/plugins/soundsourcewv/SConscript +++ b/plugins/soundsourcewv/SConscript @@ -16,8 +16,7 @@ wv_sources = [ "sources/soundsource.cpp", "sources/audiosource.cpp", "metadata/trackmetadata.cpp", - "metadata/trackmetadatataglib.cpp", - "sampleutil.cpp" + "metadata/trackmetadatataglib.cpp" ] diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 05029d918b3..385087a93b7 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -1,7 +1,5 @@ #include "audiosourcewv.h" -#include "sampleutil.h" - namespace Mixxx { AudioSourceWV::AudioSourceWV() @@ -37,11 +35,11 @@ Result AudioSourceWV::open(QString fileName) { setFrameCount(WavpackGetNumSamples(m_wpc)); if (WavpackGetMode(m_wpc) & MODE_FLOAT) { - m_sampleScale = 1.0f; + m_sampleScale = kSampleValuePeak; } else { const int bitsPerSample = WavpackGetBitsPerSample(m_wpc); - const uint32_t maxSampleValue = uint32_t(1) << (bitsPerSample - 1); - m_sampleScale = 1.0f / (0.5f + sample_type(maxSampleValue)); + const uint32_t peakSampleValue = uint32_t(1) << (bitsPerSample - 1); + m_sampleScale = kSampleValuePeak / sample_type(peakSampleValue); } return OK; @@ -75,8 +73,7 @@ AudioSource::size_type AudioSourceWV::readSampleFrames( for (size_type i = 0; i < sampleCount; ++i) { const int32_t sampleValue = reinterpret_cast(sampleBuffer)[i]; - sampleBuffer[i] = SampleUtil::clampSample( - sampleValue * m_sampleScale); + sampleBuffer[i] = sample_type(sampleValue) * m_sampleScale; } } return unpackCount; From 1081d5c8914572ada7319ff770ec566016d5bfc3 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 12:38:20 +0100 Subject: [PATCH 054/481] Optimize reading stereo samples from audio sources --- src/analyserqueue.cpp | 2 +- src/cachingreaderworker.cpp | 2 +- src/musicbrainz/chromaprinter.cpp | 48 ++++++++++++------------- src/musicbrainz/chromaprinter.h | 2 +- src/musicbrainz/tagfetcher.cpp | 2 +- src/sources/audiosource.cpp | 45 ++++++++++++++++------- src/sources/audiosource.h | 54 ++++++++++++++++++---------- src/sources/audiosourceflac.cpp | 18 +++++----- src/sources/audiosourceflac.h | 4 +-- src/sources/audiosourcemp3.cpp | 23 ++++++------ src/sources/audiosourcemp3.h | 4 +-- src/sources/audiosourceoggvorbis.cpp | 22 +++++++----- src/sources/audiosourceoggvorbis.h | 5 +-- src/sources/audiosourceopus.cpp | 15 ++++---- src/sources/audiosourceopus.h | 2 +- 15 files changed, 149 insertions(+), 99 deletions(-) diff --git a/src/analyserqueue.cpp b/src/analyserqueue.cpp index 8d2c84fe26e..39ef9cbd893 100644 --- a/src/analyserqueue.cpp +++ b/src/analyserqueue.cpp @@ -173,7 +173,7 @@ bool AnalyserQueue::doAnalysis(TrackPointer tio, Mixxx::AudioSourcePointer pAudi do { ScopedTimer t("AnalyserQueue::doAnalysis block"); - const Mixxx::AudioSource::size_type readFrameCount = pAudioSource->readSampleFramesStereo(kAnalysisFrameCount, &m_sampleBuffer[0]); + const Mixxx::AudioSource::size_type readFrameCount = pAudioSource->readSampleFramesStereo(kAnalysisFrameCount, &m_sampleBuffer[0], m_sampleBuffer.size()); // To compare apples to apples, let's only look at blocks that are the // full block size. diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index ae6144c029c..031887fad93 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -64,7 +64,7 @@ void CachingReaderWorker::processChunkReadRequest(ChunkReadRequest* request, frame_position = m_pAudioSource->seekSampleFrame(frame_position); - const Mixxx::AudioSource::size_type frames_read = m_pAudioSource->readSampleFramesStereo(frames_to_read, request->chunk->stereoSamples); + const Mixxx::AudioSource::size_type frames_read = m_pAudioSource->readSampleFramesStereo(frames_to_read, request->chunk->stereoSamples, kSamplesPerChunk); // If we've run out of music, the AudioSource can return 0 frames/samples. // Remember that AudioSource->getFrameCount() can lie to us about diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 296773bc217..0c5c880211f 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -6,6 +6,8 @@ #include +#include + namespace { @@ -16,7 +18,7 @@ namespace const Mixxx::AudioSource::size_type kFingerprintDuration = 120; // in seconds const Mixxx::AudioSource::size_type kFingerprintChannels = 2; // stereo - QString calcFingerPrint(const Mixxx::AudioSourcePointer& pAudioSource) { + QString calcFingerprint(const Mixxx::AudioSourcePointer& pAudioSource) { Mixxx::AudioSource::size_type numFrames = kFingerprintDuration * pAudioSource->getFrameRate(); @@ -28,35 +30,33 @@ namespace QTime timerReadingFile; timerReadingFile.start(); - const Mixxx::AudioSource::size_type numSamples = numFrames * kFingerprintChannels; - Mixxx::AudioSource::sample_type* sampleBuffer = new Mixxx::AudioSource::sample_type[numSamples]; - - const Mixxx::AudioSource::size_type readFrames = pAudioSource->readSampleFramesStereo(numFrames, sampleBuffer); - - const Mixxx::AudioSource::size_type readSamples = readFrames * kFingerprintChannels; - SAMPLE *pData = new SAMPLE[readSamples]; - - for (Mixxx::AudioSource::size_type i = 0; i < readSamples; ++i) { - pData[i] = SAMPLE(sampleBuffer[i] * SAMPLE_MAX); - } - - delete[] sampleBuffer; + // Allocate a sample buffer with maximum size to avoid the + // implicit allocation of a temporary buffer when reducing + // to the audio signal to stereo. + std::vector sampleBuffer( + math_max(numFrames * kFingerprintChannels, pAudioSource->frames2samples(numFrames))); + DEBUG_ASSERT(2 == kFingerprintChannels); // implicit assumption of the next line + const Mixxx::AudioSource::size_type readFrames = pAudioSource->readSampleFramesStereo(numFrames, &sampleBuffer[0], sampleBuffer.size()); if (readFrames != numFrames) { - qDebug() << "oh that's embarrasing I couldn't read the track"; - delete[] pData; + qDebug() << "oh that's embarrassing I couldn't read the track"; return QString(); } + + std::vector fingerprintSamples(readFrames * kFingerprintChannels); + // Convert floating-point to integer + for (Mixxx::AudioSource::size_type i = 0; i < fingerprintSamples.size(); ++i) { + fingerprintSamples[i] = SAMPLE(sampleBuffer[i] * SAMPLE_MAX); + } + qDebug("reading file took: %d ms" , timerReadingFile.elapsed()); ChromaprintContext* ctx = chromaprint_new(CHROMAPRINT_ALGORITHM_DEFAULT); - // we have 2 channels in mixxx always chromaprint_start(ctx, pAudioSource->getFrameRate(), kFingerprintChannels); - QTime timerGeneratingFingerPrint; - timerGeneratingFingerPrint.start(); - int success = chromaprint_feed(ctx, pData, readSamples); - delete [] pData; + QTime timerGeneratingFingerprint; + timerGeneratingFingerprint.start(); + int success = chromaprint_feed(ctx, &fingerprintSamples[0], fingerprintSamples.size()); chromaprint_finish(ctx); if (!success) { qDebug() << "could not generate fingerprint"; @@ -83,7 +83,7 @@ namespace } chromaprint_free(ctx); - qDebug("generating fingerprint took: %d ms" , timerGeneratingFingerPrint.elapsed()); + qDebug("generating fingerprint took: %d ms" , timerGeneratingFingerprint.elapsed()); return fingerprint; } @@ -93,12 +93,12 @@ ChromaPrinter::ChromaPrinter(QObject* parent) : QObject(parent) { } -QString ChromaPrinter::getFingerPrint(TrackPointer pTrack) { +QString ChromaPrinter::getFingerprint(TrackPointer pTrack) { SoundSourceProxy soundSourceProxy(pTrack); Mixxx::AudioSourcePointer pAudioSource(soundSourceProxy.openAudioSource()); if (pAudioSource.isNull()) { qDebug() << "Skipping invalid file:" << pTrack->getLocation(); return QString(); } - return calcFingerPrint(pAudioSource); + return calcFingerprint(pAudioSource); } diff --git a/src/musicbrainz/chromaprinter.h b/src/musicbrainz/chromaprinter.h index 6bbfe0115f6..99fbe1ecbdc 100644 --- a/src/musicbrainz/chromaprinter.h +++ b/src/musicbrainz/chromaprinter.h @@ -10,7 +10,7 @@ class ChromaPrinter: public QObject { public: explicit ChromaPrinter(QObject* parent = NULL); - QString getFingerPrint(TrackPointer pTrack); + QString getFingerprint(TrackPointer pTrack); }; #endif //CHROMAPRINTER_H diff --git a/src/musicbrainz/tagfetcher.cpp b/src/musicbrainz/tagfetcher.cpp index 3a559ffbe50..220dd11fb44 100644 --- a/src/musicbrainz/tagfetcher.cpp +++ b/src/musicbrainz/tagfetcher.cpp @@ -34,7 +34,7 @@ TagFetcher::TagFetcher(QObject* parent) } QString TagFetcher::getFingerprint(const TrackPointer tio) { - return ChromaPrinter(NULL).getFingerPrint(tio); + return ChromaPrinter(NULL).getFingerprint(tio); } void TagFetcher::startFetch(const TrackPointer track) { diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index 38c19754527..2a1a913b7ef 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -37,28 +37,49 @@ void AudioSource::reset() { } AudioSource::size_type AudioSource::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, + sample_type* sampleBuffer, + size_type sampleBufferSize) { + DEBUG_ASSERT((sampleBufferSize / 2) >= numberOfFrames); switch (getChannelCount()) { case 1: // mono channel { - const AudioSource::size_type readCount = readSampleFrames( + const AudioSource::size_type readFrameCount = readSampleFrames( numberOfFrames, sampleBuffer); - SampleUtil::doubleMonoToDualMono(sampleBuffer, readCount); - return readCount; + SampleUtil::doubleMonoToDualMono(sampleBuffer, readFrameCount); + return readFrameCount; } case 2: // stereo channel(s) { return readSampleFrames(numberOfFrames, sampleBuffer); } - default: // multiple channels + default: // multiple (3 or more) channels { - typedef std::vector SampleBuffer; - SampleBuffer tempBuffer(frames2samples(numberOfFrames)); - const AudioSource::size_type readCount = readSampleFrames( - numberOfFrames, &tempBuffer[0]); - SampleUtil::copyMultiToStereo(sampleBuffer, &tempBuffer[0], readCount, - getChannelCount()); - return readCount; + const size_type numberOfSamplesToRead = + frames2samples(numberOfFrames); + if (numberOfSamplesToRead <= sampleBufferSize) { + // efficient in-place transformation + const AudioSource::size_type readFrameCount = readSampleFrames( + numberOfFrames, sampleBuffer); + SampleUtil::copyMultiToStereo( + sampleBuffer, sampleBuffer, readFrameCount, getChannelCount()); + return readFrameCount; + } else { + // inefficient transformation through a temporary buffer + qDebug() << "Performance warning:" + << "Allocating a temporary buffer of size" + << numberOfSamplesToRead + << "for reading stereo samples." + << "The size of the provided sample buffer is" + << sampleBufferSize; + typedef std::vector SampleBuffer; + SampleBuffer tempBuffer(numberOfSamplesToRead); + const AudioSource::size_type readFrameCount = readSampleFrames( + numberOfFrames, &tempBuffer[0]); + SampleUtil::copyMultiToStereo( + sampleBuffer, &tempBuffer[0], readFrameCount, getChannelCount()); + return readFrameCount; + } } } } diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 7d50f021b32..3c88dd2f0ae 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -93,7 +93,7 @@ class AudioSource { return isFrameCountEmpty(); } - // The bitrate in kbit/s (optional). + // The optional bitrate in kbit/s (kbps). // Derived classes may set the actual (average) bitrate when // opening the file. The bitrate is not needed for decoding, // it is only used for informational purposes. @@ -140,36 +140,54 @@ class AudioSource { // Fills the buffer with samples from each channel starting // at the current frame seek position. // - // The required size of the sampleBuffer is numberOfSamples = - // frames2samples(numberOfFrames). The samples in the sampleBuffer - // are stored as consecutive sample frames with samples from - // each channel interleaved. + // The implicit minimum required capacity of the sampleBuffer is + // sampleBufferSize = frames2samples(numberOfFrames) + // Samples in the sampleBuffer are stored as consecutive sample + // frames with samples from each channel interleaved. // // Returns the actual number of frames that have been read which - // may be lower than the requested number of frames. The current - // frame seek position is moved forward to the next unread frame. - virtual size_type readSampleFrames(size_type numberOfFrames, + // might be lower than the requested number of frames when the end + // of the audio stream has been reached. The current frame seek + // position is moved forward towards the next unread frame. + virtual size_type readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) = 0; // Specialized function for explicitly reading stereo (= 2 channels) - // frames from an AudioSource. This is commonly used in Mixxx! + // frames from an AudioSource. This is the preferred method in Mixxx + // to read a stereo signal. // // If the source provides only a single channel (mono) the samples // of that channel will be doubled. If the source provides more - // than 2 channels only the first 2 channels will be read. The - // minimum required capacity of the sampleBuffer is numberOfFrames * 2. + // than 2 channels only the first 2 channels will be read. + // + // Most audio sources in Mixxx implicitly reduce multi-channel output + // to stereo during decoding. Other audio sources override this method + // with an optimized version that does not require a second pass through + // the sample data or that avoids the allocation of a temporary buffer + // when reducing multi-channel data to stereo. + // + // The minimum required capacity of the sampleBuffer is + // sampleBufferSize = numberOfFrames * 2 + // In order to avoid the implicit allocation of a temporary buffer + // when reducing multi-channel to stereo the caller must provide + // a sample buffer of size + // sampleBufferSize = frames2samples(numberOfFrames) // // Returns the actual number of frames that have been read which - // may be lower than the requested number of frames. The current - // frame seek position is moved forward towards the next unread - // frame. + // might be lower than the requested number of frames when the end + // of the audio stream has been reached. The current frame seek + // position is moved forward towards the next unread frame. // // Derived classes may provide an optimized version that doesn't // require any post-processing as done by this default implementation. - // Please note that the default implementation performs poorly when - // reducing more than 2 channels to stereo! - virtual size_type readSampleFramesStereo(size_type numberOfFrames, - sample_type* sampleBuffer); + // They may also have reduced space requirements on sampleBuffer, + // i.e. only the minimum size is required for an in-place + // transformation without temporary allocations. + virtual size_type readSampleFramesStereo( + size_type numberOfFrames, + sample_type* sampleBuffer, + size_type sampleBufferSize); protected: AudioSource(); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 05f139e7045..84e109e6039 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -130,20 +130,22 @@ Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame(diff_type frameIn Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, false); + return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, true); + size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { + return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize, bool readStereoSamples) { sample_type* outBuffer = sampleBuffer; - size_type framesRemaining = numberOfFrames; - while (0 < framesRemaining) { + const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); + size_type numberOfFramesRead = 0; + while (numberOfFramesTotal > numberOfFramesRead) { DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); @@ -169,7 +171,7 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( - m_decodeSampleBufferReadOffset; const size_type decodeBufferFrames = samples2frames( decodeBufferSamples); - const size_type framesToCopy = math_min(framesRemaining, decodeBufferFrames); + const size_type framesToCopy = math_min(decodeBufferFrames, numberOfFramesTotal - numberOfFramesRead); const size_type samplesToCopy = frames2samples(framesToCopy); if (readStereoSamples && !isChannelCountStereo()) { if (isChannelCountMono()) { @@ -189,12 +191,12 @@ Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( outBuffer += samplesToCopy; } m_decodeSampleBufferReadOffset += samplesToCopy; - framesRemaining -= framesToCopy; + numberOfFramesRead += framesToCopy; DEBUG_ASSERT( m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); } - return numberOfFrames - framesRemaining; + return numberOfFramesRead; } // flac callback methods diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index f878b4b1340..29afabe3978 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -22,7 +22,7 @@ class AudioSourceFLAC : public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; // callback methods FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t *bytes); @@ -41,7 +41,7 @@ class AudioSourceFLAC : public AudioSource { void close(); - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, bool readStereoSamples); + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); QFile m_file; diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index f36476f1a99..17aa643447e 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -84,7 +84,7 @@ Result AudioSourceMp3::open() { setChannelCount(kChannelCountDefault); setFrameRate(kFrameRateDefault); mad_timer_t madFileDuration = mad_timer_zero; - mad_units madUnits; + mad_units madUnits = MAD_UNITS_44100_HZ; // default value while ((m_madStream.bufend - m_madStream.this_frame) > 0) { mad_header madHeader; mad_header_init(&madHeader); @@ -292,20 +292,23 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { AudioSource::size_type AudioSourceMp3::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, false); + return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } AudioSource::size_type AudioSourceMp3::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, true); + size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { + return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } AudioSource::size_type AudioSourceMp3::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, + sample_type* sampleBuffer, + size_type sampleBufferSize, bool readStereoSamples) { - size_type framesRemaining = numberOfFrames; sample_type* pSampleBuffer = sampleBuffer; - while (0 < framesRemaining) { + const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); + size_type numberOfFramesRead = 0; + while (numberOfFramesTotal > numberOfFramesRead) { if (0 >= m_madSynthCount) { if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { @@ -330,10 +333,10 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( m_madSynthCount = m_madSynth.pcm.length; } const size_type madSynthOffset = m_madSynth.pcm.length - m_madSynthCount; - const size_type framesRead = math_min(m_madSynthCount, framesRemaining); + const size_type framesRead = math_min(m_madSynthCount, numberOfFramesTotal - numberOfFramesRead); m_madSynthCount -= framesRead; m_curFrameIndex += framesRead; - framesRemaining -= framesRead; + numberOfFramesRead += framesRead; if (NULL != pSampleBuffer) { if (isChannelCountMono()) { for (size_type i = 0; i < framesRead; ++i) { @@ -361,7 +364,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( } } } - return numberOfFrames - framesRemaining; + return numberOfFramesRead; } } // namespace Mixxx diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index b2c00ea6d90..e230953ad54 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -28,7 +28,7 @@ class AudioSourceMp3 : public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; private: explicit AudioSourceMp3(QString fileName); @@ -40,7 +40,7 @@ class AudioSourceMp3 : public AudioSource { inline size_type skipFrameSamples(size_type numberOfFrames) { return readSampleFrames(numberOfFrames, NULL); } - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, bool readStereoSamples); + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); QFile m_file; quint64 m_fileSize; diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 1f39c7e64f8..82c026162d9 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -75,24 +75,28 @@ AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, false); + return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } AudioSource::size_type AudioSourceOggVorbis::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, true); + size_type numberOfFrames, + sample_type* sampleBuffer, + size_type sampleBufferSize) { + return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer, + size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { - size_type readCount = 0; sample_type* nextSample = sampleBuffer; - while (readCount < numberOfFrames) { + const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); + size_type numberOfFramesRead = 0; + while (numberOfFramesTotal > numberOfFramesRead) { float** pcmChannels; int currentSection; const long readResult = ov_read_float(&m_vf, &pcmChannels, - numberOfFrames - readCount, ¤tSection); + numberOfFramesTotal - numberOfFramesRead, ¤tSection); if (0 == readResult) { // EOF break; // done @@ -121,13 +125,13 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( } } } - readCount += readResult; + numberOfFramesRead += readResult; } else { qWarning() << "Failed to read from OggVorbis file:" << readResult; break; // abort } } - return readCount; + return numberOfFramesRead; } } // namespace Mixxx diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index f524172e002..cc965245589 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -19,7 +19,7 @@ class AudioSourceOggVorbis: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; private: AudioSourceOggVorbis(); @@ -29,7 +29,8 @@ class AudioSourceOggVorbis: public AudioSource { void close(); size_type readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer, bool readStereoSamples); + sample_type* sampleBuffer, size_type sampleBufferSize, + bool readStereoSamples); OggVorbis_File m_vf; }; diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index d1b7acdb2d7..3b7876da01e 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -91,25 +91,26 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( } AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer) { - size_type readCount = 0; - while (readCount < numberOfFrames) { + size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { + const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); + size_type numberOfFramesRead = 0; + while (numberOfFramesTotal > numberOfFramesRead) { int readResult = op_read_float_stereo(m_pOggOpusFile, - sampleBuffer + (readCount * 2), - (numberOfFrames - readCount) * 2); + sampleBuffer + (numberOfFramesRead * 2), + (numberOfFramesTotal - numberOfFramesRead) * 2); if (0 == readResult) { // EOF break; // done } if (0 < readResult) { - readCount += readResult; + numberOfFramesRead += readResult; } else { qWarning() << "Failed to read sample data from OggOpus file:" << readResult; break; // abort } } - return readCount; + return numberOfFramesRead; } } // namespace Mixxx diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 6b0c5d08caa..222f71ed396 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -22,7 +22,7 @@ class AudioSourceOpus: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; private: AudioSourceOpus(); From 898cf30933a336b7599d1738882befae60d2857b Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 13:43:47 +0100 Subject: [PATCH 055/481] M4A: Fix remaining review comments --- plugins/soundsourcem4a/audiosourcem4a.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 2ee8515fd15..e1cd3516296 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -36,7 +36,9 @@ const MP4SampleId kMinSampleBlockId = 1; // Two blocks of samples seems to be enough here. const MP4SampleId kNumberOfPrefetchSampleBlocks = 2; -MP4TrackId findAacTrackId(MP4FileHandle hFile) { +// Searches for the first audio track in the MP4 file that +// suits our needs. +MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { const MP4TrackId maxTrackId = MP4GetNumberOfTracks(hFile, NULL, 0); for (MP4TrackId trackId = 1; trackId <= maxTrackId; ++trackId) { const char* trackType = MP4GetTrackType(hFile, trackId); @@ -104,7 +106,7 @@ Result AudioSourceM4A::open(QString fileName) { return ERR; } - m_trackId = findAacTrackId(m_hFile); + m_trackId = findFirstAudioTrackId(m_hFile); if (MP4_INVALID_TRACK_ID == m_trackId) { qWarning() << "No AAC track found in file:" << fileName; return ERR; @@ -117,7 +119,12 @@ Result AudioSourceM4A::open(QString fileName) { } m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; - m_inputBuffer.resize(MP4GetTrackMaxSampleSize(m_hFile, m_trackId), 0); + // Determine the maximum input size (in bytes) of a + // sample block for the selected track. + const u_int32_t maxSampleBlockInputSize = + MP4GetTrackMaxSampleSize(m_hFile, m_trackId); + m_inputBuffer.resize(maxSampleBlockInputSize, 0); + // Initially the input buffer is empty m_inputBufferOffset = 0; m_inputBufferLength = 0; @@ -167,7 +174,8 @@ Result AudioSourceM4A::open(QString fileName) { // Allocate one block more than the number of sample blocks // that are prefetched - const SampleBuffer::size_type prefetchSampleBufferSize = (kNumberOfPrefetchSampleBlocks + 1) * frames2samples(kFramesPerSampleBlock); + const SampleBuffer::size_type prefetchSampleBufferSize = + (kNumberOfPrefetchSampleBlocks + 1) * frames2samples(kFramesPerSampleBlock); m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); // invalidate current frame index From 6931aa84a17c97d13c1367402766666a5d2760e8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 14:21:23 +0100 Subject: [PATCH 056/481] Common base class for SoundSource plugins Reduce code duplication in SoundSource plugins by introducing a common base class. --- build/depends.py | 1 + plugins/soundsourcem4a/SConscript | 4 +- plugins/soundsourcem4a/audiosourcem4a.cpp | 2 - plugins/soundsourcem4a/soundsourcem4a.cpp | 27 ++++--------- plugins/soundsourcem4a/soundsourcem4a.h | 6 +-- plugins/soundsourcemediafoundation/SConscript | 2 +- .../soundsourcemediafoundation.cpp | 25 +++++++++++- .../soundsourcemediafoundation.h | 40 ++++--------------- plugins/soundsourcewv/SConscript | 1 + plugins/soundsourcewv/soundsourcewv.cpp | 27 ++++--------- plugins/soundsourcewv/soundsourcewv.h | 6 +-- src/sources/soundsourceplugin.cpp | 27 +++++++++++++ src/sources/soundsourceplugin.h | 25 ++++++++++++ 13 files changed, 108 insertions(+), 85 deletions(-) create mode 100644 src/sources/soundsourceplugin.cpp create mode 100644 src/sources/soundsourceplugin.h diff --git a/build/depends.py b/build/depends.py index 2197c392b42..07bbb2da143 100644 --- a/build/depends.py +++ b/build/depends.py @@ -656,6 +656,7 @@ def sources(self, build): "errordialoghandler.cpp", "upgrade.cpp", + "sources/soundsourceplugin.cpp", "sources/soundsource.cpp", "sources/audiosource.cpp", diff --git a/plugins/soundsourcem4a/SConscript b/plugins/soundsourcem4a/SConscript index 46d3449d144..f4a49fce288 100644 --- a/plugins/soundsourcem4a/SConscript +++ b/plugins/soundsourcem4a/SConscript @@ -14,10 +14,10 @@ m4a_sources = [ "soundsourcem4a.cpp", "audiosourcem4a.cpp", "sources/soundsource.cpp", + "sources/soundsourceplugin.cpp", "sources/audiosource.cpp", "metadata/trackmetadata.cpp", - "metadata/trackmetadatataglib.cpp", - "sampleutil.cpp", + "metadata/trackmetadatataglib.cpp" ] #Tell SCons to build the SoundSourceM4A plugin diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index e1cd3516296..2d5427478e0 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -1,7 +1,5 @@ #include "audiosourcem4a.h" -#include "sampleutil.h" - #ifdef __WINDOWS__ #include #include diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index 72a0c6b35f5..e8384bb1593 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -31,7 +31,7 @@ QList SoundSourceM4A::supportedFileExtensions() { } SoundSourceM4A::SoundSourceM4A(QString fileName) - : SoundSource(fileName, "m4a") { + : SoundSourcePlugin(fileName, "m4a") { } Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { @@ -86,25 +86,12 @@ extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { } extern "C" MY_EXPORT char** supportedFileExtensions() { - QList exts = Mixxx::SoundSourceM4A::supportedFileExtensions(); - //Convert to C string array. - char** c_exts = (char**)malloc((exts.count() + 1) * sizeof(char*)); - for (int i = 0; i < exts.count(); i++) - { - QByteArray qba = exts[i].toUtf8(); - c_exts[i] = strdup(qba.constData()); - qDebug() << c_exts[i]; - } - c_exts[exts.count()] = NULL; //NULL terminate the list - - return c_exts; + const QList supportedFileExtensions( + Mixxx::SoundSourceM4A::supportedFileExtensions()); + return Mixxx::SoundSourcePlugin::allocFileExtensions( + supportedFileExtensions); } -extern "C" MY_EXPORT void freeFileExtensions(char **exts) { - if (exts) { - for (int i(0); exts[i]; ++i) { - free(exts[i]); - } - free(exts); - } +extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions) { + Mixxx::SoundSourcePlugin::freeFileExtensions(fileExtensions); } diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 659318fb3de..14988412391 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -17,7 +17,7 @@ #ifndef SOUNDSOURCEM4A_H #define SOUNDSOURCEM4A_H -#include "sources/soundsource.h" +#include "sources/soundsourceplugin.h" #include "defs_version.h" //As per QLibrary docs: http://doc.trolltech.com/4.6/qlibrary.html#resolve @@ -30,7 +30,7 @@ namespace Mixxx { -class SoundSourceM4A : public SoundSource { +class SoundSourceM4A : public SoundSourcePlugin { public: static QList supportedFileExtensions(); @@ -48,6 +48,6 @@ extern "C" MY_EXPORT const char* getMixxxVersion(); extern "C" MY_EXPORT int getSoundSourceAPIVersion(); extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName); extern "C" MY_EXPORT char** supportedFileExtensions(); -extern "C" MY_EXPORT void freeFileExtensions(char **exts); +extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions); #endif diff --git a/plugins/soundsourcemediafoundation/SConscript b/plugins/soundsourcemediafoundation/SConscript index 4e72423911e..e58743b7393 100644 --- a/plugins/soundsourcemediafoundation/SConscript +++ b/plugins/soundsourcemediafoundation/SConscript @@ -18,7 +18,7 @@ if int(build.flags['mediafoundation']): else: env["LINKFLAGS"].remove("/subsystem:windows,5.01") ssmediafoundation_bin = env.SharedLibrary('soundsourcemediafoundation', - ['soundsourcemediafoundation.cpp', 'audiosourcemediafoundation.cpp', 'sources/soundsource.cpp', 'sources/audiosource.cpp', 'metadata/trackmetadata.cpp', 'metadata/trackmetadatataglib.cpp', 'sampleutil.cpp'], + ['soundsourcemediafoundation.cpp', 'audiosourcemediafoundation.cpp', 'sources/soundsource.cpp', 'sources/audiosource.cpp', 'metadata/trackmetadata.cpp', 'metadata/trackmetadatataglib.cpp'], LINKCOM = [env['LINKCOM'], 'mt.exe -nologo -manifest ${TARGET}.manifest -outputresource:$TARGET;1']) Return("ssmediafoundation_bin") diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp index fc60d8d614e..7f3736697e0 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.cpp @@ -38,7 +38,7 @@ QList SoundSourceMediaFoundation::supportedFileExtensions() { } SoundSourceMediaFoundation::SoundSourceMediaFoundation(QString fileName) - : SoundSource(fileName, "m4a") { + : SoundSourcePlugin(fileName, "m4a") { } Result SoundSourceMediaFoundation::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { @@ -78,3 +78,26 @@ QImage SoundSourceMediaFoundation::parseCoverArt() const { Mixxx::AudioSourcePointer SoundSourceMediaFoundation::open() const { return Mixxx::AudioSourceMediaFoundation::create(getFilename()); } + +extern "C" MY_EXPORT const char* getMixxxVersion() { + return VERSION; +} + +extern "C" MY_EXPORT int getSoundSourceAPIVersion() { + return MIXXX_SOUNDSOURCE_API_VERSION; +} + +extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { + return new SoundSourceMediaFoundation(fileName); +} + +extern "C" MY_EXPORT char** supportedFileExtensions() { + const QList supportedFileExtensions( + SoundSourceMediaFoundation::supportedFileExtensions()); + return Mixxx::SoundSourcePlugin::allocFileExtensions( + supportedFileExtensions); +} + +extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions) { + Mixxx::SoundSourcePlugin::freeFileExtensions(fileExtensions); +} diff --git a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h index 3b16826bc66..301fac4d924 100644 --- a/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/soundsourcemediafoundation.h @@ -20,7 +20,7 @@ #ifndef SOUNDSOURCEMEDIAFOUNDATION_H #define SOUNDSOURCEMEDIAFOUNDATION_H -#include "sources/soundsource.h" +#include "sources/soundsourceplugin.h" #include "defs_version.h" #ifdef Q_OS_WIN @@ -29,7 +29,7 @@ #define MY_EXPORT #endif -class SoundSourceMediaFoundation : public Mixxx::SoundSource { +class SoundSourceMediaFoundation : public Mixxx::SoundSourcePlugin { public: static QList supportedFileExtensions(); @@ -41,36 +41,10 @@ class SoundSourceMediaFoundation : public Mixxx::SoundSource { Mixxx::AudioSourcePointer open() const /*override*/; }; -extern "C" MY_EXPORT const char* getMixxxVersion() { - return VERSION; -} - -extern "C" MY_EXPORT int getSoundSourceAPIVersion() { - return MIXXX_SOUNDSOURCE_API_VERSION; -} - -extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { - return new SoundSourceMediaFoundation(fileName); -} - -extern "C" MY_EXPORT char** supportedFileExtensions() { - QList exts = SoundSourceMediaFoundation::supportedFileExtensions(); - //Convert to C string array. - char** c_exts = (char**) malloc((exts.count() + 1) * sizeof(char*)); - for (int i = 0; i < exts.count(); i++) { - QByteArray qba = exts[i].toUtf8(); - c_exts[i] = strdup(qba.constData()); - qDebug() << c_exts[i]; - } - c_exts[exts.count()] = NULL; //NULL terminate the list - - return c_exts; -} - -extern "C" MY_EXPORT void freeFileExtensions(char **exts) { - for (int i(0); exts[i]; ++i) - free(exts[i]); - free(exts); -} +extern "C" MY_EXPORT const char* getMixxxVersion(); +extern "C" MY_EXPORT int getSoundSourceAPIVersion(); +extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName); +extern "C" MY_EXPORT char** supportedFileExtensions(); +extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions); #endif // ifndef SOUNDSOURCEMEDIAFOUNDATION_H diff --git a/plugins/soundsourcewv/SConscript b/plugins/soundsourcewv/SConscript index 787af35d352..322fdaf9f75 100644 --- a/plugins/soundsourcewv/SConscript +++ b/plugins/soundsourcewv/SConscript @@ -14,6 +14,7 @@ wv_sources = [ "soundsourcewv.cpp", "audiosourcewv.cpp", "sources/soundsource.cpp", + "sources/soundsourceplugin.cpp", "sources/audiosource.cpp", "metadata/trackmetadata.cpp", "metadata/trackmetadatataglib.cpp" diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 9cc424c647c..3d019e7615d 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -14,7 +14,7 @@ QList SoundSourceWV::supportedFileExtensions() { } SoundSourceWV::SoundSourceWV(QString fileName) - : SoundSource(fileName, "wv") { + : SoundSourcePlugin(fileName, "wv") { } Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { @@ -69,25 +69,12 @@ extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { } extern "C" MY_EXPORT char** supportedFileExtensions() { - QList exts = Mixxx::SoundSourceWV::supportedFileExtensions(); - //Convert to C string array. - char** c_exts = (char**)malloc((exts.count() + 1) * sizeof(char*)); - for (int i = 0; i < exts.count(); i++) - { - QByteArray qba = exts[i].toUtf8(); - c_exts[i] = strdup(qba.constData()); - qDebug() << c_exts[i]; - } - c_exts[exts.count()] = NULL; //NULL terminate the list - - return c_exts; + const QList supportedFileExtensions( + Mixxx::SoundSourceWV::supportedFileExtensions()); + return Mixxx::SoundSourcePlugin::allocFileExtensions( + supportedFileExtensions); } -extern "C" MY_EXPORT void freeFileExtensions(char **exts) { - if (exts) { - for (int i(0); exts[i]; ++i) { - free(exts[i]); - } - free(exts); - } +extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions) { + Mixxx::SoundSourcePlugin::freeFileExtensions(fileExtensions); } diff --git a/plugins/soundsourcewv/soundsourcewv.h b/plugins/soundsourcewv/soundsourcewv.h index 5f50dd41b82..f2d5ffbc048 100644 --- a/plugins/soundsourcewv/soundsourcewv.h +++ b/plugins/soundsourcewv/soundsourcewv.h @@ -1,7 +1,7 @@ #ifndef SOUNDSOURCEWV_H #define SOUNDSOURCEWV_H -#include "sources/soundsource.h" +#include "sources/soundsourceplugin.h" #include "defs_version.h" #ifdef Q_OS_WIN @@ -12,7 +12,7 @@ namespace Mixxx { -class SoundSourceWV: public SoundSource { +class SoundSourceWV: public SoundSourcePlugin { public: static QList supportedFileExtensions(); @@ -30,6 +30,6 @@ extern "C" MY_EXPORT const char* getMixxxVersion(); extern "C" MY_EXPORT int getSoundSourceAPIVersion(); extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName); extern "C" MY_EXPORT char** supportedFileExtensions(); -extern "C" MY_EXPORT void freeFileExtensions(char **exts); +extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions); #endif diff --git a/src/sources/soundsourceplugin.cpp b/src/sources/soundsourceplugin.cpp new file mode 100644 index 00000000000..1d814ec69f4 --- /dev/null +++ b/src/sources/soundsourceplugin.cpp @@ -0,0 +1,27 @@ +#include "soundsourceplugin.h" + +namespace Mixxx { + +char** SoundSourcePlugin::allocFileExtensions(const QList& supportedFileExtensions) { + //Convert to C string array. + char** fileExtensions = (char**)malloc((supportedFileExtensions.count() + 1) * sizeof(char*)); + for (int i = 0; i < supportedFileExtensions.count(); i++) { + QByteArray qba = supportedFileExtensions[i].toUtf8(); + fileExtensions[i] = strdup(qba.constData()); + qDebug() << fileExtensions[i]; + } + fileExtensions[supportedFileExtensions.count()] = NULL; //NULL terminate the list + + return fileExtensions; +} + +void SoundSourcePlugin::freeFileExtensions(char** fileExtensions) { + if (fileExtensions) { + for (int i(0); fileExtensions[i]; ++i) { + free(fileExtensions[i]); + } + free(fileExtensions); + } +} + +} // namespace Mixxx diff --git a/src/sources/soundsourceplugin.h b/src/sources/soundsourceplugin.h new file mode 100644 index 00000000000..b6c3075781d --- /dev/null +++ b/src/sources/soundsourceplugin.h @@ -0,0 +1,25 @@ +#ifndef MIXXX_SOUNDSOURCEPLUGIN_H +#define MIXXX_SOUNDSOURCEPLUGIN_H + +#include "sources/soundsource.h" + +namespace Mixxx { + +// Common base class for SoundSource plugins +class SoundSourcePlugin : public SoundSource { +public: + static char** allocFileExtensions(const QList& supportedFileExtensions); + static void freeFileExtensions(char** fileExtensions); + +protected: + inline explicit SoundSourcePlugin(QString sFilename) + : SoundSource(sFilename) { + } + inline SoundSourcePlugin(QString sFilename, QString sType) + : SoundSource(sFilename, sType) { + } +}; + +} // namespace Mixxx + +#endif From 44f60a9ca6aec7776da7e0ffe27a304ee26f3d43 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 14:34:09 +0100 Subject: [PATCH 057/481] Add TODO(XXX) to the Microsoft Media Foundation plugin files This has to be done by someone with a Windows development environment. --- .../audiosourcemediafoundation.cpp | 6 ++++++ .../soundsourcemediafoundation/audiosourcemediafoundation.h | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index 95f0c5aea82..d3b0dd31d5b 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -1,3 +1,9 @@ +// TODO(XXX): This implementation has been copied from the +// original file soundsourcemediafoundation.cpp and needs +// a major refactoring! Please refer to the helpful comments +// of daschuer in the following pull requests: +// https://github.com/mixxxdj/mixxx/pull/411 + #include "audiosourcemediafoundation.h" #include diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h index 870e58fede2..63ea718e66f 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.h @@ -1,3 +1,9 @@ +// TODO(XXX): This implementation has been copied from the +// original file soundsourcemediafoundation.h and needs +// a major refactoring! Please refer to the helpful comments +// of daschuer in the following pull requests: +// https://github.com/mixxxdj/mixxx/pull/411 + #ifndef AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H #define AUDIOSOURCEMEDIAFOUNDATIONMEDIAFOUNDATION_H From 6842463bbf647f051ef8e836b716f670f1dd785d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sat, 10 Jan 2015 19:57:33 +0100 Subject: [PATCH 058/481] Fix M4A decoding after refactoring --- plugins/soundsourcem4a/audiosourcem4a.cpp | 79 +++++++++++------------ 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 2d5427478e0..03aa29a4505 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -115,7 +115,6 @@ Result AudioSourceM4A::open(QString fileName) { qWarning() << "Failed to read file structure:" << fileName; return ERR; } - m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; // Determine the maximum input size (in bytes) of a // sample block for the selected track. @@ -128,17 +127,17 @@ Result AudioSourceM4A::open(QString fileName) { if (!m_hDecoder) { // lazy initialization of the AAC decoder - m_hDecoder = faacDecOpen(); + m_hDecoder = NeAACDecOpen(); if (!m_hDecoder) { qWarning() << "Failed to open the AAC decoder!"; return ERR; } - faacDecConfigurationPtr pDecoderConfig = faacDecGetCurrentConfiguration( + NeAACDecConfigurationPtr pDecoderConfig = NeAACDecGetCurrentConfiguration( m_hDecoder); pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ pDecoderConfig->downMatrix = 1; /* 5.1 -> stereo */ pDecoderConfig->defObjectType = LC; - if (!faacDecSetConfiguration(m_hDecoder, pDecoderConfig)) { + if (!NeAACDecSetConfiguration(m_hDecoder, pDecoderConfig)) { qWarning() << "Failed to configure AAC decoder!"; return ERR; } @@ -149,7 +148,7 @@ Result AudioSourceM4A::open(QString fileName) { if (!MP4GetTrackESConfiguration(m_hFile, m_trackId, &configBuffer, &configBufferSize)) { /* failed to get mpeg-4 audio config... this is ok. - * faacDecInit2() will simply use default values instead. + * NeAACDecInit2() will simply use default values instead. */ qWarning() << "Failed to read the MP4 audio configuration. Continuing with default values."; @@ -157,7 +156,7 @@ Result AudioSourceM4A::open(QString fileName) { SAMPLERATE_TYPE sampleRate; unsigned char channelCount; - if (0 > faacDecInit2(m_hDecoder, configBuffer, configBufferSize, + if (0 > NeAACDecInit2(m_hDecoder, configBuffer, configBufferSize, &sampleRate, &channelCount)) { free(configBuffer); qWarning() << "Failed to initialize the AAC decoder!"; @@ -176,9 +175,9 @@ Result AudioSourceM4A::open(QString fileName) { (kNumberOfPrefetchSampleBlocks + 1) * frames2samples(kFramesPerSampleBlock); m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); - // invalidate current frame index + m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; m_curFrameIndex = getFrameCount(); - // seek to beginning of file + // Seek to the beginning of the file if (0 != seekSampleFrame(0)) { qWarning() << "Failed to seek to the beginning of the file!"; return ERR; @@ -217,9 +216,11 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { if (!isValidSampleBlockId(sampleBlockId)) { return m_curFrameIndex; } - if ((m_curSampleBlockId != sampleBlockId) || (frameIndex < m_curFrameIndex)) { - // Restart decoding one or more blocks of samples backwards to - // avoid audible glitches. + if ((frameIndex < m_curFrameIndex) || + !isValidSampleBlockId(m_curSampleBlockId) || + (sampleBlockId > (m_curSampleBlockId + kNumberOfPrefetchSampleBlocks))) { + // Restart decoding one or more blocks of samples backwards + // from the calculated starting block to avoid audible glitches. // Implementation note: The type MP4SampleId is unsigned so we // need to be careful when subtracting! if ((kMinSampleBlockId + kNumberOfPrefetchSampleBlocks) < sampleBlockId) { @@ -227,10 +228,8 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { } else { m_curSampleBlockId = kMinSampleBlockId; } + NeAACDecPostSeekReset(m_hDecoder, m_curSampleBlockId); m_curFrameIndex = (m_curSampleBlockId - kMinSampleBlockId) * kFramesPerSampleBlock; - // rryan 9/2009 -- the documentation is sketchy on this, but I think that - // it tells the decoder that you are seeking so it should flush its state - faacDecPostSeekReset(m_hDecoder, m_curSampleBlockId); // discard input buffer m_inputBufferOffset = 0; m_inputBufferLength = 0; @@ -260,18 +259,23 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( // reset input buffer m_inputBufferOffset = 0; m_inputBufferLength = 0; - // fill input buffer with next block of samples - const MP4SampleId nextSampleBlockId = m_curSampleBlockId + 1; - if (isValidSampleBlockId(nextSampleBlockId)) { + if (isValidSampleBlockId(m_curSampleBlockId)) { + // fill input buffer with next block of samples u_int8_t* pInputBuffer = &m_inputBuffer[0]; u_int32_t inputBufferLength = m_inputBuffer.size(); // in/out parameter - if (!MP4ReadSample(m_hFile, m_trackId, nextSampleBlockId, + if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleBlockId, &pInputBuffer, &inputBufferLength, NULL, NULL, NULL, NULL)) { - qWarning() << "Failed to read MP4 input data for sample block" << m_curSampleBlockId; + qWarning() << "Failed to read MP4 input data for sample block" + << m_curSampleBlockId + << "(" + << "min =" << kMinSampleBlockId + << "," + << "max =" << m_maxSampleBlockId + << ")"; break; // abort } - m_curSampleBlockId = nextSampleBlockId; + ++m_curSampleBlockId; m_inputBufferLength = inputBufferLength; } } @@ -280,37 +284,25 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( // EOF break; // done } - faacDecFrameInfo decFrameInfo; + NeAACDecFrameInfo decFrameInfo; decFrameInfo.bytesconsumed = 0; decFrameInfo.samples = 0; // decode samples into sampleBuffer - const size_type decodeBufferCapacityInBytes = frames2samples( - numberOfFrames - numberOfFramesRemaining) * sizeof(*sampleBuffer); + const size_type decodeBufferCapacityInBytes = + frames2samples(numberOfFramesRemaining) * sizeof(*sampleBuffer); DEBUG_ASSERT(0 < decodeBufferCapacityInBytes); - void* pDecodeBuffer = pSampleBuffer; + void* pDecodeBuffer = pSampleBuffer; // in/out parameter NeAACDecDecode2(m_hDecoder, &decFrameInfo, &m_inputBuffer[m_inputBufferOffset], - m_inputBufferLength, + m_inputBufferLength - m_inputBufferOffset, &pDecodeBuffer, decodeBufferCapacityInBytes); + DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); // verify the in/out parameter if (0 != decFrameInfo.error) { qWarning() << "AAC decoding error:" - << faacDecGetErrorMessage(decFrameInfo.error); + << NeAACDecGetErrorMessage(decFrameInfo.error); break; // abort } - // samples should have been decoded into our own buffer - DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); - pSampleBuffer += decFrameInfo.samples; - // only the input data that is available should have been read - DEBUG_ASSERT( - decFrameInfo.bytesconsumed - <= (m_inputBufferLength / sizeof(m_inputBuffer[0]))); - // consume input data - m_inputBufferOffset += decFrameInfo.bytesconsumed - * sizeof(m_inputBuffer[0]); - m_inputBufferLength -= decFrameInfo.bytesconsumed - * sizeof(m_inputBuffer[0]); - m_curFrameIndex += samples2frames(decFrameInfo.samples); - // verify output sample data for consistency + // verify decoded sample data for consistency if (getChannelCount() != decFrameInfo.channels) { qWarning() << "Corrupt or unsupported AAC file:" "Unexpected number of channels" @@ -323,6 +315,13 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( << decFrameInfo.samplerate << "<>" << getFrameRate(); break; // abort } + // consume input data + m_inputBufferOffset += decFrameInfo.bytesconsumed; + // consume decoded samples + pSampleBuffer += decFrameInfo.samples; + const size_type numberOfFramesDecoded = samples2frames(decFrameInfo.samples); + numberOfFramesRemaining -= numberOfFramesDecoded; + m_curFrameIndex += numberOfFramesDecoded; } return numberOfFrames - numberOfFramesRemaining; } From 3911475f34673e9dc919dede7a17aee2b2c343e5 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 11 Jan 2015 11:25:27 +0100 Subject: [PATCH 059/481] not working --- src/effects/native/paneffect.cpp | 52 ++++++++++++++------- src/effects/native/paneffect.h | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 15 deletions(-) diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp index 8a461edd724..dfd628a648d 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/paneffect.cpp @@ -104,12 +104,12 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, return; // DISABLED = 0x00 } - CSAMPLE lfoPeriod = roundf(m_pPeriodParameter->value()) * (float)numSamples / 2.0f; + CSAMPLE period = roundf(m_pPeriodParameter->value()) * (float)numSamples / 2.0f; CSAMPLE stepFrac = m_pStrengthParameter->value(); CSAMPLE depth = m_pDepthParameter->value(); float rampingTreshold = m_pRampingParameter->value(); - if (gs.time > lfoPeriod || 0x03 == enableState) { // ENABLING = 0x03 + if (gs.time > period || 0x03 == enableState) { // ENABLING = 0x03 gs.time = 0; } @@ -132,17 +132,28 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // todo (jclaveau) : stereo SampleUtil::mixStereoToMono(pOutput, pInput, numSamples); - // Pingpong the output. If the pingpong value is zero, all of the - // math below should result in a simple copy of delay buf to pOutput. - // todo (jclaveau) : ramping if period changes - // todo (jclaveau) : ramping if curve is square - - CSAMPLE maxDiff = 0.0f; - CSAMPLE maxDiff2 = 0.0f; - CSAMPLE frac; + /** stereo : + * + position 0 <=> 0.5 <=> 1 => gauche et droite au milieu + * + position 0.25 => droite à droite et gauche à droite + * + position 0.75 => droite à gauche et gauche à gauche + * law coef : sqrt(2) 1 sqrt(2) + * position : 1 0 0.5 + * + * position left : + * channel left : 0 -> pi/4 -> pi/2 -> pi/4 + * + * position right : + * channel right : pi/2 -> 3pi/4 -> pi -> 3pi/4 + * + */ + + // RampedSample* frac = new RampedSample(); + // frac->setRamping(rampingTreshold); + RampedSample frac(rampingTreshold); + // frac.setRamping(rampingTreshold); for (unsigned int i = 0; i + 1 < numSamples; i += 2) { - CSAMPLE periodFraction = CSAMPLE(gs.time) / lfoPeriod; + CSAMPLE periodFraction = CSAMPLE(gs.time) / period; // current quarter in the trigonometric circle float quarter = floorf(periodFraction * 4.0f); @@ -150,9 +161,10 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // part of the period fraction being steps (not in the slope) CSAMPLE stepsFractionPart = floorf((quarter+1.0f)/2.0f) * stepFrac; - // float inInterval = fmod( periodFraction, (lfoPeriod / 2.0) ); + // float inInterval = fmod( periodFraction, (period / 2.0) ); float inInterval = fmod( periodFraction, 0.5f ); + CSAMPLE position; if (inInterval > u && inInterval < ( u + stepFrac)) { // at full left or full right @@ -162,10 +174,18 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, position = (periodFraction - stepsFractionPart) * a; } - // set the curve between 0 and 1 - frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; + // transform the position into a sinusoid (between 0 and 1) + frac = (sinf(M_PI * 2.0f * position) + 1.0f) / 2.0f; + + // todo (jclaveau) for stereo : + // frac left in left + // frac left in right + // frac right in left + // frac right in right + // + /* if (oldFrac != -1.0f) { float diff = frac - oldFrac; @@ -179,6 +199,8 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, } oldFrac = frac; + */ + pOutput[i] = pOutput[i] * (1 - depth) + pOutput[i] * frac * lawCoef * depth; @@ -194,7 +216,7 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, /** / qDebug() << "stepFrac" << stepFrac << "| time :" << gs.time - // << "| period :" << lfoPeriod + // << "| period :" << period << "| a :" << a // coef of slope between 1 and -1 // << "| lawCoef :" << lawCoef // << "| enableState :" << enableState diff --git a/src/effects/native/paneffect.h b/src/effects/native/paneffect.h index 38468269c2f..8ae2396138b 100644 --- a/src/effects/native/paneffect.h +++ b/src/effects/native/paneffect.h @@ -11,6 +11,15 @@ #include "effects/effectprocessor.h" #include "sampleutil.h" +struct PanSquarySinusoid { + PanSquarySinusoid() { + lastFract = -1; + } + ~PanSquarySinusoid() { + } + CSAMPLE lastFract; +}; + struct PanGroupState { PanGroupState() { time = 0; @@ -20,6 +29,74 @@ struct PanGroupState { unsigned int time; }; +class RampedSample { + public: + RampedSample(); + inline RampedSample(const float newMaxDifference){ + setRamping(newMaxDifference); + } + + virtual ~RampedSample(); + inline void setRamping(const float newMaxDifference){ + maxDifference = newMaxDifference; + } + + inline RampedSample & operator=(const float &newValue) { + CSAMPLE difference = currentValue - newValue; + if (difference > maxDifference) + currentValue = maxDifference; + else + currentValue = newValue; + + return *this; + } + + inline RampedSample & operator-(float diff) { + CSAMPLE newValue = currentValue - diff; + CSAMPLE difference = currentValue - newValue; + if (difference > maxDifference) + currentValue = maxDifference; + else + currentValue = newValue; + + return *this; + } + + inline RampedSample & operator*(float times) { + CSAMPLE newValue = times * currentValue; + + CSAMPLE difference = currentValue - newValue; + if (difference > maxDifference) + currentValue = maxDifference; + else + currentValue = newValue; + + return *this; + } + + inline operator float() { + return value(); + } + + inline RampedSample & operator=(const RampedSample &that){ + CSAMPLE difference = currentValue - that.value(); + if (difference > maxDifference) + currentValue = maxDifference; + else + currentValue = that.value(); + + return *this; + } + + inline CSAMPLE value() const { + return currentValue; + } + + private: + float maxDifference; + CSAMPLE currentValue; +}; + class PanEffect : public GroupEffectProcessor { public: PanEffect(EngineEffect* pEffect, const EffectManifest& manifest); From 3cd0324e87275edf712df9aa3c94f3505e18e657 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 11 Jan 2015 14:24:52 +0100 Subject: [PATCH 060/481] Improve and fix AudioTagger - The preferred tag for each file type is always written - All optional tags are only written if they already exist - Replace plain with smart pointer --- src/metadata/audiotagger.cpp | 113 +++++++++++++-------------- src/metadata/trackmetadatataglib.cpp | 77 ++++++++++-------- src/metadata/trackmetadatataglib.h | 57 +++++++------- 3 files changed, 130 insertions(+), 117 deletions(-) diff --git a/src/metadata/audiotagger.cpp b/src/metadata/audiotagger.cpp index 0655f471d79..0331f0e0465 100644 --- a/src/metadata/audiotagger.cpp +++ b/src/metadata/audiotagger.cpp @@ -2,6 +2,8 @@ #include "metadata/trackmetadatataglib.h" +#include "util/assert.h" + #include #include #include @@ -11,6 +13,9 @@ #include #include #include +#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) +#include +#endif #include @@ -24,70 +29,64 @@ AudioTagger::~AudioTagger() { } bool AudioTagger::save(const Mixxx::TrackMetadata& trackMetadata) { - TagLib::File* file = NULL; + QScopedPointer pFile; - const QString& filePath = m_file.canonicalFilePath(); - QByteArray fileBA = filePath.toLocal8Bit(); + const QString filePath(m_file.canonicalFilePath()); + const QByteArray filePathByteArray(m_file.canonicalFilePath().toLocal8Bit()); + const char* filePathChars = filePathByteArray.constData(); if (filePath.endsWith(".mp3", Qt::CaseInsensitive)) { - file = new TagLib::MPEG::File(fileBA.constData()); - // process special ID3 fields, APEv2 fiels, etc - - // If the mp3 has no ID3v2 tag, we create a new one and add the TBPM and TKEY frame - writeID3v2Tag(((TagLib::MPEG::File*) file)->ID3v2Tag(true), trackMetadata); - // If the mp3 has an APE tag, we update - writeAPETag(((TagLib::MPEG::File*) file)->APETag(false), trackMetadata); - } - - if (filePath.endsWith(".m4a", Qt::CaseInsensitive)) { - file = new TagLib::MP4::File(fileBA.constData()); - // process special ID3 fields, APEv2 fiels, etc - writeMP4Tag(((TagLib::MP4::File*) file)->tag(), trackMetadata); - } - if (filePath.endsWith(".ogg", Qt::CaseInsensitive)) { - file = new TagLib::Ogg::Vorbis::File(fileBA.constData()); - // process special ID3 fields, APEv2 fiels, etc - writeXiphComment(((TagLib::Ogg::Vorbis::File*)file)->tag(), trackMetadata); - } - if (filePath.endsWith(".wav", Qt::CaseInsensitive)) { - file = new TagLib::RIFF::WAV::File(fileBA.constData()); - //If the flac has no ID3v2 tag, we create a new one and add the TBPM and TKEY frame - writeID3v2Tag(((TagLib::RIFF::WAV::File*)file)->tag(), trackMetadata); - } - if (filePath.endsWith(".flac", Qt::CaseInsensitive)) { - file = new TagLib::FLAC::File(fileBA.constData()); - - //If the flac has no ID3v2 tag, we create a new one and add the TBPM and TKEY frame - writeID3v2Tag(((TagLib::FLAC::File*)file)->ID3v2Tag(true), trackMetadata); - //If the flac has no APE tag, we create a new one and add the TBPM and TKEY frame - writeXiphComment(((TagLib::FLAC::File*) file)->xiphComment(true), trackMetadata); - - } - if (filePath.endsWith(".aif", Qt::CaseInsensitive) || + QScopedPointer pMpegFile( + new TagLib::MPEG::File(filePathChars)); + writeID3v2Tag(pMpegFile->ID3v2Tag(true), trackMetadata); // mandatory + writeAPETag(pMpegFile->APETag(false), trackMetadata); // optional + pFile.reset(pMpegFile.take()); // transfer ownership + } else if (filePath.endsWith(".m4a", Qt::CaseInsensitive)) { + QScopedPointer pMp4File( + new TagLib::MP4::File(filePathChars)); + writeMP4Tag(pMp4File->tag(), trackMetadata); + pFile.reset(pMp4File.take()); // transfer ownership + } else if (filePath.endsWith(".ogg", Qt::CaseInsensitive)) { + QScopedPointer pOggFile( + new TagLib::Ogg::Vorbis::File(filePathChars)); + writeXiphComment(pOggFile->tag(), trackMetadata); + pFile.reset(pOggFile.take()); // transfer ownership + } else if (filePath.endsWith(".flac", Qt::CaseInsensitive)) { + QScopedPointer pFlacFile( + new TagLib::FLAC::File(filePathChars)); + writeXiphComment(pFlacFile->xiphComment(true), trackMetadata); // mandatory + writeID3v2Tag(pFlacFile->ID3v2Tag(false), trackMetadata); // optional + pFile.reset(pFlacFile.take()); // transfer ownership + } else if (filePath.endsWith(".wav", Qt::CaseInsensitive)) { + QScopedPointer pWavFile( + new TagLib::RIFF::WAV::File(filePathChars)); + writeID3v2Tag(pWavFile->tag(), trackMetadata); + pFile.reset(pWavFile.take()); // transfer ownership + } else if (filePath.endsWith(".aif", Qt::CaseInsensitive) || filePath.endsWith(".aiff", Qt::CaseInsensitive)) { - file = new TagLib::RIFF::AIFF::File(fileBA.constData()); - //If the flac has no ID3v2 tag, we create a new one and add the TBPM and TKEY frame - writeID3v2Tag(((TagLib::RIFF::AIFF::File*)file)->tag(), trackMetadata); - + QScopedPointer pAiffFile( + new TagLib::RIFF::AIFF::File(filePathChars)); + writeID3v2Tag(pAiffFile->tag(), trackMetadata); + pFile.reset(pAiffFile.take()); // transfer ownership +#if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) + } else if (filePath.endsWith(".opus", Qt::CaseInsensitive)) { + QScopedPointer pOpusFile( + new TagLib::Ogg::Opus::File(filePathChars)); + writeXiphComment(pOpusFile->tag(), trackMetadata); + pFile.reset(pOpusFile.take()); // transfer ownership +#endif + } else { + qWarning() << "Unsupported file type! Could not update metadata of track " << filePath; + return false; } - //process standard tags - if (file) { - TagLib::Tag *tag = file->tag(); - if (tag) { - writeTag(tag, trackMetadata); - } - //write audio tags to file - int success = file->save(); - if (success) { - qDebug() << "Successfully updated metadata of track " << filePath; - } else { - qDebug() << "Could not update metadata of track " << filePath; - } - //delete file and return - delete file; - return success; + // write audio tags to file + DEBUG_ASSERT(pFile); + if (pFile->save()) { + qDebug() << "Successfully updated metadata of track " << filePath; + return true; } else { + qWarning() << "Failed to update metadata of track " << filePath; return false; } } diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index a1e4fc18e07..75c03b2f4d7 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -63,15 +63,16 @@ inline QString formatString(const T& value) { return QString("%1").arg(value); } -} -bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& f) { +} // anonymous namespace + +bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& file) { if (kDebugMetadata) { - qDebug() << "Parsing" << f.name(); + qDebug() << "Parsing" << file.name(); } - if (f.isValid()) { - const TagLib::AudioProperties *properties = f.audioProperties(); + if (file.isValid()) { + const TagLib::AudioProperties *properties = file.audioProperties(); if (properties) { pTrackMetadata->setChannels(properties->channels()); pTrackMetadata->setSampleRate(properties->sampleRate()); @@ -431,6 +432,32 @@ QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& tag) { return coverArt; } +namespace { + +void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { + pTag->removeFrames(pFrame->frameID()); + pTag->addFrame(pFrame); +} + +void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, const TagLib::ByteVector &id, const QString& text) { + TagLib::String::Type textType; + QByteArray textData; + if (4 <= pTag->header()->majorVersion()) { + // prefer UTF-8 for ID3v2.4.0 or higher + textType = TagLib::String::UTF8; + textData = text.toUtf8(); + } else { + textType = TagLib::String::Latin1; + textData = text.toLatin1(); + } + QScopedPointer pNewFrame( + new TagLib::ID3v2::TextIdentificationFrame(id, textType)); + pNewFrame->setText(toTagLibString(text)); + replaceID3v2Frame(pTag, pNewFrame.data()); + pNewFrame.take(); // release ownership +} + +} // anonymous namespace bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { if (NULL == pTag) { @@ -456,31 +483,6 @@ bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { return true; } -namespace { - void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { - pTag->removeFrames(pFrame->frameID()); - pTag->addFrame(pFrame); - } - - void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, const TagLib::ByteVector &id, const QString& text) { - TagLib::String::Type textType; - QByteArray textData; - if (4 <= pTag->header()->majorVersion()) { - // prefer UTF-8 for ID3v2.4.0 or higher - textType = TagLib::String::UTF8; - textData = text.toUtf8(); - } else { - textType = TagLib::String::Latin1; - textData = text.toLatin1(); - } - QScopedPointer pNewFrame( - new TagLib::ID3v2::TextIdentificationFrame(id, textType)); - pNewFrame->setText(toTagLibString(text)); - replaceID3v2Frame(pTag, pNewFrame.data()); - pNewFrame.take(); // release ownership - } -} - bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) { if (NULL == pTag) { return false; @@ -491,6 +493,10 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) return false; } + // Write common metadata + writeTag(pTag, trackMetadata); + + // additional tags writeID3v2TextIdentificationFrame(pTag, "TPE2", trackMetadata.getAlbumArtist()); writeID3v2TextIdentificationFrame(pTag, "TBPM", formatString(trackMetadata.getBpm())); writeID3v2TextIdentificationFrame(pTag, "TKEY", formatString(trackMetadata.getKey())); @@ -509,8 +515,9 @@ bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { return false; } - // Adds to the item specified by key the data value. - // If replace is true, then all of the other values on the same key will be removed first. + // Write common metadata + writeTag(pTag, trackMetadata); + pTag->addValue("BPM", toTagLibString(formatString(trackMetadata.getBpm())), true); pTag->addValue("Album Artist", toTagLibString(trackMetadata.getAlbumArtist()), true); pTag->addValue("Composer", toTagLibString(trackMetadata.getComposer()), true); @@ -525,6 +532,9 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& track return false; } + // Write common metadata + writeTag(pTag, trackMetadata); + // Taglib does not support the update of Vorbis comments. // thus, we have to remove the old comment and add the new one @@ -559,6 +569,9 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { return false; } + // Write common metadata + writeTag(pTag, trackMetadata); + pTag->itemListMap()["aART"] = TagLib::StringList(toTagLibString(trackMetadata.getAlbumArtist())); pTag->itemListMap()["tmpo"] = TagLib::StringList(toTagLibString(formatString(trackMetadata.getBpm()))); pTag->itemListMap()["----:com.apple.iTunes:BPM"] = TagLib::StringList(toTagLibString(formatString(trackMetadata.getBpm()))); diff --git a/src/metadata/trackmetadatataglib.h b/src/metadata/trackmetadatataglib.h index e4e7e0ecd49..aa0f1c44bd9 100644 --- a/src/metadata/trackmetadatataglib.h +++ b/src/metadata/trackmetadatataglib.h @@ -19,34 +19,35 @@ #include -namespace Mixxx -{ - bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& taglibFile); - - // Read common metadata (will implicitly be invoked from the specialized functions) - void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag); - - // Read additional metadata - void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag); - void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag); - void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag); - void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag); - - // Write common metadata - bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata); - - // Write additional metadata - bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata); - bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata); - bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& trackMetadata); - bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata); - - // In order to avoid processing images when it's not - // needed (TIO building), we must process it separately. - QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag); - QImage readAPETagCover(const TagLib::APE::Tag& tag); - QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag); - QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& tag); +namespace Mixxx { + +// Read common audio properties of a file +bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& file); + +// Read metadata +// The general function readTag() is implicitly invoked +// from the specialized tag reading functions! +void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag); +void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag); +void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag); +void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag); +void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag); + +// Write metadata +// The general function writeTag() is implicitly invoked +// from the specialized tag writing functions! +bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata); +bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata); +bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& trackMetadata); +bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata); + +// Read cover art +// In order to avoid processing images when it's not +// needed (TIO building), we must process it separately. +QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag); +QImage readAPETagCover(const TagLib::APE::Tag& tag); +QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag); +QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& tag); } //namespace Mixxx From 1e9af42be01dff53fc695652fe39da53eab9b51e Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 11 Jan 2015 14:26:57 +0100 Subject: [PATCH 061/481] Minor renaming --- plugins/soundsourcewv/audiosourcewv.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 385087a93b7..ced9991085e 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -38,8 +38,8 @@ Result AudioSourceWV::open(QString fileName) { m_sampleScale = kSampleValuePeak; } else { const int bitsPerSample = WavpackGetBitsPerSample(m_wpc); - const uint32_t peakSampleValue = uint32_t(1) << (bitsPerSample - 1); - m_sampleScale = kSampleValuePeak / sample_type(peakSampleValue); + const uint32_t wavpackPeakSampleValue = uint32_t(1) << (bitsPerSample - 1); + m_sampleScale = kSampleValuePeak / sample_type(wavpackPeakSampleValue); } return OK; From 2f096d40e9f15fc1bd9219138f88021e11f58a59 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 11 Jan 2015 15:13:25 +0100 Subject: [PATCH 062/481] Fix writing of BPM tag in M4A files --- src/metadata/trackmetadatataglib.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 75c03b2f4d7..72f684d5fd4 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -573,7 +573,9 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { writeTag(pTag, trackMetadata); pTag->itemListMap()["aART"] = TagLib::StringList(toTagLibString(trackMetadata.getAlbumArtist())); - pTag->itemListMap()["tmpo"] = TagLib::StringList(toTagLibString(formatString(trackMetadata.getBpm()))); + // Delete the legacy atom "tmpo" that does not seem to be supported + // very well. Replace it with the new atom "----:com.apple.iTunes:BPM". + pTag->itemListMap().erase("tmpo"); pTag->itemListMap()["----:com.apple.iTunes:BPM"] = TagLib::StringList(toTagLibString(formatString(trackMetadata.getBpm()))); pTag->itemListMap()["----:com.apple.iTunes:KEY"] = TagLib::StringList(toTagLibString(trackMetadata.getKey())); pTag->itemListMap()["\251wrt"] = TagLib::StringList(toTagLibString(trackMetadata.getComposer())); From 740f705188321fc059f89fc0f31033dd8ee76ed4 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Sun, 11 Jan 2015 16:26:51 +0100 Subject: [PATCH 063/481] More tag writing improvements --- src/metadata/trackmetadata.cpp | 4 +++ src/metadata/trackmetadata.h | 6 ++-- src/metadata/trackmetadatataglib.cpp | 54 +++++++++++++++++++++------- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index ace3ccfae04..4c9eadc111e 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -4,6 +4,10 @@ namespace Mixxx { +const double TrackMetadata::BPM_UNDEFINED = 0.0; +const double TrackMetadata::BPM_MIN = 0.0; // exclusive lower bound +const double TrackMetadata::BPM_MAX = 300.0; // inclusive upper bound + TrackMetadata::TrackMetadata() : m_channels(0), m_sampleRate(0), m_bitrate(0), m_duration(0), m_bpm(BPM_UNDEFINED), m_replayGain(REPLAYGAIN_UNDEFINED) { } diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index ae2091da31b..b547260ecf0 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -121,9 +121,9 @@ class TrackMetadata { } // beats / minute - static const double BPM_UNDEFINED = 0.0; - static const double BPM_MIN = 0.0; // exclusive lower bound - static const double BPM_MAX = 300.0; // inclusive upper bound + static const double BPM_UNDEFINED; + static const double BPM_MIN; // exclusive lower bound + static const double BPM_MAX; // inclusive upper bound inline double getBpm() const { return m_bpm; } diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 72f684d5fd4..072eee67550 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -64,6 +64,22 @@ QString formatString(const T& value) { return QString("%1").arg(value); } +template +inline +QString formatString(const T& value, const T& emptyValue) { + if (value == emptyValue) { + return QString(); /// empty string + } else { + return formatString(value); + } +} + +template +inline +QString formatBpmString(const T& bpm) { + return formatString(bpm, TrackMetadata::BPM_UNDEFINED); +} + } // anonymous namespace bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& file) { @@ -498,10 +514,10 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) // additional tags writeID3v2TextIdentificationFrame(pTag, "TPE2", trackMetadata.getAlbumArtist()); - writeID3v2TextIdentificationFrame(pTag, "TBPM", formatString(trackMetadata.getBpm())); - writeID3v2TextIdentificationFrame(pTag, "TKEY", formatString(trackMetadata.getKey())); - writeID3v2TextIdentificationFrame(pTag, "TCOM", formatString(trackMetadata.getComposer())); - writeID3v2TextIdentificationFrame(pTag, "TIT1", formatString(trackMetadata.getGrouping())); + writeID3v2TextIdentificationFrame(pTag, "TBPM", formatBpmString(trackMetadata.getBpm())); + writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); + writeID3v2TextIdentificationFrame(pTag, "TCOM", trackMetadata.getComposer()); + writeID3v2TextIdentificationFrame(pTag, "TIT1", trackMetadata.getGrouping()); if (4 <= pHeader->majorVersion()) { // ID3v2.4.0: TDRC replaces TYER + TDAT writeID3v2TextIdentificationFrame(pTag, "TDRC", formatString(trackMetadata.getYear())); @@ -518,7 +534,7 @@ bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { // Write common metadata writeTag(pTag, trackMetadata); - pTag->addValue("BPM", toTagLibString(formatString(trackMetadata.getBpm())), true); + pTag->addValue("BPM", toTagLibString(formatBpmString(trackMetadata.getBpm())), true); pTag->addValue("Album Artist", toTagLibString(trackMetadata.getAlbumArtist()), true); pTag->addValue("Composer", toTagLibString(trackMetadata.getComposer()), true); pTag->addValue("Grouping", toTagLibString(trackMetadata.getGrouping()), true); @@ -543,9 +559,9 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& track // Some tools use "BPM" so write that. pTag->removeField("BPM"); - pTag->addField("BPM", toTagLibString(formatString(trackMetadata.getBpm()))); + pTag->addField("BPM", toTagLibString(formatBpmString(trackMetadata.getBpm()))); pTag->removeField("TEMPO"); - pTag->addField("TEMPO", toTagLibString(formatString(trackMetadata.getBpm()))); + pTag->addField("TEMPO", toTagLibString(formatBpmString(trackMetadata.getBpm()))); pTag->removeField("INITIALKEY"); pTag->addField("INITIALKEY", toTagLibString(trackMetadata.getKey())); @@ -564,6 +580,18 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& track return true; } +namespace { + + void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, const QString& value) { + if (value.isEmpty()) { + pTag->itemListMap().erase(key); + } else { + pTag->itemListMap()[key] = TagLib::StringList(toTagLibString(value)); + } + } + +} // anonymous namespace + bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { if (NULL == pTag) { return false; @@ -572,15 +600,15 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { // Write common metadata writeTag(pTag, trackMetadata); - pTag->itemListMap()["aART"] = TagLib::StringList(toTagLibString(trackMetadata.getAlbumArtist())); + writeMP4Atom(pTag, "aART", trackMetadata.getAlbumArtist()); + writeMP4Atom(pTag, "\251wrt", trackMetadata.getComposer()); + writeMP4Atom(pTag, "\251grp", trackMetadata.getGrouping()); + writeMP4Atom(pTag, "\251day", trackMetadata.getYear()); // Delete the legacy atom "tmpo" that does not seem to be supported // very well. Replace it with the new atom "----:com.apple.iTunes:BPM". pTag->itemListMap().erase("tmpo"); - pTag->itemListMap()["----:com.apple.iTunes:BPM"] = TagLib::StringList(toTagLibString(formatString(trackMetadata.getBpm()))); - pTag->itemListMap()["----:com.apple.iTunes:KEY"] = TagLib::StringList(toTagLibString(trackMetadata.getKey())); - pTag->itemListMap()["\251wrt"] = TagLib::StringList(toTagLibString(trackMetadata.getComposer())); - pTag->itemListMap()["\251grp"] = TagLib::StringList(toTagLibString(trackMetadata.getGrouping())); - pTag->itemListMap()["\251day"] = TagLib::StringList(toTagLibString(trackMetadata.getYear())); + writeMP4Atom(pTag, "----:com.apple.iTunes:BPM", formatBpmString(trackMetadata.getBpm())); + writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", trackMetadata.getKey()); return true; } From 3dc95fcc18ca6f1260d29147c34e1d91da2b1ee4 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 11 Jan 2015 21:05:01 +0100 Subject: [PATCH 064/481] cleaning --- src/effects/native/paneffect.cpp | 134 ++++++++----------------------- src/effects/native/paneffect.h | 109 +++++++++---------------- 2 files changed, 74 insertions(+), 169 deletions(-) diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp index dfd628a648d..96d82398a02 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/paneffect.cpp @@ -4,9 +4,6 @@ #include "sampleutil.h" -#define INCREMENT_RING(index, increment, length) index = (index + increment) % length -#define RAMP_LENGTH 500 - // static QString PanEffect::getId() { return "org.mixxx.effects.pan"; @@ -19,8 +16,11 @@ EffectManifest PanEffect::getManifest() { manifest.setName(QObject::tr("Pan")); manifest.setAuthor("The Mixxx Team"); manifest.setVersion("1.0"); - manifest.setDescription(QObject::tr("Simple Pan")); + manifest.setDescription(QObject::tr("Bounce the sound from a channel to another")); + // TODO(jclaveau) : this doesn't look like a good name but doesn't exist on + // my mixer. Any suggestion? + // This parameter controls the easing of the sound from a side to another. EffectManifestParameter* strength = manifest.addParameter(); strength->setId("strength"); strength->setName(QObject::tr("Strength")); @@ -30,8 +30,7 @@ EffectManifest PanEffect::getManifest() { strength->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); strength->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); strength->setMinimum(0.0); - strength->setMaximum(0.5); - // strength->setDefault(0.25); + strength->setMaximum(0.5); // there are two steps per period so max is half strength->setDefault(0.5); EffectManifestParameter* period = manifest.addParameter(); @@ -43,9 +42,12 @@ EffectManifest PanEffect::getManifest() { period->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); period->setMinimum(1.0); period->setMaximum(500.0); - // period->setDefault(250.0); period->setDefault(50.0); + // This only extists for testing and finding the best value + // TODO : remove when a satisfying value is chosen + // The current value is taken with a simple laptop soundcard a sample rate + // of 44100 EffectManifestParameter* ramping = manifest.addParameter(); ramping->setId("ramping_treshold"); ramping->setName(QObject::tr("Ramping")); @@ -54,11 +56,10 @@ EffectManifest PanEffect::getManifest() { ramping->setSemanticHint(EffectManifestParameter::SEMANTIC_UNKNOWN); ramping->setUnitsHint(EffectManifestParameter::UNITS_UNKNOWN); ramping->setMinimum(0.000000000001f); - ramping->setMaximum(0.015978f); - // ramping->setDefault(250.0); - ramping->setDefault(0.003f); - // 0.00387852 => other good value + ramping->setMaximum(0.015978f); // max good value with my soundcard + ramping->setDefault(0.003f); // best value + // This control is hidden for the moment in Deere (replace by the previous one) EffectManifestParameter* depth = manifest.addParameter(); depth->setId("depth"); depth->setName(QObject::tr("Depth")); @@ -80,7 +81,6 @@ PanEffect::PanEffect(EngineEffect* pEffect, const EffectManifest& manifest) m_pPeriodParameter(pEffect->getParameterById("period")), m_pRampingParameter(pEffect->getParameterById("ramping_treshold")) { - oldFrac = -1.0f; Q_UNUSED(manifest); } @@ -99,9 +99,8 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, PanGroupState& gs = *pGroupState; - // if (EnableState::DISABLED == enableState) { - if (0x00 == enableState) { - return; // DISABLED = 0x00 + if (enableState == EffectProcessor::DISABLED) { + return; } CSAMPLE period = roundf(m_pPeriodParameter->value()) * (float)numSamples / 2.0f; @@ -109,7 +108,7 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, CSAMPLE depth = m_pDepthParameter->value(); float rampingTreshold = m_pRampingParameter->value(); - if (gs.time > period || 0x03 == enableState) { // ENABLING = 0x03 + if (gs.time > period || enableState == EffectProcessor::ENABLING) { gs.time = 0; } @@ -123,34 +122,15 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, a = 0.0001f; } - // define the increasing of gain when only one output channel is used - float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; - - // size of a segment of slope - float u = ( 0.5f - stepFrac ) / 2.0f; - - // todo (jclaveau) : stereo - SampleUtil::mixStereoToMono(pOutput, pInput, numSamples); - - /** stereo : - * + position 0 <=> 0.5 <=> 1 => gauche et droite au milieu - * + position 0.25 => droite à droite et gauche à droite - * + position 0.75 => droite à gauche et gauche à gauche - * law coef : sqrt(2) 1 sqrt(2) - * position : 1 0 0.5 - * - * position left : - * channel left : 0 -> pi/4 -> pi/2 -> pi/4 - * - * position right : - * channel right : pi/2 -> 3pi/4 -> pi -> 3pi/4 - * - */ - - // RampedSample* frac = new RampedSample(); - // frac->setRamping(rampingTreshold); - RampedSample frac(rampingTreshold); - // frac.setRamping(rampingTreshold); + // define the increasing of gain. + float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; + + // size of a segment of slope (controled by the "strength" parameter) + float u = (0.5f - stepFrac) / 2.0f; + + gs.frac.setRamping(rampingTreshold); + gs.frac.ramped = false; // just for debug + for (unsigned int i = 0; i + 1 < numSamples; i += 2) { CSAMPLE periodFraction = CSAMPLE(gs.time) / period; @@ -158,15 +138,14 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // current quarter in the trigonometric circle float quarter = floorf(periodFraction * 4.0f); - // part of the period fraction being steps (not in the slope) + // part of the period fraction being step (not in the slope) CSAMPLE stepsFractionPart = floorf((quarter+1.0f)/2.0f) * stepFrac; // float inInterval = fmod( periodFraction, (period / 2.0) ); float inInterval = fmod( periodFraction, 0.5f ); - CSAMPLE position; - if (inInterval > u && inInterval < ( u + stepFrac)) { + if (inInterval > u && inInterval < (u + stepFrac)) { // at full left or full right position = quarter < 2.0f ? 0.25f : 0.75f; } else { @@ -174,70 +153,25 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, position = (periodFraction - stepsFractionPart) * a; } - // transform the position into a sinusoid (between 0 and 1) - frac = (sinf(M_PI * 2.0f * position) + 1.0f) / 2.0f; - - // todo (jclaveau) for stereo : - // frac left in left - // frac left in right - // frac right in left - // frac right in right - - - // - /* - if (oldFrac != -1.0f) { - float diff = frac - oldFrac; - - if( fabs(diff) >= rampingTreshold ){ - frac = oldFrac + (diff / fabs(diff) * rampingTreshold); - } - - if( fabs(diff) > maxDiff ){ - maxDiff = fabs(diff); - } - } + // transform the position into a sinusoid (but between 0 and 1) + gs.frac = (sin(M_PI * 2.0f * position) + 1.0f) / 2.0f; - oldFrac = frac; - */ + pOutput[i] = pInput[i] * (1 - depth) + + pInput[i] * gs.frac * lawCoef * depth; - - pOutput[i] = pOutput[i] * (1 - depth) - + pOutput[i] * frac * lawCoef * depth; - - pOutput[i+1] = pOutput[i+1] * (1 - depth) - + pOutput[i+1] * (1.0f - frac) * lawCoef * depth; + pOutput[i+1] = pInput[i+1] * (1 - depth) + + pInput[i+1] * (1.0f - gs.frac) * lawCoef * depth; gs.time++; } - /** / - qDebug() << "stepFrac" << stepFrac - << "| time :" << gs.time - // << "| period :" << period - << "| a :" << a // coef of slope between 1 and -1 - // << "| lawCoef :" << lawCoef - // << "| enableState :" << enableState - // << "| numSamples :" << numSamples - ; - - /**/ - - qDebug() - // < < "| position :" << (roundf(position * 100.0f) / 100.0f) - << "| frac :" << frac - // << "| maxDiff :" << maxDiff - // << "| maxDiff2 :" << maxDiff2 + << "| ramped :" << gs.frac.ramped + << "| frac :" << gs.frac << "| time :" << gs.time << "| rampingTreshold :" << rampingTreshold ; - // << "| a :" << a // coef of slope between 1 and -1 - // << "| numSamples :" << numSamples; - - /**/ - // qDebug() << "pOutput[1]" << pOutput[1] << "| pOutput[2]" << pOutput[2]; } diff --git a/src/effects/native/paneffect.h b/src/effects/native/paneffect.h index 8ae2396138b..176a752ddca 100644 --- a/src/effects/native/paneffect.h +++ b/src/effects/native/paneffect.h @@ -11,92 +11,65 @@ #include "effects/effectprocessor.h" #include "sampleutil.h" -struct PanSquarySinusoid { - PanSquarySinusoid() { - lastFract = -1; - } - ~PanSquarySinusoid() { - } - CSAMPLE lastFract; -}; - -struct PanGroupState { - PanGroupState() { - time = 0; - } - ~PanGroupState() { - } - unsigned int time; -}; - +// This class provides a float value that cannot be increased or decreased +// by more than a given value to avoid clicks. +// Finally I use it with only one value but i wonder if it can be useful +// somewhere else (I here clicks when I change the period of flanger for example). class RampedSample { public: - RampedSample(); - inline RampedSample(const float newMaxDifference){ - setRamping(newMaxDifference); - } - virtual ~RampedSample(); - inline void setRamping(const float newMaxDifference){ - maxDifference = newMaxDifference; - } + inline RampedSample() + : ramped(false), + maxDifference(1.0f), + initialized(false) {} - inline RampedSample & operator=(const float &newValue) { - CSAMPLE difference = currentValue - newValue; - if (difference > maxDifference) - currentValue = maxDifference; - else - currentValue = newValue; - - return *this; - } + virtual ~RampedSample(){}; - inline RampedSample & operator-(float diff) { - CSAMPLE newValue = currentValue - diff; - CSAMPLE difference = currentValue - newValue; - if (difference > maxDifference) - currentValue = maxDifference; - else - currentValue = newValue; - - return *this; + inline void setRamping(const float newMaxDifference) { + maxDifference = newMaxDifference; } - inline RampedSample & operator*(float times) { - CSAMPLE newValue = times * currentValue; - - CSAMPLE difference = currentValue - newValue; - if (difference > maxDifference) - currentValue = maxDifference; - else + inline RampedSample & operator=(const float newValue) { + if (!initialized) { currentValue = newValue; - + initialized = true; + } else { + float difference = newValue - currentValue; + if (fabs(difference) > maxDifference) { + currentValue += difference / fabs(difference) * maxDifference; + ramped = true; + } else { + currentValue = newValue; + } + } return *this; } inline operator float() { - return value(); - } - - inline RampedSample & operator=(const RampedSample &that){ - CSAMPLE difference = currentValue - that.value(); - if (difference > maxDifference) - currentValue = maxDifference; - else - currentValue = that.value(); - - return *this; - } - - inline CSAMPLE value() const { return currentValue; } + // TODO(jclaveau) : remove when maxDiff value is fixed + bool ramped; + private: float maxDifference; - CSAMPLE currentValue; + float currentValue; + bool initialized; +}; + + +struct PanGroupState { + PanGroupState() { + time = 0; + } + ~PanGroupState() { + } + unsigned int time; + RampedSample frac; }; + class PanEffect : public GroupEffectProcessor { public: PanEffect(EngineEffect* pEffect, const EffectManifest& manifest); @@ -125,8 +98,6 @@ class PanEffect : public GroupEffectProcessor { EngineEffectParameter* m_pPeriodParameter; EngineEffectParameter* m_pRampingParameter; - CSAMPLE oldFrac; - DISALLOW_COPY_AND_ASSIGN(PanEffect); }; From c57be2eeb2a9a114728481591e1d94c23fdc5363 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Sun, 11 Jan 2015 21:41:49 +0100 Subject: [PATCH 065/481] comments and review --- src/effects/native/paneffect.cpp | 41 +++++++++++++++++++------------- src/effects/native/paneffect.h | 2 +- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp index 96d82398a02..5a8e1f3b33e 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/paneffect.cpp @@ -16,7 +16,8 @@ EffectManifest PanEffect::getManifest() { manifest.setName(QObject::tr("Pan")); manifest.setAuthor("The Mixxx Team"); manifest.setVersion("1.0"); - manifest.setDescription(QObject::tr("Bounce the sound from a channel to another")); + manifest.setDescription(QObject::tr( + "Bounce the sound from a channel to another, fastly or softly")); // TODO(jclaveau) : this doesn't look like a good name but doesn't exist on // my mixer. Any suggestion? @@ -33,6 +34,9 @@ EffectManifest PanEffect::getManifest() { strength->setMaximum(0.5); // there are two steps per period so max is half strength->setDefault(0.5); + // On my mixer, the period is defined as a multiple of the BPM + // I wonder if I should implement it as the bouncing is really nice + // when it is synced. EffectManifestParameter* period = manifest.addParameter(); period->setId("period"); period->setName(QObject::tr("Period")); @@ -45,11 +49,11 @@ EffectManifest PanEffect::getManifest() { period->setDefault(50.0); // This only extists for testing and finding the best value - // TODO : remove when a satisfying value is chosen - // The current value is taken with a simple laptop soundcard a sample rate - // of 44100 + // TODO(jclaveau) : remove when a satisfying value is chosen + // The current value is taken with a simple laptop soundcard with a sample + // rate of 44100 EffectManifestParameter* ramping = manifest.addParameter(); - ramping->setId("ramping_treshold"); + ramping->setId("ramping_threshold"); ramping->setName(QObject::tr("Ramping")); ramping->setDescription(""); ramping->setControlHint(EffectManifestParameter::CONTROL_KNOB_LINEAR); @@ -59,7 +63,8 @@ EffectManifest PanEffect::getManifest() { ramping->setMaximum(0.015978f); // max good value with my soundcard ramping->setDefault(0.003f); // best value - // This control is hidden for the moment in Deere (replace by the previous one) + // This control is hidden for the moment in Deere (replace by the previous + // one wich would be useless soon) EffectManifestParameter* depth = manifest.addParameter(); depth->setId("depth"); depth->setName(QObject::tr("Depth")); @@ -79,7 +84,7 @@ PanEffect::PanEffect(EngineEffect* pEffect, const EffectManifest& manifest) m_pDepthParameter(pEffect->getParameterById("depth")), m_pStrengthParameter(pEffect->getParameterById("strength")), m_pPeriodParameter(pEffect->getParameterById("period")), - m_pRampingParameter(pEffect->getParameterById("ramping_treshold")) + m_pRampingParameter(pEffect->getParameterById("ramping_threshold")) { Q_UNUSED(manifest); } @@ -112,17 +117,19 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, gs.time = 0; } + // Normally, the position goes from 0 to 1 linearly. Here we make steps at + // 0.25 and 0.75 to have the sound fully on the right or fully on the left. + // At the end, the "position" value can describe a sinusoid or a square + // curve depending of the size of those steps. + // coef of the slope // a = (y2 - y1) / (x2 - x1) // 1 / ( 1 - 2 * stepfrac) - float a; - if( stepFrac != 0.5f ){ - a = 1.0f / (1.0f - stepFrac * 2.0f); - } else { - a = 0.0001f; - } + float a = stepFrac != 0.5f ? 1.0f / (1.0f - stepFrac * 2.0f) : 1.0f; - // define the increasing of gain. + // define the increasing of gain (the music sounds lower if only one + // channel is enabled). + // TODO(jclaveau) : Challenge this value float lawCoef = 1.0f / sqrtf(2.0f) * 2.0f; // size of a segment of slope (controled by the "strength" parameter) @@ -138,14 +145,14 @@ void PanEffect::processGroup(const QString& group, PanGroupState* pGroupState, // current quarter in the trigonometric circle float quarter = floorf(periodFraction * 4.0f); - // part of the period fraction being step (not in the slope) + // part of the period fraction being a step (not in the slope) CSAMPLE stepsFractionPart = floorf((quarter+1.0f)/2.0f) * stepFrac; // float inInterval = fmod( periodFraction, (period / 2.0) ); - float inInterval = fmod( periodFraction, 0.5f ); + float inStepInterval = fmod(periodFraction, 0.5f); CSAMPLE position; - if (inInterval > u && inInterval < (u + stepFrac)) { + if (inStepInterval > u && inStepInterval < (u + stepFrac)) { // at full left or full right position = quarter < 2.0f ? 0.25f : 0.75f; } else { diff --git a/src/effects/native/paneffect.h b/src/effects/native/paneffect.h index 176a752ddca..c41fa334ed4 100644 --- a/src/effects/native/paneffect.h +++ b/src/effects/native/paneffect.h @@ -14,7 +14,7 @@ // This class provides a float value that cannot be increased or decreased // by more than a given value to avoid clicks. // Finally I use it with only one value but i wonder if it can be useful -// somewhere else (I here clicks when I change the period of flanger for example). +// somewhere else (I hear clicks when I change the period of flanger for example). class RampedSample { public: From acc3edb86a8741f93dd179b46351c20d9f2b59ea Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 12 Jan 2015 00:44:47 +0100 Subject: [PATCH 066/481] Fix writing of atom "tmpo" for .m4a files --- src/metadata/trackmetadatataglib.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 072eee67550..d0a4b07fedd 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -14,6 +14,8 @@ #include #include +#include + #include namespace Mixxx { @@ -582,11 +584,16 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& track namespace { + template + inline void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, const T& value) { + pTag->itemListMap()[key] = value; + } + void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, const QString& value) { if (value.isEmpty()) { pTag->itemListMap().erase(key); } else { - pTag->itemListMap()[key] = TagLib::StringList(toTagLibString(value)); + writeMP4Atom(pTag, key, TagLib::StringList(toTagLibString(value))); } } @@ -604,9 +611,12 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { writeMP4Atom(pTag, "\251wrt", trackMetadata.getComposer()); writeMP4Atom(pTag, "\251grp", trackMetadata.getGrouping()); writeMP4Atom(pTag, "\251day", trackMetadata.getYear()); - // Delete the legacy atom "tmpo" that does not seem to be supported - // very well. Replace it with the new atom "----:com.apple.iTunes:BPM". - pTag->itemListMap().erase("tmpo"); + if (trackMetadata.isBpmValid()) { + const int tmpoValue = round(trackMetadata.getBpm()); + writeMP4Atom(pTag, "tmpo", tmpoValue); + } else { + pTag->itemListMap().erase("tmpo"); + } writeMP4Atom(pTag, "----:com.apple.iTunes:BPM", formatBpmString(trackMetadata.getBpm())); writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", trackMetadata.getKey()); From 21d8b6752f191c825bfb04efd8a98e2648cbd3f2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 12 Jan 2015 01:24:59 +0100 Subject: [PATCH 067/481] Fix reading of BPM fields from .m4a files --- src/metadata/trackmetadata.cpp | 4 ++++ src/metadata/trackmetadatataglib.cpp | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 4c9eadc111e..7e9208a5e23 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -13,9 +13,13 @@ TrackMetadata::TrackMetadata() } double TrackMetadata::parseBpmString(const QString& sBpm) { + if (sBpm.trimmed().isEmpty()) { + return BPM_UNDEFINED; + } bool bpmValid = false; double bpm = sBpm.toDouble(&bpmValid); if ((!bpmValid) || (BPM_MIN > bpm)) { + qDebug() << "Failed to parse BPM:" << sBpm; return BPM_UNDEFINED; } while (bpm > BPM_MAX) { diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index d0a4b07fedd..388850b7090 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -345,9 +345,15 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) // Get BPM if (tag.itemListMap().contains("tmpo")) { - pTrackMetadata->setBpmString(toQString(tag.itemListMap()["tmpo"])); - } else if (tag.itemListMap().contains("----:com.apple.iTunes:BPM")) { - // This is an alternate way of storing BPM. + // Read the BPM as an integer value. + pTrackMetadata->setBpm(tag.itemListMap()["tmpo"].toInt()); + } + if (tag.itemListMap().contains("----:com.apple.iTunes:BPM")) { + // This is the preferred field for storing the BPM + // with fractional digits as a floating-point value. + // If this field contains a valid value the integer + // BPM value that might have been read before is + // overwritten. pTrackMetadata->setBpmString(toQString(tag.itemListMap()["----:com.apple.iTunes:BPM"])); } From ab2627b9ffbd87c9836592168e831fe52e15ddb5 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 12 Jan 2015 11:11:02 +0100 Subject: [PATCH 068/481] Move static constants from header to implementation file --- src/metadata/trackmetadata.cpp | 10 +++++++--- src/metadata/trackmetadata.h | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 7e9208a5e23..47c7b5a6335 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -4,9 +4,13 @@ namespace Mixxx { -const double TrackMetadata::BPM_UNDEFINED = 0.0; -const double TrackMetadata::BPM_MIN = 0.0; // exclusive lower bound -const double TrackMetadata::BPM_MAX = 300.0; // inclusive upper bound +/*static*/ const double TrackMetadata::BPM_UNDEFINED = 0.0; +/*static*/ const double TrackMetadata::BPM_MIN = 0.0; // exclusive lower bound +/*static*/ const double TrackMetadata::BPM_MAX = 300.0; // inclusive upper bound + +/*static*/ const double TrackMetadata::REPLAYGAIN_UNDEFINED = 0.0f; +/*static*/ const double TrackMetadata::REPLAYGAIN_MIN = 0.0f; // exclusive lower bound +/*static*/ const double TrackMetadata::REPLAYGAIN_0DB = 1.0f; TrackMetadata::TrackMetadata() : m_channels(0), m_sampleRate(0), m_bitrate(0), m_duration(0), m_bpm(BPM_UNDEFINED), m_replayGain(REPLAYGAIN_UNDEFINED) { diff --git a/src/metadata/trackmetadata.h b/src/metadata/trackmetadata.h index b547260ecf0..3cfc07d56b8 100644 --- a/src/metadata/trackmetadata.h +++ b/src/metadata/trackmetadata.h @@ -139,9 +139,9 @@ class TrackMetadata { static double parseBpmString(const QString& sBpm); bool setBpmString(const QString& sBpm); - static const double REPLAYGAIN_UNDEFINED = 0.0f; - static const double REPLAYGAIN_MIN = 0.0f; // exclusive lower bound - static const double REPLAYGAIN_0DB = 1.0f; + static const double REPLAYGAIN_UNDEFINED; + static const double REPLAYGAIN_MIN; // exclusive lower bound + static const double REPLAYGAIN_0DB; inline float getReplayGain() const { return m_replayGain; } From fa699aece9fab0675d4df72497974391bc8c4a92 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 12 Jan 2015 13:40:34 +0100 Subject: [PATCH 069/481] Improve reading of metadata - Uniform handling of multi-valued fields - No special case handling for ID3v2 Artist/AlbumArtist --- src/metadata/trackmetadatataglib.cpp | 169 ++++++++++++++++----------- 1 file changed, 99 insertions(+), 70 deletions(-) diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 388850b7090..10385638cf7 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -38,15 +38,55 @@ QString toQString(const TagLib::String& tString) { // Concatenates the elements of a TagLib string list // into a single string. inline -QString toQString(const TagLib::StringList& strList) { +QString toQStringConcat(const TagLib::StringList& strList) { return toQString(strList.toString()); } -// Concatenates the string list of an MP4 atom -// into a single string +// Concatenates the frame list of an ID3v2 tag into a +// single string. inline -QString toQString(const TagLib::MP4::Item& mp4Item) { - return toQString(mp4Item.toStringList()); +QString toQString(const TagLib::ID3v2::Frame& frame) { + return toQString(frame.toString()); +} + +// Concatenates the frame list of an ID3v2 tag into a +// single string. +QString toQStringConcat(const TagLib::ID3v2::FrameList& frameList) { + QString result; + for (TagLib::ID3v2::FrameList::ConstIterator i(frameList.begin()); frameList.end() != i; ++i) { + result += toQString(**i); + } + return result; +} + +// Returns the first ID3v2 frame that is not empty. +QString toQStringFirst(const TagLib::ID3v2::FrameList& frameList) { + for (TagLib::ID3v2::FrameList::ConstIterator i(frameList.begin()); frameList.end() != i; ++i) { + const QString value(toQString(**i).trimmed()); + if (!value.isEmpty()) { + return value; + } + } + return QString(); +} + +// Concatenates the string list of an MP4 item +// into a single string. +inline +QString toQStringConcat(const TagLib::MP4::Item& mp4Item) { + return toQStringConcat(mp4Item.toStringList()); +} + +// Returns the first MP4 item string that is not empty. +QString toQStringFirst(const TagLib::MP4::Item& mp4Item) { + const TagLib::StringList strList(mp4Item.toStringList()); + for (TagLib::StringList::ConstIterator i(strList.begin()); strList.end() != i; ++i) { + const QString value(toQString(*i).trimmed()); + if (!value.isEmpty()) { + return value; + } + } + return QString(); } inline @@ -144,8 +184,8 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) // Print every frame in the file. if (kDebugMetadata) { - TagLib::ID3v2::FrameList::ConstIterator it = tag.frameList().begin(); - for(; it != tag.frameList().end(); it++) { + for(TagLib::ID3v2::FrameList::ConstIterator it = tag.frameList().begin(); + it != tag.frameList().end(); ++it) { qDebug() << "ID3V2" << (*it)->frameID().data() << "-" << toQString((*it)->toString()); } @@ -153,70 +193,60 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) readTag(pTrackMetadata, tag); - TagLib::ID3v2::FrameList bpmFrame = tag.frameListMap()["TBPM"]; + const TagLib::ID3v2::FrameList bpmFrame(tag.frameListMap()["TBPM"]); if (!bpmFrame.isEmpty()) { - QString sBpm = toQString(bpmFrame.front()->toString()); - pTrackMetadata->setBpmString(sBpm); + pTrackMetadata->setBpmString(toQStringFirst(bpmFrame)); } - TagLib::ID3v2::FrameList keyFrame = tag.frameListMap()["TKEY"]; + const TagLib::ID3v2::FrameList keyFrame(tag.frameListMap()["TKEY"]); if (!keyFrame.isEmpty()) { - QString sKey = toQString(keyFrame.front()->toString()); - pTrackMetadata->setKey(sKey); + pTrackMetadata->setKey(toQStringFirst(keyFrame)); } // Foobar2000-style ID3v2.3.0 tags // TODO: Check if everything is ok. - TagLib::ID3v2::FrameList frames = tag.frameListMap()["TXXX"]; - for (TagLib::ID3v2::FrameList::Iterator it = frames.begin(); it != frames.end(); ++it) { - TagLib::ID3v2::UserTextIdentificationFrame* ReplayGainframe = + TagLib::ID3v2::FrameList textFrames(tag.frameListMap()["TXXX"]); + for (TagLib::ID3v2::FrameList::ConstIterator it = textFrames.begin(); it != textFrames.end(); ++it) { + TagLib::ID3v2::UserTextIdentificationFrame* replaygainFrame = dynamic_cast(*it); - if (ReplayGainframe && ReplayGainframe->fieldList().size() >= 2) { - QString desc = toQString(ReplayGainframe->description()).toLower(); + if (replaygainFrame && replaygainFrame->fieldList().size() >= 2) { + const QString desc(toQString(replaygainFrame->description()).toLower()); if (desc == "replaygain_album_gain") { - QString sReplayGain = toQString(ReplayGainframe->fieldList()[1]); - pTrackMetadata->setReplayGainDbString(sReplayGain); + const QString albumGain(toQString(replaygainFrame->fieldList()[1])); + pTrackMetadata->setReplayGainDbString(albumGain); } - //Prefer track gain over album gain. + // Prefer track gain over album gain. if (desc == "replaygain_track_gain") { - QString sReplayGain = toQString(ReplayGainframe->fieldList()[1]); - pTrackMetadata->setReplayGainDbString(sReplayGain); + const QString trackGain(toQString(replaygainFrame->fieldList()[1])); + pTrackMetadata->setReplayGainDbString(trackGain); } } } - TagLib::ID3v2::FrameList albumArtistFrame = tag.frameListMap()["TPE2"]; + const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()["TPE2"]); if (!albumArtistFrame.isEmpty()) { - QString sAlbumArtist = toQString(albumArtistFrame.front()->toString()); - pTrackMetadata->setAlbumArtist(sAlbumArtist); - - if (pTrackMetadata->getArtist().length() == 0) { - pTrackMetadata->setArtist(sAlbumArtist); - } + pTrackMetadata->setAlbumArtist(toQStringConcat(albumArtistFrame)); } - TagLib::ID3v2::FrameList originalAlbumFrame = tag.frameListMap()["TOAL"]; - if (pTrackMetadata->getAlbum().length() == 0 && !originalAlbumFrame.isEmpty()) { - QString sOriginalAlbum = TStringToQString(originalAlbumFrame.front()->toString()); - pTrackMetadata->setAlbum(sOriginalAlbum); + + if (pTrackMetadata->getAlbum().isEmpty()) { + const TagLib::ID3v2::FrameList originalAlbumFrame(tag.frameListMap()["TOAL"]); + pTrackMetadata->setAlbum(toQStringConcat(originalAlbumFrame)); } - TagLib::ID3v2::FrameList composerFrame = tag.frameListMap()["TCOM"]; + const TagLib::ID3v2::FrameList composerFrame(tag.frameListMap()["TCOM"]); if (!composerFrame.isEmpty()) { - QString sComposer = toQString(composerFrame.front()->toString()); - pTrackMetadata->setComposer(sComposer); + pTrackMetadata->setComposer(toQStringConcat(composerFrame)); } - TagLib::ID3v2::FrameList groupingFrame = tag.frameListMap()["TIT1"]; + const TagLib::ID3v2::FrameList groupingFrame(tag.frameListMap()["TIT1"]); if (!groupingFrame.isEmpty()) { - QString sGrouping = toQString(groupingFrame.front()->toString()); - pTrackMetadata->setGrouping(sGrouping); + pTrackMetadata->setGrouping(toQStringConcat(groupingFrame)); } // ID3v2.4.0: TDRC replaces TYER + TDAT - TagLib::ID3v2::FrameList recordingDateFrame = tag.frameListMap()["TDRC"]; + const TagLib::ID3v2::FrameList recordingDateFrame(tag.frameListMap()["TDRC"]); if (!recordingDateFrame.isEmpty()) { - QString sRecordingDate = toQString(recordingDateFrame.front()->toString()); - pTrackMetadata->setYear(sRecordingDate); + pTrackMetadata->setYear(toQStringFirst(recordingDateFrame)); } } @@ -274,25 +304,25 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme // from DESCRIPTION) is still empty we will additionally read this field. // Reference: http://www.xiph.org/vorbis/doc/v-comment.html if (pTrackMetadata->getComment().isEmpty() && tag.fieldListMap().contains("COMMENT")) { - pTrackMetadata->setComment(toQString(tag.fieldListMap()["COMMENT"])); + pTrackMetadata->setComment(toQStringConcat(tag.fieldListMap()["COMMENT"])); } // Some tags use "BPM" so check for that. if (tag.fieldListMap().contains("BPM")) { - pTrackMetadata->setBpmString(toQString(tag.fieldListMap()["BPM"])); + pTrackMetadata->setBpmString(toQStringFirst(tag.fieldListMap()["BPM"])); } // Give preference to the "TEMPO" tag which seems to be more standard if (tag.fieldListMap().contains("TEMPO")) { - pTrackMetadata->setBpmString(toQString(tag.fieldListMap()["TEMPO"])); + pTrackMetadata->setBpmString(toQStringFirst(tag.fieldListMap()["TEMPO"])); } if (tag.fieldListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pTrackMetadata->setReplayGainDbString(toQString(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); + pTrackMetadata->setReplayGainDbString(toQStringFirst(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. if (tag.fieldListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainDbString(toQString(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); + pTrackMetadata->setReplayGainDbString(toQStringFirst(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); } /* @@ -304,31 +334,35 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme * or a "KEY" vorbis comment. */ if (tag.fieldListMap().contains("KEY")) { - pTrackMetadata->setKey(toQString(tag.fieldListMap()["KEY"])); + pTrackMetadata->setKey(toQStringFirst(tag.fieldListMap()["KEY"])); } if (pTrackMetadata->getKey().isEmpty() && tag.fieldListMap().contains("INITIALKEY")) { - pTrackMetadata->setKey(toQString(tag.fieldListMap()["INITIALKEY"])); + // try alternative field name + pTrackMetadata->setKey(toQStringFirst(tag.fieldListMap()["INITIALKEY"])); } if (tag.fieldListMap().contains("ALBUMARTIST")) { - pTrackMetadata->setAlbumArtist(toQString(tag.fieldListMap()["ALBUMARTIST"])); - } else { + pTrackMetadata->setAlbumArtist(toQStringConcat(tag.fieldListMap()["ALBUMARTIST"])); + } + if (pTrackMetadata->getAlbumArtist().isEmpty() && tag.fieldListMap().contains("ALBUM_ARTIST")) { // try alternative field name - if (tag.fieldListMap().contains("ALBUM_ARTIST")) { - pTrackMetadata->setAlbumArtist(toQString(tag.fieldListMap()["ALBUM_ARTIST"])); - } + pTrackMetadata->setAlbumArtist(toQStringConcat(tag.fieldListMap()["ALBUM_ARTIST"])); + } + if (pTrackMetadata->getAlbumArtist().isEmpty() && tag.fieldListMap().contains("ALBUM ARTIST")) { + // try alternative field name + pTrackMetadata->setAlbumArtist(toQStringConcat(tag.fieldListMap()["ALBUM ARTIST"])); } if (tag.fieldListMap().contains("COMPOSER")) { - pTrackMetadata->setComposer(toQString(tag.fieldListMap()["COMPOSER"])); + pTrackMetadata->setComposer(toQStringConcat(tag.fieldListMap()["COMPOSER"])); } if (tag.fieldListMap().contains("GROUPING")) { - pTrackMetadata->setGrouping(toQString(tag.fieldListMap()["GROUPING"])); + pTrackMetadata->setGrouping(toQStringConcat(tag.fieldListMap()["GROUPING"])); } if (tag.fieldListMap().contains("DATE")) { - pTrackMetadata->setYear(toQString(tag.fieldListMap()["DATE"])); + pTrackMetadata->setYear(toQStringFirst(tag.fieldListMap()["DATE"])); } } @@ -337,7 +371,7 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) for(TagLib::MP4::ItemListMap::ConstIterator it = tag.itemListMap().begin(); it != tag.itemListMap().end(); ++it) { qDebug() << "MP4" << toQString((*it).first) << "-" - << toQString((*it).second); + << toQStringConcat((*it).second); } } @@ -354,47 +388,42 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) // If this field contains a valid value the integer // BPM value that might have been read before is // overwritten. - pTrackMetadata->setBpmString(toQString(tag.itemListMap()["----:com.apple.iTunes:BPM"])); + pTrackMetadata->setBpmString(toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:BPM"])); } // Get Album Artist if (tag.itemListMap().contains("aART")) { - pTrackMetadata->setAlbumArtist(toQString(tag.itemListMap()["aART"])); + pTrackMetadata->setAlbumArtist(toQStringConcat(tag.itemListMap()["aART"])); } // Get Composer if (tag.itemListMap().contains("\251wrt")) { - pTrackMetadata->setComposer(toQString(tag.itemListMap()["\251wrt"])); + pTrackMetadata->setComposer(toQStringConcat(tag.itemListMap()["\251wrt"])); } // Get Grouping if (tag.itemListMap().contains("\251grp")) { - pTrackMetadata->setGrouping(toQString(tag.itemListMap()["\251grp"])); + pTrackMetadata->setGrouping(toQStringConcat(tag.itemListMap()["\251grp"])); } // Get date/year as string if (tag.itemListMap().contains("\251day")) { - pTrackMetadata->setYear(toQString(tag.itemListMap()["\251day"])); + pTrackMetadata->setYear(toQStringFirst(tag.itemListMap()["\251day"])); } // Get KEY (conforms to Rapid Evolution) if (tag.itemListMap().contains("----:com.apple.iTunes:KEY")) { - QString key = toQString( - tag.itemListMap()["----:com.apple.iTunes:KEY"]); + QString key = toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:KEY"]); pTrackMetadata->setKey(key); } // Apparently iTunes stores replaygain in this property. if (tag.itemListMap().contains("----:com.apple.iTunes:replaygain_album_gain")) { // TODO(XXX) find tracks with this property and check what it looks - // like. - //QString replaygain = toQString(tag.itemListMap()["----:com.apple.iTunes:replaygain_album_gain"]); } //Prefer track gain over album gain. if (tag.itemListMap().contains("----:com.apple.iTunes:replaygain_track_gain")) { // TODO(XXX) find tracks with this property and check what it looks - // like. - //QString replaygain = toQString(tag.itemListMap()["----:com.apple.iTunes:replaygain_track_gain"]); } } From 35d98787e06c6d4e691b4b194830c1312e699862 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 12 Jan 2015 17:50:40 +0100 Subject: [PATCH 070/481] Fix code formatting (review comments) --- src/cachingreaderworker.cpp | 4 ++- src/musicbrainz/chromaprinter.cpp | 6 +++-- src/sources/audiosource.cpp | 41 +++++++++++++++---------------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/cachingreaderworker.cpp b/src/cachingreaderworker.cpp index 031887fad93..dc96b1fc87f 100644 --- a/src/cachingreaderworker.cpp +++ b/src/cachingreaderworker.cpp @@ -64,7 +64,9 @@ void CachingReaderWorker::processChunkReadRequest(ChunkReadRequest* request, frame_position = m_pAudioSource->seekSampleFrame(frame_position); - const Mixxx::AudioSource::size_type frames_read = m_pAudioSource->readSampleFramesStereo(frames_to_read, request->chunk->stereoSamples, kSamplesPerChunk); + const Mixxx::AudioSource::size_type frames_read = + m_pAudioSource->readSampleFramesStereo( + frames_to_read, request->chunk->stereoSamples, kSamplesPerChunk); // If we've run out of music, the AudioSource can return 0 frames/samples. // Remember that AudioSource->getFrameCount() can lie to us about diff --git a/src/musicbrainz/chromaprinter.cpp b/src/musicbrainz/chromaprinter.cpp index 0c5c880211f..a339b309cfc 100644 --- a/src/musicbrainz/chromaprinter.cpp +++ b/src/musicbrainz/chromaprinter.cpp @@ -32,12 +32,14 @@ namespace // Allocate a sample buffer with maximum size to avoid the // implicit allocation of a temporary buffer when reducing - // to the audio signal to stereo. + // the audio signal to stereo. std::vector sampleBuffer( math_max(numFrames * kFingerprintChannels, pAudioSource->frames2samples(numFrames))); DEBUG_ASSERT(2 == kFingerprintChannels); // implicit assumption of the next line - const Mixxx::AudioSource::size_type readFrames = pAudioSource->readSampleFramesStereo(numFrames, &sampleBuffer[0], sampleBuffer.size()); + const Mixxx::AudioSource::size_type readFrames = + pAudioSource->readSampleFramesStereo( + numFrames, &sampleBuffer[0], sampleBuffer.size()); if (readFrames != numFrames) { qDebug() << "oh that's embarrassing I couldn't read the track"; return QString(); diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index 2a1a913b7ef..f3915350db1 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -7,13 +7,15 @@ namespace Mixxx { /*static*/const AudioSource::sample_type AudioSource::kSampleValueZero = - CSAMPLE_ZERO; + CSAMPLE_ZERO; /*static*/const AudioSource::sample_type AudioSource::kSampleValuePeak = - CSAMPLE_PEAK; + CSAMPLE_PEAK; AudioSource::AudioSource() - : m_channelCount(kChannelCountDefault), m_frameRate(kFrameRateDefault), m_frameCount( - kFrameCountDefault), m_bitrate(kBitrateDefault) { + : m_channelCount(kChannelCountDefault), + m_frameRate(kFrameRateDefault), + m_frameCount(kFrameCountDefault), + m_bitrate(kBitrateDefault) { } AudioSource::~AudioSource() { @@ -37,15 +39,14 @@ void AudioSource::reset() { } AudioSource::size_type AudioSource::readSampleFramesStereo( - size_type numberOfFrames, - sample_type* sampleBuffer, - size_type sampleBufferSize) { + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize) { DEBUG_ASSERT((sampleBufferSize / 2) >= numberOfFrames); switch (getChannelCount()) { case 1: // mono channel { const AudioSource::size_type readFrameCount = readSampleFrames( - numberOfFrames, sampleBuffer); + numberOfFrames, sampleBuffer); SampleUtil::doubleMonoToDualMono(sampleBuffer, readFrameCount); return readFrameCount; } @@ -55,29 +56,27 @@ AudioSource::size_type AudioSource::readSampleFramesStereo( } default: // multiple (3 or more) channels { - const size_type numberOfSamplesToRead = - frames2samples(numberOfFrames); + const size_type numberOfSamplesToRead = frames2samples(numberOfFrames); if (numberOfSamplesToRead <= sampleBufferSize) { // efficient in-place transformation const AudioSource::size_type readFrameCount = readSampleFrames( - numberOfFrames, sampleBuffer); - SampleUtil::copyMultiToStereo( - sampleBuffer, sampleBuffer, readFrameCount, getChannelCount()); + numberOfFrames, sampleBuffer); + SampleUtil::copyMultiToStereo(sampleBuffer, sampleBuffer, + readFrameCount, getChannelCount()); return readFrameCount; } else { // inefficient transformation through a temporary buffer qDebug() << "Performance warning:" - << "Allocating a temporary buffer of size" - << numberOfSamplesToRead - << "for reading stereo samples." - << "The size of the provided sample buffer is" - << sampleBufferSize; + << "Allocating a temporary buffer of size" + << numberOfSamplesToRead << "for reading stereo samples." + << "The size of the provided sample buffer is" + << sampleBufferSize; typedef std::vector SampleBuffer; SampleBuffer tempBuffer(numberOfSamplesToRead); const AudioSource::size_type readFrameCount = readSampleFrames( - numberOfFrames, &tempBuffer[0]); - SampleUtil::copyMultiToStereo( - sampleBuffer, &tempBuffer[0], readFrameCount, getChannelCount()); + numberOfFrames, &tempBuffer[0]); + SampleUtil::copyMultiToStereo(sampleBuffer, &tempBuffer[0], + readFrameCount, getChannelCount()); return readFrameCount; } } From cbfa5393037aaeb5db7dba856f2a120c470074e2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 12 Jan 2015 18:10:37 +0100 Subject: [PATCH 071/481] Improve documentation and code formatting of M4A decoder (review comments) --- plugins/soundsourcem4a/audiosourcem4a.cpp | 126 ++++++++++++---------- plugins/soundsourcem4a/audiosourcem4a.h | 2 +- 2 files changed, 68 insertions(+), 60 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 03aa29a4505..118eda16cdd 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -52,8 +52,8 @@ MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { continue; } if (MP4_MPEG4_AUDIO_TYPE == audioType) { - const u_int8_t audioMpeg4Type = MP4GetTrackAudioMpeg4Type(hFile, - trackId); + const u_int8_t audioMpeg4Type = + MP4GetTrackAudioMpeg4Type(hFile, trackId); if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(audioMpeg4Type)) { return trackId; } @@ -67,14 +67,14 @@ MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { } AudioSourceM4A::AudioSourceM4A() - : m_hFile(MP4_INVALID_FILE_HANDLE), - m_trackId(MP4_INVALID_TRACK_ID), - m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), - m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), - m_inputBufferOffset(0), - m_inputBufferLength(0), - m_hDecoder(NULL), - m_curFrameIndex(0) { + : m_hFile(MP4_INVALID_FILE_HANDLE), + m_trackId(MP4_INVALID_TRACK_ID), + m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_inputBufferOffset(0), + m_inputBufferLength(0), + m_hDecoder(NULL), + m_curFrameIndex(0) { } AudioSourceM4A::~AudioSourceM4A() { @@ -125,39 +125,38 @@ Result AudioSourceM4A::open(QString fileName) { m_inputBufferOffset = 0; m_inputBufferLength = 0; + DEBUG_ASSERT(NULL == m_hDecoder); // not already opened + m_hDecoder = NeAACDecOpen(); if (!m_hDecoder) { - // lazy initialization of the AAC decoder - m_hDecoder = NeAACDecOpen(); - if (!m_hDecoder) { - qWarning() << "Failed to open the AAC decoder!"; - return ERR; - } - NeAACDecConfigurationPtr pDecoderConfig = NeAACDecGetCurrentConfiguration( - m_hDecoder); - pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ - pDecoderConfig->downMatrix = 1; /* 5.1 -> stereo */ - pDecoderConfig->defObjectType = LC; - if (!NeAACDecSetConfiguration(m_hDecoder, pDecoderConfig)) { - qWarning() << "Failed to configure AAC decoder!"; - return ERR; - } + qWarning() << "Failed to open the AAC decoder!"; + return ERR; + } + NeAACDecConfigurationPtr pDecoderConfig = NeAACDecGetCurrentConfiguration( + m_hDecoder); + pDecoderConfig->outputFormat = FAAD_FMT_FLOAT; /* 32-bit float */ + pDecoderConfig->downMatrix = 1; /* 5.1 -> stereo */ + pDecoderConfig->defObjectType = LC; + if (!NeAACDecSetConfiguration(m_hDecoder, pDecoderConfig)) { + qWarning() << "Failed to configure AAC decoder!"; + return ERR; } u_int8_t* configBuffer = NULL; u_int32_t configBufferSize = 0; - if (!MP4GetTrackESConfiguration(m_hFile, m_trackId, &configBuffer, - &configBufferSize)) { + if (!MP4GetTrackESConfiguration( + m_hFile, m_trackId, &configBuffer, &configBufferSize)) { /* failed to get mpeg-4 audio config... this is ok. * NeAACDecInit2() will simply use default values instead. */ qWarning() - << "Failed to read the MP4 audio configuration. Continuing with default values."; + << "Failed to read the MP4 audio configuration." + << "Continuing with default values."; } SAMPLERATE_TYPE sampleRate; unsigned char channelCount; - if (0 > NeAACDecInit2(m_hDecoder, configBuffer, configBufferSize, - &sampleRate, &channelCount)) { + if (0 > NeAACDecInit2( + m_hDecoder, configBuffer, configBufferSize, &sampleRate, &channelCount)) { free(configBuffer); qWarning() << "Failed to initialize the AAC decoder!"; return ERR; @@ -172,7 +171,7 @@ Result AudioSourceM4A::open(QString fileName) { // Allocate one block more than the number of sample blocks // that are prefetched const SampleBuffer::size_type prefetchSampleBufferSize = - (kNumberOfPrefetchSampleBlocks + 1) * frames2samples(kFramesPerSampleBlock); + (kNumberOfPrefetchSampleBlocks + 1) * frames2samples(kFramesPerSampleBlock); m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; @@ -211,14 +210,14 @@ bool AudioSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { if (m_curFrameIndex != frameIndex) { - const MP4SampleId sampleBlockId = kMinSampleBlockId - + (frameIndex / kFramesPerSampleBlock); + const MP4SampleId sampleBlockId = + kMinSampleBlockId + (frameIndex / kFramesPerSampleBlock); if (!isValidSampleBlockId(sampleBlockId)) { return m_curFrameIndex; } - if ((frameIndex < m_curFrameIndex) || - !isValidSampleBlockId(m_curSampleBlockId) || - (sampleBlockId > (m_curSampleBlockId + kNumberOfPrefetchSampleBlocks))) { + if ((frameIndex < m_curFrameIndex) || // seeking backwards? + !isValidSampleBlockId(m_curSampleBlockId) || // invalid seek position? + (sampleBlockId > (m_curSampleBlockId + kNumberOfPrefetchSampleBlocks))) { // jumping forward? // Restart decoding one or more blocks of samples backwards // from the calculated starting block to avoid audible glitches. // Implementation note: The type MP4SampleId is unsigned so we @@ -263,16 +262,18 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( // fill input buffer with next block of samples u_int8_t* pInputBuffer = &m_inputBuffer[0]; u_int32_t inputBufferLength = m_inputBuffer.size(); // in/out parameter - if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleBlockId, - &pInputBuffer, &inputBufferLength, - NULL, NULL, NULL, NULL)) { - qWarning() << "Failed to read MP4 input data for sample block" - << m_curSampleBlockId - << "(" - << "min =" << kMinSampleBlockId - << "," - << "max =" << m_maxSampleBlockId - << ")"; + if (!MP4ReadSample( + m_hFile, m_trackId, m_curSampleBlockId, + &pInputBuffer, &inputBufferLength, + NULL, NULL, NULL, NULL)) { + qWarning() + << "Failed to read MP4 input data for sample block" + << m_curSampleBlockId + << "(" + << "min =" << kMinSampleBlockId + << "," + << "max =" << m_maxSampleBlockId + << ")"; break; // abort } ++m_curSampleBlockId; @@ -292,27 +293,34 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( frames2samples(numberOfFramesRemaining) * sizeof(*sampleBuffer); DEBUG_ASSERT(0 < decodeBufferCapacityInBytes); void* pDecodeBuffer = pSampleBuffer; // in/out parameter - NeAACDecDecode2(m_hDecoder, &decFrameInfo, - &m_inputBuffer[m_inputBufferOffset], - m_inputBufferLength - m_inputBufferOffset, - &pDecodeBuffer, decodeBufferCapacityInBytes); + void* pDecodeResult = NeAACDecDecode2( + m_hDecoder, &decFrameInfo, + &m_inputBuffer[m_inputBufferOffset], + m_inputBufferLength - m_inputBufferOffset, + &pDecodeBuffer, decodeBufferCapacityInBytes); + // verify our assumptions about the decoding API DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); // verify the in/out parameter + DEBUG_ASSERT(pSampleBuffer == pDecodeResult); // verify the result pointer + // verify the decoding result if (0 != decFrameInfo.error) { - qWarning() << "AAC decoding error:" - << NeAACDecGetErrorMessage(decFrameInfo.error); + qWarning() + << "AAC decoding error:" + << NeAACDecGetErrorMessage(decFrameInfo.error); break; // abort } - // verify decoded sample data for consistency + // verify the decoded sample data for consistency if (getChannelCount() != decFrameInfo.channels) { - qWarning() << "Corrupt or unsupported AAC file:" - "Unexpected number of channels" - << decFrameInfo.channels << "<>" << getChannelCount(); + qWarning() + << "Corrupt or unsupported AAC file:" + << "Unexpected number of channels" + << decFrameInfo.channels << "<>" << getChannelCount(); break; // abort } if (getFrameRate() != decFrameInfo.samplerate) { - qWarning() << "Corrupt or unsupported AAC file:" - << "Unexpected sample rate" - << decFrameInfo.samplerate << "<>" << getFrameRate(); + qWarning() + << "Corrupt or unsupported AAC file:" + << "Unexpected sample rate" + << decFrameInfo.samplerate << "<>" << getFrameRate(); break; // abort } // consume input data diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index d8a9cc457c4..781d0af957c 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -53,7 +53,7 @@ class AudioSourceM4A : public AudioSource { InputBuffer::size_type m_inputBufferOffset; InputBuffer::size_type m_inputBufferLength; - faacDecHandle m_hDecoder; + NeAACDecHandle m_hDecoder; typedef std::vector SampleBuffer; SampleBuffer m_prefetchSampleBuffer; From 632df06095336b74724a67cee339d17d8c29d650 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 12 Jan 2015 18:43:47 +0100 Subject: [PATCH 072/481] Fix coding style issues (indentation) --- plugins/soundsourcem4a/soundsourcem4a.cpp | 4 +- plugins/soundsourcewv/audiosourcewv.cpp | 12 +- plugins/soundsourcewv/soundsourcewv.cpp | 6 +- src/metadata/audiotagger.cpp | 32 ++- src/metadata/trackmetadata.cpp | 10 +- src/metadata/trackmetadatataglib.cpp | 297 ++++++++++++++-------- src/sources/audiosourceflac.cpp | 162 ++++++------ src/sources/audiosourcemp3.cpp | 120 +++++---- src/sources/audiosourceoggvorbis.cpp | 21 +- src/sources/audiosourceopus.cpp | 28 +- src/sources/audiosourcesndfile.cpp | 33 ++- src/sources/soundsourceflac.cpp | 2 +- src/sources/soundsourcemp3.cpp | 2 +- src/sources/soundsourceoggvorbis.cpp | 2 +- src/sources/soundsourceopus.cpp | 2 +- src/sources/soundsourcesndfile.cpp | 4 +- 16 files changed, 451 insertions(+), 286 deletions(-) diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index e8384bb1593..c1d9c955ee0 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -87,9 +87,9 @@ extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { extern "C" MY_EXPORT char** supportedFileExtensions() { const QList supportedFileExtensions( - Mixxx::SoundSourceM4A::supportedFileExtensions()); + Mixxx::SoundSourceM4A::supportedFileExtensions()); return Mixxx::SoundSourcePlugin::allocFileExtensions( - supportedFileExtensions); + supportedFileExtensions); } extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions) { diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index ced9991085e..99c4699f21b 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -3,7 +3,8 @@ namespace Mixxx { AudioSourceWV::AudioSourceWV() - : m_wpc(NULL), m_sampleScale(0.0f) { + : m_wpc(NULL), + m_sampleScale(0.0f) { } AudioSourceWV::~AudioSourceWV() { @@ -23,8 +24,9 @@ AudioSourcePointer AudioSourceWV::create(QString fileName) { Result AudioSourceWV::open(QString fileName) { char msg[80]; // hold possible error message - m_wpc = WavpackOpenFileInput(fileName.toLocal8Bit().constData(), msg, - OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); + m_wpc = WavpackOpenFileInput( + fileName.toLocal8Bit().constData(), msg, + OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); if (!m_wpc) { qDebug() << "SSWV::open: failed to open file : " << msg; return ERR; @@ -66,13 +68,13 @@ AudioSource::size_type AudioSourceWV::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { // static assert: sizeof(sample_type) == sizeof(int32_t) size_type unpackCount = WavpackUnpackSamples(m_wpc, - reinterpret_cast(sampleBuffer), numberOfFrames); + reinterpret_cast(sampleBuffer), numberOfFrames); if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { // signed integer -> float const size_type sampleCount = frames2samples(unpackCount); for (size_type i = 0; i < sampleCount; ++i) { const int32_t sampleValue = - reinterpret_cast(sampleBuffer)[i]; + reinterpret_cast(sampleBuffer)[i]; sampleBuffer[i] = sample_type(sampleValue) * m_sampleScale; } } diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 3d019e7615d..96838b8d343 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -14,7 +14,7 @@ QList SoundSourceWV::supportedFileExtensions() { } SoundSourceWV::SoundSourceWV(QString fileName) - : SoundSourcePlugin(fileName, "wv") { + : SoundSourcePlugin(fileName, "wv") { } Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { @@ -70,9 +70,9 @@ extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { extern "C" MY_EXPORT char** supportedFileExtensions() { const QList supportedFileExtensions( - Mixxx::SoundSourceWV::supportedFileExtensions()); + Mixxx::SoundSourceWV::supportedFileExtensions()); return Mixxx::SoundSourcePlugin::allocFileExtensions( - supportedFileExtensions); + supportedFileExtensions); } extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions) { diff --git a/src/metadata/audiotagger.cpp b/src/metadata/audiotagger.cpp index 0331f0e0465..5eb8d617e7f 100644 --- a/src/metadata/audiotagger.cpp +++ b/src/metadata/audiotagger.cpp @@ -19,10 +19,22 @@ #include +namespace { + +inline +SecurityTokenPointer openSecurityToken(QFileInfo file, const SecurityTokenPointer& pToken) { + if (pToken.isNull()) { + return Sandbox::openSecurityToken(file, true); + } else { + return pToken; + } +} + +} // anonymous namespace + AudioTagger::AudioTagger(const QString& file, SecurityTokenPointer pToken) - : m_file(file), - m_pSecurityToken(pToken.isNull() ? Sandbox::openSecurityToken( - m_file, true) : pToken) { + : m_file(file), + m_pSecurityToken(openSecurityToken(m_file, pToken)) { } AudioTagger::~AudioTagger() { @@ -37,41 +49,41 @@ bool AudioTagger::save(const Mixxx::TrackMetadata& trackMetadata) { if (filePath.endsWith(".mp3", Qt::CaseInsensitive)) { QScopedPointer pMpegFile( - new TagLib::MPEG::File(filePathChars)); + new TagLib::MPEG::File(filePathChars)); writeID3v2Tag(pMpegFile->ID3v2Tag(true), trackMetadata); // mandatory writeAPETag(pMpegFile->APETag(false), trackMetadata); // optional pFile.reset(pMpegFile.take()); // transfer ownership } else if (filePath.endsWith(".m4a", Qt::CaseInsensitive)) { QScopedPointer pMp4File( - new TagLib::MP4::File(filePathChars)); + new TagLib::MP4::File(filePathChars)); writeMP4Tag(pMp4File->tag(), trackMetadata); pFile.reset(pMp4File.take()); // transfer ownership } else if (filePath.endsWith(".ogg", Qt::CaseInsensitive)) { QScopedPointer pOggFile( - new TagLib::Ogg::Vorbis::File(filePathChars)); + new TagLib::Ogg::Vorbis::File(filePathChars)); writeXiphComment(pOggFile->tag(), trackMetadata); pFile.reset(pOggFile.take()); // transfer ownership } else if (filePath.endsWith(".flac", Qt::CaseInsensitive)) { QScopedPointer pFlacFile( - new TagLib::FLAC::File(filePathChars)); + new TagLib::FLAC::File(filePathChars)); writeXiphComment(pFlacFile->xiphComment(true), trackMetadata); // mandatory writeID3v2Tag(pFlacFile->ID3v2Tag(false), trackMetadata); // optional pFile.reset(pFlacFile.take()); // transfer ownership } else if (filePath.endsWith(".wav", Qt::CaseInsensitive)) { QScopedPointer pWavFile( - new TagLib::RIFF::WAV::File(filePathChars)); + new TagLib::RIFF::WAV::File(filePathChars)); writeID3v2Tag(pWavFile->tag(), trackMetadata); pFile.reset(pWavFile.take()); // transfer ownership } else if (filePath.endsWith(".aif", Qt::CaseInsensitive) || filePath.endsWith(".aiff", Qt::CaseInsensitive)) { QScopedPointer pAiffFile( - new TagLib::RIFF::AIFF::File(filePathChars)); + new TagLib::RIFF::AIFF::File(filePathChars)); writeID3v2Tag(pAiffFile->tag(), trackMetadata); pFile.reset(pAiffFile.take()); // transfer ownership #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) } else if (filePath.endsWith(".opus", Qt::CaseInsensitive)) { QScopedPointer pOpusFile( - new TagLib::Ogg::Opus::File(filePathChars)); + new TagLib::Ogg::Opus::File(filePathChars)); writeXiphComment(pOpusFile->tag(), trackMetadata); pFile.reset(pOpusFile.take()); // transfer ownership #endif diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 47c7b5a6335..9f029f4530f 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -13,7 +13,12 @@ namespace Mixxx { /*static*/ const double TrackMetadata::REPLAYGAIN_0DB = 1.0f; TrackMetadata::TrackMetadata() - : m_channels(0), m_sampleRate(0), m_bitrate(0), m_duration(0), m_bpm(BPM_UNDEFINED), m_replayGain(REPLAYGAIN_UNDEFINED) { + : m_channels(0), + m_sampleRate(0), + m_bitrate(0), + m_duration(0), + m_bpm(BPM_UNDEFINED), + m_replayGain(REPLAYGAIN_UNDEFINED) { } double TrackMetadata::parseBpmString(const QString& sBpm) { @@ -54,7 +59,8 @@ float TrackMetadata::parseReplayGainDbString(QString sReplayGainDb) { return REPLAYGAIN_UNDEFINED; } // I found some mp3s of mine with replaygain tag set to 0dB even if not normalized. - // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by setting value to 0 (i.e. rescan via analyserrg) + // This is because of Rapid Evolution 3, I suppose. I prefer to rescan them by + // setting value to 0 (i.e. rescan via analyserrg) if (REPLAYGAIN_0DB == replayGain) { return REPLAYGAIN_UNDEFINED; } diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index 10385638cf7..de3e0a59bb0 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -53,7 +53,8 @@ QString toQString(const TagLib::ID3v2::Frame& frame) { // single string. QString toQStringConcat(const TagLib::ID3v2::FrameList& frameList) { QString result; - for (TagLib::ID3v2::FrameList::ConstIterator i(frameList.begin()); frameList.end() != i; ++i) { + for (TagLib::ID3v2::FrameList::ConstIterator + i(frameList.begin()); frameList.end() != i; ++i) { result += toQString(**i); } return result; @@ -61,7 +62,8 @@ QString toQStringConcat(const TagLib::ID3v2::FrameList& frameList) { // Returns the first ID3v2 frame that is not empty. QString toQStringFirst(const TagLib::ID3v2::FrameList& frameList) { - for (TagLib::ID3v2::FrameList::ConstIterator i(frameList.begin()); frameList.end() != i; ++i) { + for (TagLib::ID3v2::FrameList::ConstIterator + i(frameList.begin()); frameList.end() != i; ++i) { const QString value(toQString(**i).trimmed()); if (!value.isEmpty()) { return value; @@ -80,7 +82,8 @@ QString toQStringConcat(const TagLib::MP4::Item& mp4Item) { // Returns the first MP4 item string that is not empty. QString toQStringFirst(const TagLib::MP4::Item& mp4Item) { const TagLib::StringList strList(mp4Item.toStringList()); - for (TagLib::StringList::ConstIterator i(strList.begin()); strList.end() != i; ++i) { + for (TagLib::StringList::ConstIterator + i(strList.begin()); strList.end() != i; ++i) { const QString value(toQString(*i).trimmed()); if (!value.isEmpty()) { return value; @@ -138,11 +141,12 @@ bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& file pTrackMetadata->setBitrate(properties->bitrate()); if (kDebugMetadata) { - qDebug() << "TagLib" - << "channels" << pTrackMetadata->getChannels() - << "sampleRate" << pTrackMetadata->getSampleRate() - << "bitrate" << pTrackMetadata->getBitrate() - << "duration" << pTrackMetadata->getDuration(); + qDebug() + << "TagLib" + << "channels" << pTrackMetadata->getChannels() + << "sampleRate" << pTrackMetadata->getSampleRate() + << "bitrate" << pTrackMetadata->getBitrate() + << "duration" << pTrackMetadata->getDuration(); } return true; @@ -169,14 +173,15 @@ void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { } if (kDebugMetadata) { - qDebug() << "TagLib" - << "title" << pTrackMetadata->getTitle() - << "artist" << pTrackMetadata->getArtist() - << "album" << pTrackMetadata->getAlbum() - << "comment" << pTrackMetadata->getComment() - << "genre" << pTrackMetadata->getGenre() - << "year" << pTrackMetadata->getYear() - << "trackNumber" << pTrackMetadata->getTrackNumber(); + qDebug() + << "TagLib" + << "title" << pTrackMetadata->getTitle() + << "artist" << pTrackMetadata->getArtist() + << "album" << pTrackMetadata->getAlbum() + << "comment" << pTrackMetadata->getComment() + << "genre" << pTrackMetadata->getGenre() + << "year" << pTrackMetadata->getYear() + << "trackNumber" << pTrackMetadata->getTrackNumber(); } } @@ -184,10 +189,13 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) // Print every frame in the file. if (kDebugMetadata) { - for(TagLib::ID3v2::FrameList::ConstIterator it = tag.frameList().begin(); - it != tag.frameList().end(); ++it) { - qDebug() << "ID3V2" << (*it)->frameID().data() << "-" - << toQString((*it)->toString()); + for(TagLib::ID3v2::FrameList::ConstIterator + it(tag.frameList().begin()); it != tag.frameList().end(); ++it) { + qDebug() + << "ID3V2" + << (*it)->frameID().data() + << "-" + << toQString((*it)->toString()); } } @@ -195,105 +203,129 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) const TagLib::ID3v2::FrameList bpmFrame(tag.frameListMap()["TBPM"]); if (!bpmFrame.isEmpty()) { - pTrackMetadata->setBpmString(toQStringFirst(bpmFrame)); + pTrackMetadata->setBpmString( + toQStringFirst(bpmFrame)); } const TagLib::ID3v2::FrameList keyFrame(tag.frameListMap()["TKEY"]); if (!keyFrame.isEmpty()) { - pTrackMetadata->setKey(toQStringFirst(keyFrame)); + pTrackMetadata->setKey( + toQStringFirst(keyFrame)); } // Foobar2000-style ID3v2.3.0 tags // TODO: Check if everything is ok. TagLib::ID3v2::FrameList textFrames(tag.frameListMap()["TXXX"]); - for (TagLib::ID3v2::FrameList::ConstIterator it = textFrames.begin(); it != textFrames.end(); ++it) { + for (TagLib::ID3v2::FrameList::ConstIterator + it(textFrames.begin()); it != textFrames.end(); ++it) { TagLib::ID3v2::UserTextIdentificationFrame* replaygainFrame = dynamic_cast(*it); if (replaygainFrame && replaygainFrame->fieldList().size() >= 2) { - const QString desc(toQString(replaygainFrame->description()).toLower()); + const QString desc( + toQString(replaygainFrame->description()).toLower()); if (desc == "replaygain_album_gain") { - const QString albumGain(toQString(replaygainFrame->fieldList()[1])); - pTrackMetadata->setReplayGainDbString(albumGain); + pTrackMetadata->setReplayGainDbString( + toQString(replaygainFrame->fieldList()[1])); } // Prefer track gain over album gain. if (desc == "replaygain_track_gain") { - const QString trackGain(toQString(replaygainFrame->fieldList()[1])); - pTrackMetadata->setReplayGainDbString(trackGain); + pTrackMetadata->setReplayGainDbString( + toQString(replaygainFrame->fieldList()[1])); } } } const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()["TPE2"]); if (!albumArtistFrame.isEmpty()) { - pTrackMetadata->setAlbumArtist(toQStringConcat(albumArtistFrame)); + pTrackMetadata->setAlbumArtist( + toQStringConcat(albumArtistFrame)); } if (pTrackMetadata->getAlbum().isEmpty()) { const TagLib::ID3v2::FrameList originalAlbumFrame(tag.frameListMap()["TOAL"]); - pTrackMetadata->setAlbum(toQStringConcat(originalAlbumFrame)); + pTrackMetadata->setAlbum( + toQStringConcat(originalAlbumFrame)); } const TagLib::ID3v2::FrameList composerFrame(tag.frameListMap()["TCOM"]); if (!composerFrame.isEmpty()) { - pTrackMetadata->setComposer(toQStringConcat(composerFrame)); + pTrackMetadata->setComposer( + toQStringConcat(composerFrame)); } const TagLib::ID3v2::FrameList groupingFrame(tag.frameListMap()["TIT1"]); if (!groupingFrame.isEmpty()) { - pTrackMetadata->setGrouping(toQStringConcat(groupingFrame)); + pTrackMetadata->setGrouping( + toQStringConcat(groupingFrame)); } // ID3v2.4.0: TDRC replaces TYER + TDAT const TagLib::ID3v2::FrameList recordingDateFrame(tag.frameListMap()["TDRC"]); if (!recordingDateFrame.isEmpty()) { - pTrackMetadata->setYear(toQStringFirst(recordingDateFrame)); + pTrackMetadata->setYear( + toQStringFirst(recordingDateFrame)); } } void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { if (kDebugMetadata) { - for(TagLib::APE::ItemListMap::ConstIterator it = tag.itemListMap().begin(); - it != tag.itemListMap().end(); ++it) { - qDebug() << "APE" << toQString((*it).first) << "-" << toQString((*it).second.toString()); + for(TagLib::APE::ItemListMap::ConstIterator + it(tag.itemListMap().begin()); it != tag.itemListMap().end(); ++it) { + qDebug() + << "APE" + << toQString((*it).first) + << "-" + << toQString((*it).second.toString()); } } readTag(pTrackMetadata, tag); if (tag.itemListMap().contains("BPM")) { - pTrackMetadata->setBpmString(toQString(tag.itemListMap()["BPM"])); + pTrackMetadata->setBpmString( + toQString(tag.itemListMap()["BPM"])); } if (tag.itemListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pTrackMetadata->setReplayGainDbString(toQString(tag.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); + pTrackMetadata->setReplayGainDbString( + toQString(tag.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. if (tag.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainDbString(toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); + pTrackMetadata->setReplayGainDbString( + toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); } if (tag.itemListMap().contains("Album Artist")) { - pTrackMetadata->setAlbumArtist(toQString(tag.itemListMap()["Album Artist"])); + pTrackMetadata->setAlbumArtist( + toQString(tag.itemListMap()["Album Artist"])); } if (tag.itemListMap().contains("Composer")) { - pTrackMetadata->setComposer(toQString(tag.itemListMap()["Composer"])); + pTrackMetadata->setComposer( + toQString(tag.itemListMap()["Composer"])); } if (tag.itemListMap().contains("Grouping")) { - pTrackMetadata->setGrouping(toQString(tag.itemListMap()["Grouping"])); + pTrackMetadata->setGrouping( + toQString(tag.itemListMap()["Grouping"])); } if (tag.itemListMap().contains("Year")) { - pTrackMetadata->setYear(toQString(tag.itemListMap()["Year"])); + pTrackMetadata->setYear( + toQString(tag.itemListMap()["Year"])); } } void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag) { if (kDebugMetadata) { - for (TagLib::Ogg::FieldListMap::ConstIterator it = tag.fieldListMap().begin(); - it != tag.fieldListMap().end(); ++it) { - qDebug() << "XIPH" << toQString((*it).first) << "-" << toQString((*it).second.toString()); + for (TagLib::Ogg::FieldListMap::ConstIterator + it(tag.fieldListMap().begin()); it != tag.fieldListMap().end(); ++it) { + qDebug() + << "XIPH" + << toQString((*it).first) + << "-" + << toQString((*it).second.toString()); } } @@ -303,26 +335,32 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme // instead DESCRIPTION. If the comment field (correctly populated by TagLib // from DESCRIPTION) is still empty we will additionally read this field. // Reference: http://www.xiph.org/vorbis/doc/v-comment.html - if (pTrackMetadata->getComment().isEmpty() && tag.fieldListMap().contains("COMMENT")) { - pTrackMetadata->setComment(toQStringConcat(tag.fieldListMap()["COMMENT"])); + if (pTrackMetadata->getComment().isEmpty() && + tag.fieldListMap().contains("COMMENT")) { + pTrackMetadata->setComment( + toQStringConcat(tag.fieldListMap()["COMMENT"])); } // Some tags use "BPM" so check for that. if (tag.fieldListMap().contains("BPM")) { - pTrackMetadata->setBpmString(toQStringFirst(tag.fieldListMap()["BPM"])); + pTrackMetadata->setBpmString( + toQStringFirst(tag.fieldListMap()["BPM"])); } // Give preference to the "TEMPO" tag which seems to be more standard if (tag.fieldListMap().contains("TEMPO")) { - pTrackMetadata->setBpmString(toQStringFirst(tag.fieldListMap()["TEMPO"])); + pTrackMetadata->setBpmString( + toQStringFirst(tag.fieldListMap()["TEMPO"])); } if (tag.fieldListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { - pTrackMetadata->setReplayGainDbString(toQStringFirst(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); + pTrackMetadata->setReplayGainDbString( + toQStringFirst(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. if (tag.fieldListMap().contains("REPLAYGAIN_TRACK_GAIN")) { - pTrackMetadata->setReplayGainDbString(toQStringFirst(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); + pTrackMetadata->setReplayGainDbString( + toQStringFirst(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); } /* @@ -334,23 +372,31 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme * or a "KEY" vorbis comment. */ if (tag.fieldListMap().contains("KEY")) { - pTrackMetadata->setKey(toQStringFirst(tag.fieldListMap()["KEY"])); + pTrackMetadata->setKey( + toQStringFirst(tag.fieldListMap()["KEY"])); } - if (pTrackMetadata->getKey().isEmpty() && tag.fieldListMap().contains("INITIALKEY")) { + if (pTrackMetadata->getKey().isEmpty() && + tag.fieldListMap().contains("INITIALKEY")) { // try alternative field name - pTrackMetadata->setKey(toQStringFirst(tag.fieldListMap()["INITIALKEY"])); + pTrackMetadata->setKey( + toQStringFirst(tag.fieldListMap()["INITIALKEY"])); } if (tag.fieldListMap().contains("ALBUMARTIST")) { - pTrackMetadata->setAlbumArtist(toQStringConcat(tag.fieldListMap()["ALBUMARTIST"])); + pTrackMetadata->setAlbumArtist( + toQStringConcat(tag.fieldListMap()["ALBUMARTIST"])); } - if (pTrackMetadata->getAlbumArtist().isEmpty() && tag.fieldListMap().contains("ALBUM_ARTIST")) { + if (pTrackMetadata->getAlbumArtist().isEmpty() && + tag.fieldListMap().contains("ALBUM_ARTIST")) { // try alternative field name - pTrackMetadata->setAlbumArtist(toQStringConcat(tag.fieldListMap()["ALBUM_ARTIST"])); + pTrackMetadata->setAlbumArtist( + toQStringConcat(tag.fieldListMap()["ALBUM_ARTIST"])); } - if (pTrackMetadata->getAlbumArtist().isEmpty() && tag.fieldListMap().contains("ALBUM ARTIST")) { + if (pTrackMetadata->getAlbumArtist().isEmpty() && + tag.fieldListMap().contains("ALBUM ARTIST")) { // try alternative field name - pTrackMetadata->setAlbumArtist(toQStringConcat(tag.fieldListMap()["ALBUM ARTIST"])); + pTrackMetadata->setAlbumArtist( + toQStringConcat(tag.fieldListMap()["ALBUM ARTIST"])); } if (tag.fieldListMap().contains("COMPOSER")) { @@ -368,10 +414,13 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) { if (kDebugMetadata) { - for(TagLib::MP4::ItemListMap::ConstIterator it = tag.itemListMap().begin(); - it != tag.itemListMap().end(); ++it) { - qDebug() << "MP4" << toQString((*it).first) << "-" - << toQStringConcat((*it).second); + for(TagLib::MP4::ItemListMap::ConstIterator + it(tag.itemListMap().begin()); it != tag.itemListMap().end(); ++it) { + qDebug() + << "MP4" + << toQString((*it).first) + << "-" + << toQStringConcat((*it).second); } } @@ -380,7 +429,8 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) // Get BPM if (tag.itemListMap().contains("tmpo")) { // Read the BPM as an integer value. - pTrackMetadata->setBpm(tag.itemListMap()["tmpo"].toInt()); + pTrackMetadata->setBpm( + tag.itemListMap()["tmpo"].toInt()); } if (tag.itemListMap().contains("----:com.apple.iTunes:BPM")) { // This is the preferred field for storing the BPM @@ -388,33 +438,38 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) // If this field contains a valid value the integer // BPM value that might have been read before is // overwritten. - pTrackMetadata->setBpmString(toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:BPM"])); + pTrackMetadata->setBpmString( + toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:BPM"])); } // Get Album Artist if (tag.itemListMap().contains("aART")) { - pTrackMetadata->setAlbumArtist(toQStringConcat(tag.itemListMap()["aART"])); + pTrackMetadata->setAlbumArtist( + toQStringConcat(tag.itemListMap()["aART"])); } // Get Composer if (tag.itemListMap().contains("\251wrt")) { - pTrackMetadata->setComposer(toQStringConcat(tag.itemListMap()["\251wrt"])); + pTrackMetadata->setComposer( + toQStringConcat(tag.itemListMap()["\251wrt"])); } // Get Grouping if (tag.itemListMap().contains("\251grp")) { - pTrackMetadata->setGrouping(toQStringConcat(tag.itemListMap()["\251grp"])); + pTrackMetadata->setGrouping( + toQStringConcat(tag.itemListMap()["\251grp"])); } // Get date/year as string if (tag.itemListMap().contains("\251day")) { - pTrackMetadata->setYear(toQStringFirst(tag.itemListMap()["\251day"])); + pTrackMetadata->setYear( + toQStringFirst(tag.itemListMap()["\251day"])); } // Get KEY (conforms to Rapid Evolution) if (tag.itemListMap().contains("----:com.apple.iTunes:KEY")) { - QString key = toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:KEY"]); - pTrackMetadata->setKey(key); + pTrackMetadata->setKey( + toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:KEY"])); } // Apparently iTunes stores replaygain in this property. @@ -431,8 +486,9 @@ QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag) { QImage coverArt; TagLib::ID3v2::FrameList covertArtFrame = tag.frameListMap()["APIC"]; if (!covertArtFrame.isEmpty()) { - TagLib::ID3v2::AttachedPictureFrame* picframe = static_cast - (covertArtFrame.front()); + TagLib::ID3v2::AttachedPictureFrame* picframe = + static_cast( + covertArtFrame.front()); TagLib::ByteVector data = picframe->picture(); coverArt = QImage::fromData( reinterpret_cast(data.data()), data.size()); @@ -476,8 +532,8 @@ QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag) { QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& tag) { QImage coverArt; if (tag.itemListMap().contains("covr")) { - TagLib::MP4::CoverArtList coverArtList = tag.itemListMap()["covr"] - .toCoverArtList(); + TagLib::MP4::CoverArtList coverArtList = + tag.itemListMap()["covr"].toCoverArtList(); TagLib::ByteVector data = coverArtList.front().data(); coverArt = QImage::fromData( reinterpret_cast(data.data()), data.size()); @@ -550,14 +606,20 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) writeTag(pTag, trackMetadata); // additional tags - writeID3v2TextIdentificationFrame(pTag, "TPE2", trackMetadata.getAlbumArtist()); - writeID3v2TextIdentificationFrame(pTag, "TBPM", formatBpmString(trackMetadata.getBpm())); - writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); - writeID3v2TextIdentificationFrame(pTag, "TCOM", trackMetadata.getComposer()); - writeID3v2TextIdentificationFrame(pTag, "TIT1", trackMetadata.getGrouping()); + writeID3v2TextIdentificationFrame(pTag, + "TPE2", trackMetadata.getAlbumArtist()); + writeID3v2TextIdentificationFrame(pTag, + "TBPM", formatBpmString(trackMetadata.getBpm())); + writeID3v2TextIdentificationFrame(pTag, + "TKEY", trackMetadata.getKey()); + writeID3v2TextIdentificationFrame(pTag, + "TCOM", trackMetadata.getComposer()); + writeID3v2TextIdentificationFrame(pTag, + "TIT1", trackMetadata.getGrouping()); if (4 <= pHeader->majorVersion()) { // ID3v2.4.0: TDRC replaces TYER + TDAT - writeID3v2TextIdentificationFrame(pTag, "TDRC", formatString(trackMetadata.getYear())); + writeID3v2TextIdentificationFrame(pTag, + "TDRC", formatString(trackMetadata.getYear())); } return true; @@ -571,11 +633,16 @@ bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { // Write common metadata writeTag(pTag, trackMetadata); - pTag->addValue("BPM", toTagLibString(formatBpmString(trackMetadata.getBpm())), true); - pTag->addValue("Album Artist", toTagLibString(trackMetadata.getAlbumArtist()), true); - pTag->addValue("Composer", toTagLibString(trackMetadata.getComposer()), true); - pTag->addValue("Grouping", toTagLibString(trackMetadata.getGrouping()), true); - pTag->addValue("Year", toTagLibString(trackMetadata.getYear()), true); + pTag->addValue("BPM", + toTagLibString(formatBpmString(trackMetadata.getBpm())), true); + pTag->addValue("Album Artist", + toTagLibString(trackMetadata.getAlbumArtist()), true); + pTag->addValue("Composer", + toTagLibString(trackMetadata.getComposer()), true); + pTag->addValue("Grouping", + toTagLibString(trackMetadata.getGrouping()), true); + pTag->addValue("Year", + toTagLibString(trackMetadata.getYear()), true); return true; } @@ -592,27 +659,35 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& track // thus, we have to remove the old comment and add the new one pTag->removeField("ALBUMARTIST"); - pTag->addField("ALBUMARTIST", toTagLibString(trackMetadata.getAlbumArtist())); + pTag->addField("ALBUMARTIST", + toTagLibString(trackMetadata.getAlbumArtist())); // Some tools use "BPM" so write that. pTag->removeField("BPM"); - pTag->addField("BPM", toTagLibString(formatBpmString(trackMetadata.getBpm()))); + pTag->addField("BPM", + toTagLibString(formatBpmString(trackMetadata.getBpm()))); pTag->removeField("TEMPO"); - pTag->addField("TEMPO", toTagLibString(formatBpmString(trackMetadata.getBpm()))); + pTag->addField("TEMPO", + toTagLibString(formatBpmString(trackMetadata.getBpm()))); pTag->removeField("INITIALKEY"); - pTag->addField("INITIALKEY", toTagLibString(trackMetadata.getKey())); + pTag->addField("INITIALKEY", + toTagLibString(trackMetadata.getKey())); pTag->removeField("KEY"); - pTag->addField("KEY", toTagLibString(trackMetadata.getKey())); + pTag->addField("KEY", + toTagLibString(trackMetadata.getKey())); pTag->removeField("COMPOSER"); - pTag->addField("COMPOSER", toTagLibString(trackMetadata.getComposer())); + pTag->addField("COMPOSER", + toTagLibString(trackMetadata.getComposer())); pTag->removeField("GROUPING"); - pTag->addField("GROUPING", toTagLibString(trackMetadata.getGrouping())); + pTag->addField("GROUPING", + toTagLibString(trackMetadata.getGrouping())); pTag->removeField("DATE"); - pTag->addField("DATE", toTagLibString(trackMetadata.getYear())); + pTag->addField("DATE", + toTagLibString(trackMetadata.getYear())); return true; } @@ -620,11 +695,17 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& track namespace { template - inline void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, const T& value) { + inline void writeMP4Atom( + TagLib::MP4::Tag* pTag, + const TagLib::String& key, + const T& value) { pTag->itemListMap()[key] = value; } - void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, const QString& value) { + void writeMP4Atom( + TagLib::MP4::Tag* pTag, + const TagLib::String& key, + const QString& value) { if (value.isEmpty()) { pTag->itemListMap().erase(key); } else { @@ -642,18 +723,24 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { // Write common metadata writeTag(pTag, trackMetadata); - writeMP4Atom(pTag, "aART", trackMetadata.getAlbumArtist()); - writeMP4Atom(pTag, "\251wrt", trackMetadata.getComposer()); - writeMP4Atom(pTag, "\251grp", trackMetadata.getGrouping()); - writeMP4Atom(pTag, "\251day", trackMetadata.getYear()); + writeMP4Atom(pTag, "aART", + trackMetadata.getAlbumArtist()); + writeMP4Atom(pTag, "\251wrt", + trackMetadata.getComposer()); + writeMP4Atom(pTag, "\251grp", + trackMetadata.getGrouping()); + writeMP4Atom(pTag, "\251day", + trackMetadata.getYear()); if (trackMetadata.isBpmValid()) { - const int tmpoValue = round(trackMetadata.getBpm()); - writeMP4Atom(pTag, "tmpo", tmpoValue); + writeMP4Atom(pTag, "tmpo", + (int) round(trackMetadata.getBpm())); } else { pTag->itemListMap().erase("tmpo"); } - writeMP4Atom(pTag, "----:com.apple.iTunes:BPM", formatBpmString(trackMetadata.getBpm())); - writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", trackMetadata.getKey()); + writeMP4Atom(pTag, "----:com.apple.iTunes:BPM", + formatBpmString(trackMetadata.getBpm())); + writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", + trackMetadata.getKey()); return true; } diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 84e109e6039..263c2a6b3c6 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -13,22 +13,22 @@ namespace // begin callbacks (have to be regular functions because normal libFLAC isn't C++-aware) FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder*, - FLAC__byte buffer[], size_t *bytes, void *client_data) { + FLAC__byte buffer[], size_t *bytes, void *client_data) { return static_cast(client_data)->flacRead(buffer, bytes); } FLAC__StreamDecoderSeekStatus FLAC_seek_cb(const FLAC__StreamDecoder*, - FLAC__uint64 absolute_byte_offset, void *client_data) { + FLAC__uint64 absolute_byte_offset, void *client_data) { return static_cast(client_data)->flacSeek(absolute_byte_offset); } FLAC__StreamDecoderTellStatus FLAC_tell_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *absolute_byte_offset, void *client_data) { + FLAC__uint64 *absolute_byte_offset, void *client_data) { return static_cast(client_data)->flacTell(absolute_byte_offset); } FLAC__StreamDecoderLengthStatus FLAC_length_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *stream_length, void *client_data) { + FLAC__uint64 *stream_length, void *client_data) { return static_cast(client_data)->flacLength(stream_length); } @@ -37,18 +37,18 @@ FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder*, void *client_data) { } FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder*, - const FLAC__Frame *frame, const FLAC__int32 * const buffer[], - void *client_data) { + const FLAC__Frame *frame, const FLAC__int32 * const buffer[], + void *client_data) { return static_cast(client_data)->flacWrite(frame, buffer); } void FLAC_metadata_cb(const FLAC__StreamDecoder*, - const FLAC__StreamMetadata *metadata, void *client_data) { + const FLAC__StreamMetadata *metadata, void *client_data) { static_cast(client_data)->flacMetadata(metadata); } void FLAC_error_cb(const FLAC__StreamDecoder*, - FLAC__StreamDecoderErrorStatus status, void *client_data) { + FLAC__StreamDecoderErrorStatus status, void *client_data) { static_cast(client_data)->flacError(status); } @@ -56,10 +56,15 @@ void FLAC_error_cb(const FLAC__StreamDecoder*, } AudioSourceFLAC::AudioSourceFLAC(QString fileName) - : m_file(fileName), m_decoder(NULL), m_minBlocksize( - 0), m_maxBlocksize(0), m_minFramesize(0), m_maxFramesize(0), m_sampleScale( - kSampleValueZero), m_decodeSampleBufferReadOffset(0), m_decodeSampleBufferWriteOffset( - 0) { + : m_file(fileName), + m_decoder(NULL), + m_minBlocksize(0), + m_maxBlocksize(0), + m_minFramesize(0), + m_maxFramesize(0), + m_sampleScale(kSampleValueZero), + m_decodeSampleBufferReadOffset(0), + m_decodeSampleBufferWriteOffset(0) { } AudioSourceFLAC::~AudioSourceFLAC() { @@ -67,7 +72,8 @@ AudioSourceFLAC::~AudioSourceFLAC() { } AudioSourcePointer AudioSourceFLAC::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceFLAC(fileName)); + QSharedPointer pAudioSource( + new AudioSourceFLAC(fileName)); if (OK == pAudioSource->open()) { // success return pAudioSource; @@ -90,16 +96,19 @@ Result AudioSourceFLAC::open() { } FLAC__stream_decoder_set_md5_checking(m_decoder, FALSE); const FLAC__StreamDecoderInitStatus initStatus( - FLAC__stream_decoder_init_stream(m_decoder, FLAC_read_cb, - FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, - FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); + FLAC__stream_decoder_init_stream(m_decoder, FLAC_read_cb, + FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, + FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { - qWarning() << "SSFLAC: decoder init failed:" << initStatus; + qWarning() + << "SSFLAC: decoder init failed:" + << initStatus; return ERR; } if (!FLAC__stream_decoder_process_until_end_of_metadata(m_decoder)) { - qWarning() << "SSFLAC: process to end of meta failed:" - << FLAC__stream_decoder_get_state(m_decoder); + qWarning() + << "SSFLAC: process to end of meta failed:" + << FLAC__stream_decoder_get_state(m_decoder); return ERR; } @@ -129,79 +138,77 @@ Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame(diff_type frameIn } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, sample_type* sampleBuffer) { return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { + size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer, - size_type sampleBufferSize, - bool readStereoSamples) { + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize, + bool readStereoSamples) { sample_type* outBuffer = sampleBuffer; const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; while (numberOfFramesTotal > numberOfFramesRead) { DEBUG_ASSERT( - m_decodeSampleBufferReadOffset - <= m_decodeSampleBufferWriteOffset); + m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); // if our buffer from libflac is empty (either because we explicitly cleared // it or because we've simply used all the samples), ask for a new buffer if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { if (FLAC__stream_decoder_process_single(m_decoder)) { - if (m_decodeSampleBufferReadOffset - >= m_decodeSampleBufferWriteOffset) { + if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { // EOF break; } } else { - qWarning() << "SSFLAC: decoder_process_single returned false (" - << m_file.fileName() << ")"; + qWarning() + << "SSFLAC: decoder_process_single returned false (" + << m_file.fileName() << ")"; break; } } DEBUG_ASSERT( - m_decodeSampleBufferReadOffset - <= m_decodeSampleBufferWriteOffset); + m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); const size_type decodeBufferSamples = m_decodeSampleBufferWriteOffset - m_decodeSampleBufferReadOffset; const size_type decodeBufferFrames = samples2frames( decodeBufferSamples); - const size_type framesToCopy = math_min(decodeBufferFrames, numberOfFramesTotal - numberOfFramesRead); + const size_type framesToCopy = + math_min(decodeBufferFrames, numberOfFramesTotal - numberOfFramesRead); const size_type samplesToCopy = frames2samples(framesToCopy); if (readStereoSamples && !isChannelCountStereo()) { if (isChannelCountMono()) { SampleUtil::copyMonoToDualMono(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy); + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy); } else { SampleUtil::copyMultiToStereo(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy, getChannelCount()); + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy, getChannelCount()); } outBuffer += framesToCopy * 2; // copied 2 samples per frame } else { SampleUtil::copy(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - samplesToCopy); + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + samplesToCopy); outBuffer += samplesToCopy; } m_decodeSampleBufferReadOffset += samplesToCopy; numberOfFramesRead += framesToCopy; DEBUG_ASSERT( - m_decodeSampleBufferReadOffset - <= m_decodeSampleBufferWriteOffset); + m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); } return numberOfFramesRead; } // flac callback methods -FLAC__StreamDecoderReadStatus AudioSourceFLAC::flacRead(FLAC__byte buffer[], - size_t *bytes) { +FLAC__StreamDecoderReadStatus AudioSourceFLAC::flacRead( + FLAC__byte buffer[], size_t *bytes) { *bytes = m_file.read((char*) buffer, *bytes); if (*bytes > 0) { return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; @@ -231,7 +238,7 @@ FLAC__StreamDecoderTellStatus AudioSourceFLAC::flacTell(FLAC__uint64 *offset) { } FLAC__StreamDecoderLengthStatus AudioSourceFLAC::flacLength( - FLAC__uint64 *length) { + FLAC__uint64 *length) { if (m_file.isSequential()) { return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; } @@ -247,29 +254,35 @@ FLAC__bool AudioSourceFLAC::flacEOF() { } FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( - const FLAC__Frame *frame, const FLAC__int32 * const buffer[]) { + const FLAC__Frame *frame, const FLAC__int32 * const buffer[]) { // decode buffer must be empty before decoding the next frame DEBUG_ASSERT(m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset); // reset decode buffer m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; if (getChannelCount() != frame->header.channels) { - qWarning() << "Corrupt or unsupported FLAC file:" - << "Invalid number of channels in FLAC frame header" - << frame->header.channels << "<>" << getChannelCount(); + qWarning() + << "Corrupt or unsupported FLAC file:" + << "Invalid number of channels in FLAC frame header" + << frame->header.channels << "<>" + << getChannelCount(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } if (getFrameRate() != frame->header.sample_rate) { - qWarning() << "Corrupt or unsupported FLAC file:" - << "Invalid sample rate in FLAC frame header" - << frame->header.sample_rate << "<>" << getFrameRate(); + qWarning() + << "Corrupt or unsupported FLAC file:" + << "Invalid sample rate in FLAC frame header" + << frame->header.sample_rate << "<>" + << getFrameRate(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } const SampleBuffer::size_type maxBlocksize = samples2frames(m_decodeSampleBuffer.size()); if (maxBlocksize < frame->header.blocksize) { - qWarning() << "Corrupt or unsupported FLAC file:" - << "Block size in FLAC frame header exceeds the maximum block size" - << frame->header.blocksize << ">" << maxBlocksize; + qWarning() + << "Corrupt or unsupported FLAC file:" + << "Block size in FLAC frame header exceeds the maximum block size" + << frame->header.blocksize << ">" + << maxBlocksize; } switch (getChannelCount()) { case 1: { @@ -277,7 +290,7 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( DEBUG_ASSERT(1 <= frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[0][i] * m_sampleScale; + buffer[0][i] * m_sampleScale; } break; } @@ -286,9 +299,9 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( DEBUG_ASSERT(2 <= frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[0][i] * m_sampleScale; + buffer[0][i] * m_sampleScale; m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[1][i] * m_sampleScale; + buffer[1][i] * m_sampleScale; } break; } @@ -298,7 +311,7 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( for (unsigned i = 0; i < frame->header.blocksize; ++i) { for (unsigned j = 0; j < frame->header.channels; ++j) { m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[j][i] * m_sampleScale; + buffer[j][i] * m_sampleScale; } } } @@ -313,21 +326,27 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata *metadata) { setChannelCount(metadata->data.stream_info.channels); setFrameRate(metadata->data.stream_info.sample_rate); setFrameCount(metadata->data.stream_info.total_samples); - m_sampleScale = kSampleValuePeak - / sample_type( - FLAC__int32(1) - << metadata->data.stream_info.bits_per_sample); - qDebug() << "FLAC file " << m_file.fileName(); - qDebug() << getChannelCount() << " @ " << getFrameRate() << " Hz, " - << getFrameCount() << " total, " << "bit depth" - << " metadata->data.stream_info.bits_per_sample"; + m_sampleScale = kSampleValuePeak / sample_type( + FLAC__int32(1) << metadata->data.stream_info.bits_per_sample); + qDebug() + << "FLAC file " + << m_file.fileName(); + qDebug() + << getChannelCount() << " ch," + << getFrameRate() << " Hz," + << getFrameCount() << " total," + << "bit depth" + << metadata->data.stream_info.bits_per_sample; m_minBlocksize = metadata->data.stream_info.min_blocksize; m_maxBlocksize = metadata->data.stream_info.max_blocksize; m_minFramesize = metadata->data.stream_info.min_framesize; m_maxFramesize = metadata->data.stream_info.max_framesize; - qDebug() << "Blocksize in [" << m_minBlocksize << ", " << m_maxBlocksize - << "], Framesize in [" << m_minFramesize << ", " - << m_maxFramesize << "]"; + qDebug() + << "Blocksize in [" + << m_minBlocksize << "," << m_maxBlocksize + << "], Framesize in [" + << m_minFramesize << "," << m_maxFramesize + << "]"; m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; m_decodeSampleBuffer.resize(m_maxBlocksize * getChannelCount()); @@ -355,8 +374,9 @@ void AudioSourceFLAC::flacError(FLAC__StreamDecoderErrorStatus status) { error = "STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM"; break; } - qWarning() << "SSFLAC got error" << error << "from libFLAC for file" - << m_file.fileName(); + qWarning() + << "SSFLAC got error" << error + << "from libFLAC for file" << m_file.fileName(); // not much else to do here... whatever function that initiated whatever // decoder method resulted in this error will return an error, and the caller // will bail. libFLAC docs say to not close the decoder here -- bkgood diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index 17aa643447e..cc41869f7d1 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -9,28 +9,36 @@ namespace Mixxx namespace { - const AudioSource::size_type kSeekFramePrefetchCount = 2; // required for synchronization + const AudioSource::size_type kSeekFramePrefetchCount = + 2; // required for synchronization - const AudioSource::size_type kMaxSamplesPerMp3Frame = 1152; + const AudioSource::size_type kMaxSamplesPerMp3Frame = + 1152; - const AudioSource::diff_type kMaxSkipFrameSamplesWhenSeeking = 2 * kSeekFramePrefetchCount * kMaxSamplesPerMp3Frame; + const AudioSource::diff_type kMaxSkipFrameSamplesWhenSeeking = + 2 * kSeekFramePrefetchCount * kMaxSamplesPerMp3Frame; const AudioSource::sample_type kMadScale = - AudioSource::kSampleValuePeak / AudioSource::sample_type(MAD_F_ONE); + AudioSource::kSampleValuePeak / AudioSource::sample_type(MAD_F_ONE); - inline AudioSource::sample_type madScale(mad_fixed_t sample) { + inline + AudioSource::sample_type madScale(mad_fixed_t sample) { return sample * kMadScale; } // Optimization: Reserve initial capacity for seek frame list - const AudioSource::size_type kMinutesPerFile = 10; // enough for the majority of files (tunable) - const AudioSource::size_type kSecondsPerMinute = 60; // fixed - const AudioSource::size_type kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 - const AudioSource::size_type kSeekFrameListCapacity = kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; + const AudioSource::size_type kMinutesPerFile = + 10; // enough for the majority of files (tunable) + const AudioSource::size_type kSecondsPerMinute = + 60; // fixed + const AudioSource::size_type kMaxMp3FramesPerSecond = + 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 + const AudioSource::size_type kSeekFrameListCapacity = + kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; bool mad_skip_id3_tag(mad_stream* pStream) { - long tagsize = id3_tag_query(pStream->this_frame, - pStream->bufend - pStream->this_frame); + long tagsize = id3_tag_query( + pStream->this_frame, pStream->bufend - pStream->this_frame); if (0 < tagsize) { mad_stream_skip(pStream, tagsize); return true; @@ -41,12 +49,12 @@ namespace } AudioSourceMp3::AudioSourceMp3(QString fileName) - : m_file(fileName), - m_fileSize(0), - m_pFileData(NULL), - m_avgSeekFrameCount(0), - m_curFrameIndex(0), - m_madSynthCount(0) { + : m_file(fileName), + m_fileSize(0), + m_pFileData(NULL), + m_avgSeekFrameCount(0), + m_curFrameIndex(0), + m_madSynthCount(0) { initDecoding(); m_seekFrameList.reserve(kSeekFrameListCapacity); } @@ -56,7 +64,8 @@ AudioSourceMp3::~AudioSourceMp3() { } AudioSourcePointer AudioSourceMp3::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceMp3(fileName)); + QSharedPointer pAudioSource( + new AudioSourceMp3(fileName)); if (OK == pAudioSource->open()) { // success return pAudioSource; @@ -94,7 +103,9 @@ Result AudioSourceMp3::open() { // Ignore LOSTSYNC due to ID3 tags mad_skip_id3_tag(&m_madStream); } else { - qDebug() << "Recoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); + qDebug() + << "Recoverable MP3 header error:" + << mad_stream_errorstr(&m_madStream); } mad_header_finish(&madHeader); continue; @@ -103,7 +114,9 @@ Result AudioSourceMp3::open() { // EOF break; // done } else { - qWarning() << "Unrecoverable MP3 header error:" << mad_stream_errorstr(&m_madStream); + qWarning() + << "Unrecoverable MP3 header error:" + << mad_stream_errorstr(&m_madStream); mad_header_finish(&madHeader); return ERR; // abort } @@ -118,9 +131,10 @@ Result AudioSourceMp3::open() { } else { // check for consistent number of channels if ((0 < madChannelCount) && getChannelCount() != madChannelCount) { - qWarning() << "Differing number of channels in some headers:" - << m_file.fileName() << getChannelCount() << "<>" - << madChannelCount; + qWarning() + << "Differing number of channels in some headers:" + << m_file.fileName() << getChannelCount() << "<>" + << madChannelCount; } } const size_type madSampleRate = madHeader.samplerate; @@ -163,9 +177,10 @@ Result AudioSourceMp3::open() { } else { // check for consistent frame/sample rate if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { - qWarning() << "Differing sample rate in some headers:" - << m_file.fileName() << getFrameRate() << "<>" - << madSampleRate; + qWarning() + << "Differing sample rate in some headers:" + << m_file.fileName() + << getFrameRate() << "<>" << madSampleRate; return ERR; // abort } } @@ -191,7 +206,8 @@ Result AudioSourceMp3::open() { setBitrate(avgBitrate / 1000); } else { // This is not a working MP3 file. - qWarning() << "SSMP3: This is not a working MP3 file:" << m_file.fileName(); + qWarning() + << "SSMP3: This is not a working MP3 file:" << m_file.fileName(); return ERR; // abort } @@ -218,7 +234,9 @@ void AudioSourceMp3::finishDecoding() { void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { finishDecoding(); initDecoding(); - mad_stream_buffer(&m_madStream, seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); + mad_stream_buffer( + &m_madStream, + seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); m_curFrameIndex = seekFrame.frameIndex; } @@ -242,7 +260,8 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff return 0; } // Guess position of frame in m_seekFrameList based on average frame size - AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; + AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = + frameIndex / m_avgSeekFrameCount; if (seekFrameIndex >= m_seekFrameList.size()) { seekFrameIndex = m_seekFrameList.size() - 1; } @@ -261,7 +280,8 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff } DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); - DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); + DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || + (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); return seekFrameIndex; } @@ -273,7 +293,8 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { return seekSampleFrame(0); } // simply skip frames when jumping no more than kMaxSkipFrameSamplesWhenSeeking frames forward - if ((frameIndex < m_curFrameIndex) || ((frameIndex - m_curFrameIndex) > kMaxSkipFrameSamplesWhenSeeking)) { + if ((frameIndex < m_curFrameIndex) || + ((frameIndex - m_curFrameIndex) > kMaxSkipFrameSamplesWhenSeeking)) { SeekFrameList::size_type seekFrameIndex = findSeekFrameIndex(frameIndex); if (seekFrameIndex <= kSeekFramePrefetchCount) { seekFrameIndex = 0; @@ -291,22 +312,23 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { } AudioSource::size_type AudioSourceMp3::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, sample_type* sampleBuffer) { return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } AudioSource::size_type AudioSourceMp3::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { + size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } AudioSource::size_type AudioSourceMp3::readSampleFrames( - size_type numberOfFrames, - sample_type* sampleBuffer, - size_type sampleBufferSize, - bool readStereoSamples) { + size_type numberOfFrames, + sample_type* sampleBuffer, + size_type sampleBufferSize, + bool readStereoSamples) { sample_type* pSampleBuffer = sampleBuffer; - const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); + const size_type numberOfFramesTotal = + math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; while (numberOfFramesTotal > numberOfFramesRead) { if (0 >= m_madSynthCount) { @@ -316,12 +338,16 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( // Ignore LOSTSYNC due to ID3 tags mad_skip_id3_tag(&m_madStream); } else { - qDebug() << "Recoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); + qDebug() + << "Recoverable MP3 decoding error:" + << mad_stream_errorstr(&m_madStream); } continue; } else { if (MAD_ERROR_BUFLEN != m_madStream.error) { - qWarning() << "Unrecoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); + qWarning() + << "Unrecoverable MP3 decoding error:" + << mad_stream_errorstr(&m_madStream); } break; } @@ -332,8 +358,10 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( mad_synth_frame(&m_madSynth, &m_madFrame); m_madSynthCount = m_madSynth.pcm.length; } - const size_type madSynthOffset = m_madSynth.pcm.length - m_madSynthCount; - const size_type framesRead = math_min(m_madSynthCount, numberOfFramesTotal - numberOfFramesRead); + const size_type madSynthOffset = + m_madSynth.pcm.length - m_madSynthCount; + const size_type framesRead = + math_min(m_madSynthCount, numberOfFramesTotal - numberOfFramesRead); m_madSynthCount -= framesRead; m_curFrameIndex += framesRead; numberOfFramesRead += framesRead; @@ -341,7 +369,7 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( if (isChannelCountMono()) { for (size_type i = 0; i < framesRead; ++i) { const sample_type sampleValue = madScale( - m_madSynth.pcm.samples[0][madSynthOffset + i]); + m_madSynth.pcm.samples[0][madSynthOffset + i]); *(pSampleBuffer++) = sampleValue; if (readStereoSamples) { *(pSampleBuffer++) = sampleValue; @@ -350,15 +378,15 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( } else if (isChannelCountStereo() || readStereoSamples) { for (size_type i = 0; i < framesRead; ++i) { *(pSampleBuffer++) = madScale( - m_madSynth.pcm.samples[0][madSynthOffset + i]); + m_madSynth.pcm.samples[0][madSynthOffset + i]); *(pSampleBuffer++) = madScale( - m_madSynth.pcm.samples[1][madSynthOffset + i]); + m_madSynth.pcm.samples[1][madSynthOffset + i]); } } else { for (size_type i = 0; i < framesRead; ++i) { for (size_type j = 0; j < getChannelCount(); ++j) { *(pSampleBuffer++) = madScale( - m_madSynth.pcm.samples[j][madSynthOffset + i]); + m_madSynth.pcm.samples[j][madSynthOffset + i]); } } } diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 82c026162d9..0684586cd93 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -14,7 +14,8 @@ AudioSourceOggVorbis::~AudioSourceOggVorbis() { } AudioSourcePointer AudioSourceOggVorbis::create(QString fileName) { - QSharedPointer pAudioSource(new AudioSourceOggVorbis); + QSharedPointer pAudioSource( + new AudioSourceOggVorbis); if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; @@ -65,7 +66,7 @@ void AudioSourceOggVorbis::close() { } AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( - diff_type frameIndex) { + diff_type frameIndex) { const int seekResult = ov_pcm_seek(&m_vf, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggVorbis file:" << seekResult; @@ -74,21 +75,21 @@ AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( } AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { + size_type numberOfFrames, sample_type* sampleBuffer) { return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); } AudioSource::size_type AudioSourceOggVorbis::readSampleFramesStereo( - size_type numberOfFrames, - sample_type* sampleBuffer, - size_type sampleBufferSize) { + size_type numberOfFrames, + sample_type* sampleBuffer, + size_type sampleBufferSize) { return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); } AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( - size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize, - bool readStereoSamples) { + size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize, + bool readStereoSamples) { sample_type* nextSample = sampleBuffer; const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; @@ -96,7 +97,7 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( float** pcmChannels; int currentSection; const long readResult = ov_read_float(&m_vf, &pcmChannels, - numberOfFramesTotal - numberOfFramesRead, ¤tSection); + numberOfFramesTotal - numberOfFramesRead, ¤tSection); if (0 == readResult) { // EOF break; // done diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index 3b7876da01e..f577f2768fa 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -28,8 +28,9 @@ Result AudioSourceOpus::open(QString fileName) { const QByteArray qbaFilename(fileName.toLocal8Bit()); m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); if (!m_pOggOpusFile) { - qDebug() << "Failed to open OggOpus file:" << fileName - << "errorCode" << errorCode; + qDebug() + << "Failed to open OggOpus file:" << fileName + << "errorCode" << errorCode; return ERR; } @@ -73,8 +74,8 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( size_type readCount = 0; while (readCount < numberOfFrames) { int readResult = op_read_float(m_pOggOpusFile, - sampleBuffer + frames2samples(readCount), - frames2samples(numberOfFrames - readCount), NULL); + sampleBuffer + frames2samples(readCount), + frames2samples(numberOfFrames - readCount), NULL); if (0 == readResult) { // EOF break; // done @@ -82,8 +83,9 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( if (0 < readResult) { readCount += readResult; } else { - qWarning() << "Failed to read sample data from OggOpus file:" - << readResult; + qWarning() + << "Failed to read sample data from OggOpus file:" + << readResult; break; // abort } } @@ -91,13 +93,14 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( } AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { - const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); + size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { + const size_type numberOfFramesTotal = + math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; while (numberOfFramesTotal > numberOfFramesRead) { int readResult = op_read_float_stereo(m_pOggOpusFile, - sampleBuffer + (numberOfFramesRead * 2), - (numberOfFramesTotal - numberOfFramesRead) * 2); + sampleBuffer + (numberOfFramesRead * 2), + (numberOfFramesTotal - numberOfFramesRead) * 2); if (0 == readResult) { // EOF break; // done @@ -105,8 +108,9 @@ AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( if (0 < readResult) { numberOfFramesRead += readResult; } else { - qWarning() << "Failed to read sample data from OggOpus file:" - << readResult; + qWarning() + << "Failed to read sample data from OggOpus file:" + << readResult; break; // abort } } diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index ba6a3f5b9da..ef339352cfe 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -3,7 +3,6 @@ namespace Mixxx { - AudioSourceSndFile::AudioSourceSndFile() : m_pSndFile(NULL) { memset(&m_sfInfo, 0, sizeof(m_sfInfo)); @@ -34,14 +33,16 @@ Result AudioSourceSndFile::open(QString fileName) { #endif if (m_pSndFile == NULL) { // sf_format_check is only for writes - qWarning() << "Error opening libsndfile file:" << fileName - << sf_strerror(m_pSndFile); + qWarning() + << "Error opening libsndfile file:" << fileName + << sf_strerror(m_pSndFile); return ERR; } if (sf_error(m_pSndFile) > 0) { - qWarning() << "Error opening libsndfile file:" << fileName - << sf_strerror(m_pSndFile); + qWarning() + << "Error opening libsndfile file:" << fileName + << sf_strerror(m_pSndFile); return ERR; } @@ -56,8 +57,9 @@ void AudioSourceSndFile::close() { if (m_pSndFile) { const int closeResult = sf_close(m_pSndFile); if (0 != closeResult) { - qWarning() << "Failed to close libsnd file:" << closeResult - << sf_strerror(m_pSndFile); + qWarning() + << "Failed to close libsnd file:" << closeResult + << sf_strerror(m_pSndFile); } m_pSndFile = NULL; memset(&m_sfInfo, 0, sizeof(m_sfInfo)); @@ -66,25 +68,28 @@ void AudioSourceSndFile::close() { } AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( - diff_type frameIndex) { + diff_type frameIndex) { const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); if (0 <= seekResult) { return seekResult; } else { - qWarning() << "Failed to seek libsnd file:" << seekResult - << sf_strerror(m_pSndFile); + qWarning() + << "Failed to seek libsnd file:" << seekResult + << sf_strerror(m_pSndFile); return sf_seek(m_pSndFile, 0, SEEK_CUR); } } AudioSource::size_type AudioSourceSndFile::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { - const sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); + size_type numberOfFrames, sample_type* sampleBuffer) { + const sf_count_t readCount = + sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); if (0 <= readCount) { return readCount; } else { - qWarning() << "Failed to read from libsnd file:" - << readCount << sf_strerror(m_pSndFile); + qWarning() + << "Failed to read from libsnd file:" + << readCount << sf_strerror(m_pSndFile); return 0; } } diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 4efffe8423a..0b57a4226d5 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -29,7 +29,7 @@ QList SoundSourceFLAC::supportedFileExtensions() { } SoundSourceFLAC::SoundSourceFLAC(QString fileName) - : SoundSource(fileName, "flac") { + : SoundSource(fileName, "flac") { } Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index d0341360397..67522916b68 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -28,7 +28,7 @@ QList SoundSourceMp3::supportedFileExtensions() { } SoundSourceMp3::SoundSourceMp3(QString qFilename) - : SoundSource(qFilename, "mp3") { + : SoundSource(qFilename, "mp3") { } Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index 0853221c6d2..ac77a355fd8 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -28,7 +28,7 @@ QList SoundSourceOggVorbis::supportedFileExtensions() { } SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) - : SoundSource(qFilename, "ogg") { + : SoundSource(qFilename, "ogg") { } /* diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 0af4641c91d..2a30aec31e6 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -15,7 +15,7 @@ QList SoundSourceOpus::supportedFileExtensions() { } SoundSourceOpus::SoundSourceOpus(QString qFilename) - : SoundSource(qFilename, "opus") { + : SoundSource(qFilename, "opus") { } namespace diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 88db535a2fe..283704f950b 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -18,7 +18,7 @@ QList SoundSourceSndFile::supportedFileExtensions() { } SoundSourceSndFile::SoundSourceSndFile(QString qFilename) - : SoundSource(qFilename) { + : SoundSource(qFilename) { } Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { @@ -101,7 +101,7 @@ QImage SoundSourceSndFile::parseCoverArt() const { std::list::iterator it = covers.begin(); TagLib::FLAC::Picture* cover = *it; coverArt = QImage::fromData( - QByteArray(cover->data().data(), cover->data().size())); + QByteArray(cover->data().data(), cover->data().size())); } } } else if (getType() == "wav") { From 2f6b02de8b3e67522b758480be28796c7c596e60 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 12 Jan 2015 23:18:56 +0100 Subject: [PATCH 073/481] Add preliminary version of seek tests from daschuer --- src/test/soundproxy_test.cpp | 144 +++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index b4728db3cfd..cf65a2197a2 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -57,3 +57,147 @@ TEST_F(SoundSourceProxyTest, TOAL_TPE2) { EXPECT_EQ("ARTIST", trackMetadata.getAlbum()); EXPECT_EQ("TITLE", trackMetadata.getAlbumArtist()); } + +TEST_F(SoundSourceProxyTest, seekTheSame) { + const unsigned int kSeekFrameIndex = 10000; + const unsigned int kTestFrameCount = 10; + + const QString kFilePath( + QDir::currentPath() + "/src/test/id3-test-data/cover-test."); + + QStringList extensions; + extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; + + foreach (const QString& extension, extensions) { + QString filePath = kFilePath + extension; + + Mixxx::AudioSourcePointer pAudioSource1( + openAudioSource(filePath)); + EXPECT_FALSE(pAudioSource1.isNull()); + const unsigned int sampleCount1 = pAudioSource1->frames2samples(kTestFrameCount); + CSAMPLE *pData1 = new CSAMPLE[sampleCount1]; + pAudioSource1->seekSampleFrame(kSeekFrameIndex); + unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); + EXPECT_EQ(readCount1, kTestFrameCount); + + Mixxx::AudioSourcePointer pAudioSource2( + openAudioSource(filePath)); + EXPECT_FALSE(pAudioSource2.isNull()); + const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); + CSAMPLE *pData2 = new CSAMPLE[sampleCount2]; + pAudioSource2->seekSampleFrame(kSeekFrameIndex); + unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); + EXPECT_EQ(readCount2, kTestFrameCount); + + EXPECT_EQ(readCount1, readCount2); + for (unsigned int i = 0; i < readCount1; i++) { + if (pData1[i] != pData2[i]) { + qDebug() << filePath << "Test Sample" << i; + } + EXPECT_EQ(pData1[i], pData2[i]); + //qDebug() << pData1[i]; + } + + delete[] pData1; + delete[] pData2; + } +} + + +TEST_F(SoundSourceProxyTest, readBeforeSeek) { + const unsigned int kSeekFrameIndex = 10000; + const unsigned int kTestFrameCount = 10; + + const QString kFilePath( + QDir::currentPath() + "/src/test/id3-test-data/cover-test."); + + QStringList extensions; + extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; + + foreach (const QString& extension, extensions) { + QString filePath = kFilePath + extension; + + Mixxx::AudioSourcePointer pAudioSource1( + openAudioSource(filePath)); + EXPECT_FALSE(pAudioSource1.isNull()); + const unsigned int sampleCount1 = pAudioSource1->frames2samples(kTestFrameCount); + CSAMPLE *pData1 = new CSAMPLE[sampleCount1]; + unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); + EXPECT_EQ(readCount1, kTestFrameCount); + pAudioSource1->seekSampleFrame(kSeekFrameIndex); + readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); + EXPECT_EQ(readCount1, kTestFrameCount); + + Mixxx::AudioSourcePointer pAudioSource2( + openAudioSource(filePath)); + EXPECT_FALSE(pAudioSource2.isNull()); + const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); + CSAMPLE *pData2 = new CSAMPLE[sampleCount2]; + pAudioSource2->seekSampleFrame(kSeekFrameIndex); + unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); + EXPECT_EQ(readCount2, kTestFrameCount); + + EXPECT_EQ(readCount1, readCount2); + for (unsigned int i = 0; i < readCount1; i++) { + if (pData1[i] != pData2[i]) { + qDebug() << filePath << "Test Sample" << i; + } + EXPECT_EQ(pData1[i], pData2[i]); + //qDebug() << pData1[i]; + } + + delete[] pData1; + delete[] pData2; + } +} + +TEST_F(SoundSourceProxyTest, seekForward) { + const unsigned int kSeekFrameIndex = 10000; + const unsigned int kTestFrameCount = 10; + + const QString kFilePath( + QDir::currentPath() + "/src/test/id3-test-data/cover-test."); + + QStringList extensions; + extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; + + foreach (const QString& extension, extensions) { + QString filePath = kFilePath + extension; + + Mixxx::AudioSourcePointer pAudioSource1( + openAudioSource(filePath)); + EXPECT_FALSE(pAudioSource1.isNull()); + const unsigned int sampleCount1 = pAudioSource1->frames2samples(kTestFrameCount); + CSAMPLE *pData1 = new CSAMPLE[sampleCount1]; + unsigned int readCount1; + for (unsigned int i = 0; i <= kSeekFrameIndex / kTestFrameCount; ++i) { + readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); + EXPECT_EQ(readCount1, kTestFrameCount); + } + + Mixxx::AudioSourcePointer pAudioSource2( + openAudioSource(filePath)); + EXPECT_FALSE(pAudioSource2.isNull()); + const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); + CSAMPLE *pData2 = new CSAMPLE[sampleCount2]; + pAudioSource2->seekSampleFrame(kSeekFrameIndex); + unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); + EXPECT_EQ(readCount2, kTestFrameCount); + + EXPECT_EQ(readCount1, readCount2); + for (unsigned int i = 0; i < readCount1; i++) { + if (pData1[i] != pData2[i]) { + qDebug() << filePath << "Test Sample" << i; + } + EXPECT_EQ(pData1[i], pData2[i]); + //qDebug() << pData1[i]; + } + + delete[] pData1; + delete[] pData2; + } + +} + + + From 99b19b763882280265f4381c4ff97dc6f050cc7c Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Mon, 12 Jan 2015 23:20:35 +0100 Subject: [PATCH 074/481] Fix MP3 seek test --- src/sources/audiosourcemp3.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index cc41869f7d1..ea16dda1b9c 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -10,7 +10,7 @@ namespace Mixxx namespace { const AudioSource::size_type kSeekFramePrefetchCount = - 2; // required for synchronization + 5; // required for synchronization const AudioSource::size_type kMaxSamplesPerMp3Frame = 1152; From be5ee5551dc41a23c3c453a39d56dd724fd51a15 Mon Sep 17 00:00:00 2001 From: Jean Claveau Date: Tue, 13 Jan 2015 18:20:11 +0100 Subject: [PATCH 075/481] missing header for windows --- src/effects/native/paneffect.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/effects/native/paneffect.cpp b/src/effects/native/paneffect.cpp index 5a8e1f3b33e..d29788634da 100644 --- a/src/effects/native/paneffect.cpp +++ b/src/effects/native/paneffect.cpp @@ -1,3 +1,4 @@ +#include "util/math.h" #include #include "effects/native/paneffect.h" From bd701d57bfb4d04af8efba4744b6ea545dda2134 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 13 Jan 2015 19:17:56 +0100 Subject: [PATCH 076/481] Add some assertions for debugging purposes --- plugins/soundsourcem4a/audiosourcem4a.cpp | 2 ++ .../audiosourcemediafoundation.cpp | 1 + plugins/soundsourcewv/audiosourcewv.cpp | 1 + src/sources/audiosource.h | 14 ++++++++++++-- src/sources/audiosourcecoreaudio.cpp | 1 + src/sources/audiosourceffmpeg.cpp | 2 ++ src/sources/audiosourceflac.cpp | 1 + src/sources/audiosourceoggvorbis.cpp | 7 ++++--- src/sources/audiosourceoggvorbis.h | 4 ++++ src/sources/audiosourceopus.cpp | 7 ++++++- src/sources/audiosourceopus.h | 5 +++++ src/sources/audiosourcesndfile.cpp | 1 + 12 files changed, 40 insertions(+), 6 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index 118eda16cdd..bee45541817 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -209,6 +209,7 @@ bool AudioSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { } AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (m_curFrameIndex != frameIndex) { const MP4SampleId sampleBlockId = kMinSampleBlockId + (frameIndex / kFramesPerSampleBlock); @@ -246,6 +247,7 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { AudioSource::size_type AudioSourceM4A::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); if (!isValidSampleBlockId(m_curSampleBlockId)) { qWarning() << "Invalid MP4 sample block" << m_curSampleBlockId; return 0; diff --git a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp index d3b0dd31d5b..3b743d36aa2 100644 --- a/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp +++ b/plugins/soundsourcemediafoundation/audiosourcemediafoundation.cpp @@ -168,6 +168,7 @@ void AudioSourceMediaFoundation::close() { Mixxx::AudioSource::diff_type AudioSourceMediaFoundation::seekSampleFrame( diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (sDebug) { qDebug() << "seekSampleFrame()" << frameIndex; } diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 99c4699f21b..29b5cba112a 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -56,6 +56,7 @@ void AudioSourceWV::close() { } AudioSource::diff_type AudioSourceWV::seekSampleFrame(diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (WavpackSeekSample(m_wpc, frameIndex) == TRUE) { return frameIndex; } else { diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 3c88dd2f0ae..933214eb872 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -129,9 +129,19 @@ class AudioSource { return sampleCount / getChannelCount(); } + // The (sample) frame index only valid in the range + // [0, getFrameCount()]. The value frameIndex = getFrameCount() + // points behind of the audio data stream, but is a valid + // parameter for seeking. + inline bool isValidFrameIndex(diff_type frameIndex) const { + return (0 <= frameIndex) && + (getFrameCount() >= size_type(frameIndex)); + } + // Adjusts the current frame seek index: - // - Index of first frame: frameIndex = 0 - // - Index of last frame: frameIndex = totalFrames() - 1 + // - Precondition: isValidFrameIndex(frameIndex) == true + // - Index of first frame: frameIndex = 0 + // - Index of last frame: frameIndex = getFrameCount() - 1 // - The seek position in seconds is frameIndex / frameRate() // Returns the actual current frame index which may differ from the // requested index if the source does not support accurate seeking. diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index a7d790194b0..6f31725bee8 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -121,6 +121,7 @@ void AudioSourceCoreAudio::close() { } AudioSource::diff_type AudioSourceCoreAudio::seekSampleFrame(diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); //_ThrowExceptionIfErr(@"ExtAudioFileSeek", err); //qDebug() << "SSCA: Seeking to" << frameIndex; diff --git a/src/sources/audiosourceffmpeg.cpp b/src/sources/audiosourceffmpeg.cpp index 12aaf64500d..dc77850ede2 100644 --- a/src/sources/audiosourceffmpeg.cpp +++ b/src/sources/audiosourceffmpeg.cpp @@ -404,6 +404,8 @@ bool AudioSourceFFmpeg::getBytesFromCache(char *buffer, quint64 offset, } AudioSource::diff_type AudioSourceFFmpeg::seekSampleFrame(diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + const diff_type filepos = frames2samples(frameIndex); int ret = 0; diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 263c2a6b3c6..000cd0bce43 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -127,6 +127,7 @@ void AudioSourceFLAC::close() { } Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame(diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); // clear decode buffer before seeking m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index 0684586cd93..ec8bcc2fa0d 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -1,7 +1,5 @@ #include "sources/audiosourceoggvorbis.h" -#include - namespace Mixxx { @@ -67,11 +65,12 @@ void AudioSourceOggVorbis::close() { AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); const int seekResult = ov_pcm_seek(&m_vf, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggVorbis file:" << seekResult; } - return ov_pcm_tell(&m_vf); + return getCurrentFrameIndex(); } AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( @@ -90,6 +89,8 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + sample_type* nextSample = sampleBuffer; const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index cc965245589..207aa68a9d1 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -28,6 +28,10 @@ class AudioSourceOggVorbis: public AudioSource { void close(); + inline diff_type getCurrentFrameIndex() { + return ov_pcm_tell(&m_vf); + } + size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index f577f2768fa..fd67dcc1c86 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -62,15 +62,18 @@ void AudioSourceOpus::close() { } AudioSource::diff_type AudioSourceOpus::seekSampleFrame(diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); int seekResult = op_pcm_seek(m_pOggOpusFile, frameIndex); if (0 != seekResult) { qWarning() << "Failed to seek OggOpus file:" << seekResult; } - return op_pcm_tell(m_pOggOpusFile); + return getCurrentFrameIndex(); } AudioSource::size_type AudioSourceOpus::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + size_type readCount = 0; while (readCount < numberOfFrames) { int readResult = op_read_float(m_pOggOpusFile, @@ -94,6 +97,8 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { + DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); + const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 222f71ed396..5d6432d5b9d 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -31,6 +31,11 @@ class AudioSourceOpus: public AudioSource { void close(); + inline diff_type getCurrentFrameIndex() const { + DEBUG_ASSERT(NULL != m_pOggOpusFile); + return op_pcm_tell(m_pOggOpusFile); + } + OggOpusFile *m_pOggOpusFile; }; diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index ef339352cfe..efb1850f00b 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -69,6 +69,7 @@ void AudioSourceSndFile::close() { AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( diff_type frameIndex) { + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); if (0 <= seekResult) { return seekResult; From 5259015b400ce7cda7160f95d42383d0d8614bf6 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 13 Jan 2015 19:20:16 +0100 Subject: [PATCH 077/481] Extend the coverage of the audio decoding tests --- src/test/soundproxy_test.cpp | 78 +++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index cf65a2197a2..568a6546c35 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -153,7 +153,9 @@ TEST_F(SoundSourceProxyTest, readBeforeSeek) { TEST_F(SoundSourceProxyTest, seekForward) { const unsigned int kSeekFrameIndex = 10000; - const unsigned int kTestFrameCount = 10; + const unsigned int kTestFrameCount = 100; + + EXPECT_EQ(0, int(kSeekFrameIndex % kTestFrameCount)); const QString kFilePath( QDir::currentPath() + "/src/test/id3-test-data/cover-test."); @@ -161,40 +163,52 @@ TEST_F(SoundSourceProxyTest, seekForward) { QStringList extensions; extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; - foreach (const QString& extension, extensions) { - QString filePath = kFilePath + extension; + for (unsigned int seekFrameIndex = 0; ; seekFrameIndex += kSeekFrameIndex) { + qDebug() << "seekFrameIndex =" << seekFrameIndex; + foreach (const QString& extension, extensions) { + QString filePath = kFilePath + extension; - Mixxx::AudioSourcePointer pAudioSource1( - openAudioSource(filePath)); - EXPECT_FALSE(pAudioSource1.isNull()); - const unsigned int sampleCount1 = pAudioSource1->frames2samples(kTestFrameCount); - CSAMPLE *pData1 = new CSAMPLE[sampleCount1]; - unsigned int readCount1; - for (unsigned int i = 0; i <= kSeekFrameIndex / kTestFrameCount; ++i) { - readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); - EXPECT_EQ(readCount1, kTestFrameCount); - } - - Mixxx::AudioSourcePointer pAudioSource2( - openAudioSource(filePath)); - EXPECT_FALSE(pAudioSource2.isNull()); - const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); - CSAMPLE *pData2 = new CSAMPLE[sampleCount2]; - pAudioSource2->seekSampleFrame(kSeekFrameIndex); - unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); - EXPECT_EQ(readCount2, kTestFrameCount); - - EXPECT_EQ(readCount1, readCount2); - for (unsigned int i = 0; i < readCount1; i++) { - if (pData1[i] != pData2[i]) { - qDebug() << filePath << "Test Sample" << i; + Mixxx::AudioSourcePointer pAudioSource1( + openAudioSource(filePath)); + EXPECT_FALSE(pAudioSource1.isNull()); + if ((seekFrameIndex + kTestFrameCount) > pAudioSource1->getFrameCount()) { + break; // finished + } + const unsigned int sampleCount1 = pAudioSource1->frames2samples(kTestFrameCount); + CSAMPLE *pData1 = new CSAMPLE[sampleCount1]; + unsigned int frameIndex1 = 0; + while (frameIndex1 < seekFrameIndex) { + unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); + EXPECT_EQ(kTestFrameCount, readCount1); + frameIndex1 += readCount1; + } + EXPECT_EQ(seekFrameIndex, frameIndex1); + unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); + + Mixxx::AudioSourcePointer pAudioSource2( + openAudioSource(filePath)); + EXPECT_FALSE(pAudioSource2.isNull()); + if ((seekFrameIndex + kTestFrameCount) > pAudioSource2->getFrameCount()) { + break; // finished + } + const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); + CSAMPLE *pData2 = new CSAMPLE[sampleCount2]; + pAudioSource2->seekSampleFrame(seekFrameIndex); + unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); + EXPECT_EQ(kTestFrameCount, readCount2); + + EXPECT_EQ(readCount1, readCount2); + for (unsigned int i = 0; i < readCount1; i++) { + if (pData1[i] != pData2[i]) { + qDebug() << filePath << "Test Sample" << i; + } + EXPECT_EQ(pData1[i], pData2[i]); + //qDebug() << pData1[i]; } - EXPECT_EQ(pData1[i], pData2[i]); - //qDebug() << pData1[i]; - } - delete[] pData1; - delete[] pData2; + delete[] pData1; + delete[] pData2; + } } } From 29db3151342acf71aac737ac4c8056bda50cebb2 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 13 Jan 2015 19:20:30 +0100 Subject: [PATCH 078/481] Delete obsolete audio decoding tests --- src/test/soundproxy_test.cpp | 93 ------------------------------------ 1 file changed, 93 deletions(-) diff --git a/src/test/soundproxy_test.cpp b/src/test/soundproxy_test.cpp index 568a6546c35..75b8b941853 100644 --- a/src/test/soundproxy_test.cpp +++ b/src/test/soundproxy_test.cpp @@ -58,99 +58,6 @@ TEST_F(SoundSourceProxyTest, TOAL_TPE2) { EXPECT_EQ("TITLE", trackMetadata.getAlbumArtist()); } -TEST_F(SoundSourceProxyTest, seekTheSame) { - const unsigned int kSeekFrameIndex = 10000; - const unsigned int kTestFrameCount = 10; - - const QString kFilePath( - QDir::currentPath() + "/src/test/id3-test-data/cover-test."); - - QStringList extensions; - extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; - - foreach (const QString& extension, extensions) { - QString filePath = kFilePath + extension; - - Mixxx::AudioSourcePointer pAudioSource1( - openAudioSource(filePath)); - EXPECT_FALSE(pAudioSource1.isNull()); - const unsigned int sampleCount1 = pAudioSource1->frames2samples(kTestFrameCount); - CSAMPLE *pData1 = new CSAMPLE[sampleCount1]; - pAudioSource1->seekSampleFrame(kSeekFrameIndex); - unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); - EXPECT_EQ(readCount1, kTestFrameCount); - - Mixxx::AudioSourcePointer pAudioSource2( - openAudioSource(filePath)); - EXPECT_FALSE(pAudioSource2.isNull()); - const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); - CSAMPLE *pData2 = new CSAMPLE[sampleCount2]; - pAudioSource2->seekSampleFrame(kSeekFrameIndex); - unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); - EXPECT_EQ(readCount2, kTestFrameCount); - - EXPECT_EQ(readCount1, readCount2); - for (unsigned int i = 0; i < readCount1; i++) { - if (pData1[i] != pData2[i]) { - qDebug() << filePath << "Test Sample" << i; - } - EXPECT_EQ(pData1[i], pData2[i]); - //qDebug() << pData1[i]; - } - - delete[] pData1; - delete[] pData2; - } -} - - -TEST_F(SoundSourceProxyTest, readBeforeSeek) { - const unsigned int kSeekFrameIndex = 10000; - const unsigned int kTestFrameCount = 10; - - const QString kFilePath( - QDir::currentPath() + "/src/test/id3-test-data/cover-test."); - - QStringList extensions; - extensions << "aiff" << "flac" << "mp3" << "ogg" << "wav"; - - foreach (const QString& extension, extensions) { - QString filePath = kFilePath + extension; - - Mixxx::AudioSourcePointer pAudioSource1( - openAudioSource(filePath)); - EXPECT_FALSE(pAudioSource1.isNull()); - const unsigned int sampleCount1 = pAudioSource1->frames2samples(kTestFrameCount); - CSAMPLE *pData1 = new CSAMPLE[sampleCount1]; - unsigned int readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); - EXPECT_EQ(readCount1, kTestFrameCount); - pAudioSource1->seekSampleFrame(kSeekFrameIndex); - readCount1 = pAudioSource1->readSampleFrames(kTestFrameCount, pData1); - EXPECT_EQ(readCount1, kTestFrameCount); - - Mixxx::AudioSourcePointer pAudioSource2( - openAudioSource(filePath)); - EXPECT_FALSE(pAudioSource2.isNull()); - const unsigned int sampleCount2 = pAudioSource2->frames2samples(kTestFrameCount); - CSAMPLE *pData2 = new CSAMPLE[sampleCount2]; - pAudioSource2->seekSampleFrame(kSeekFrameIndex); - unsigned int readCount2 = pAudioSource2->readSampleFrames(kTestFrameCount, pData2); - EXPECT_EQ(readCount2, kTestFrameCount); - - EXPECT_EQ(readCount1, readCount2); - for (unsigned int i = 0; i < readCount1; i++) { - if (pData1[i] != pData2[i]) { - qDebug() << filePath << "Test Sample" << i; - } - EXPECT_EQ(pData1[i], pData2[i]); - //qDebug() << pData1[i]; - } - - delete[] pData1; - delete[] pData2; - } -} - TEST_F(SoundSourceProxyTest, seekForward) { const unsigned int kSeekFrameIndex = 10000; const unsigned int kTestFrameCount = 100; From 7cef4ff7040536cfdacb48f73e297d64364d2aa8 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 13 Jan 2015 19:23:53 +0100 Subject: [PATCH 079/481] Fix prefetch-after-seek for MP3 (tests still fail) --- src/sources/audiosourcemp3.cpp | 152 +++++++++++++++++++++------------ 1 file changed, 97 insertions(+), 55 deletions(-) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index ea16dda1b9c..a8034b0b642 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -7,16 +7,12 @@ namespace Mixxx { -namespace -{ - const AudioSource::size_type kSeekFramePrefetchCount = - 5; // required for synchronization - - const AudioSource::size_type kMaxSamplesPerMp3Frame = - 1152; +namespace { - const AudioSource::diff_type kMaxSkipFrameSamplesWhenSeeking = - 2 * kSeekFramePrefetchCount * kMaxSamplesPerMp3Frame; + // In the worst case up to 29 MP3 frames need to be prefetched: + // http://www.mars.org/mailman/public/mad-dev/2002-May/000634.html + const AudioSource::size_type kSeekFramePrefetchCount = + 4; // required for synchronization const AudioSource::sample_type kMadScale = AudioSource::kSampleValuePeak / AudioSource::sample_type(MAD_F_ONE); @@ -46,7 +42,8 @@ namespace return false; } } -} + +} // anonymous namespace AudioSourceMp3::AudioSourceMp3(QString fileName) : m_file(fileName), @@ -217,27 +214,32 @@ Result AudioSourceMp3::open() { } void AudioSourceMp3::initDecoding() { + m_madSynthCount = 0; mad_stream_init(&m_madStream); mad_stream_options(&m_madStream, MAD_OPTION_IGNORECRC); mad_frame_init(&m_madFrame); mad_synth_init(&m_madSynth); + m_curFrameIndex = 0; } void AudioSourceMp3::finishDecoding() { + m_madSynthCount = 0; mad_synth_finish(&m_madSynth); mad_frame_finish(&m_madFrame); mad_stream_finish(&m_madStream); - m_madSynthCount = 0; m_curFrameIndex = getFrameCount(); // invalidate } void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { - finishDecoding(); - initDecoding(); + m_madSynthCount = 0; mad_stream_buffer( &m_madStream, seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); m_curFrameIndex = seekFrame.frameIndex; + // Calling mad_synth_mute() and mad_frame_mute() is not + // necessary, because we will prefetch (decode and skip) + // some frames before actually reading any audio samples + // from the stream. } void AudioSourceMp3::close() { @@ -256,9 +258,8 @@ void AudioSourceMp3::close() { } AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { - if ((0 >= frameIndex) || m_seekFrameList.empty()) { - return 0; - } + DEBUG_ASSERT(!m_seekFrameList.empty()); + DEBUG_ASSERT(0 < m_avgSeekFrameCount); // Guess position of frame in m_seekFrameList based on average frame size AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = frameIndex / m_avgSeekFrameCount; @@ -286,28 +287,35 @@ AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff } AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { - if (m_curFrameIndex == frameIndex) { - return m_curFrameIndex; - } - if (0 > frameIndex) { - return seekSampleFrame(0); - } - // simply skip frames when jumping no more than kMaxSkipFrameSamplesWhenSeeking frames forward - if ((frameIndex < m_curFrameIndex) || - ((frameIndex - m_curFrameIndex) > kMaxSkipFrameSamplesWhenSeeking)) { - SeekFrameList::size_type seekFrameIndex = findSeekFrameIndex(frameIndex); - if (seekFrameIndex <= kSeekFramePrefetchCount) { - seekFrameIndex = 0; - } else { - seekFrameIndex -= kSeekFramePrefetchCount; + //qDebug() << "seekSampleFrame(): frameIndex =" << frameIndex; + DEBUG_ASSERT(isValidFrameIndex(frameIndex)); + if (!m_seekFrameList.empty()) { + const SeekFrameList::size_type curSeekFrameIndex = findSeekFrameIndex(m_curFrameIndex); + //qDebug() << "curSeekFrameIndex" << curSeekFrameIndex; + DEBUG_ASSERT(m_seekFrameList.size() > curSeekFrameIndex); + SeekFrameList::size_type nextSeekFrameIndex = findSeekFrameIndex(frameIndex); + DEBUG_ASSERT(m_seekFrameList.size() > nextSeekFrameIndex); + //qDebug() << "nextSeekFrameIndex" << nextSeekFrameIndex; + if ((frameIndex < m_curFrameIndex) || // seeking backwards? + (nextSeekFrameIndex > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jumping forward? + // Implementation note: The type size_type is unsigned so we + // need to be careful when subtracting! + if (nextSeekFrameIndex <= kSeekFramePrefetchCount) { + nextSeekFrameIndex = 0; + } else { + nextSeekFrameIndex -= kSeekFramePrefetchCount; + } + //qDebug() << "restarting at nextSeekFrameIndex" << nextSeekFrameIndex; + restartDecoding(m_seekFrameList[nextSeekFrameIndex]); + DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == nextSeekFrameIndex); } - DEBUG_ASSERT(seekFrameIndex < m_seekFrameList.size()); - restartDecoding(m_seekFrameList[seekFrameIndex]); + // decoding starts before the actual target position + DEBUG_ASSERT(m_curFrameIndex <= frameIndex); + // decode and discard prefetch data + const size_type prefetchFrameCount = frameIndex - m_curFrameIndex; + skipFrameSamples(prefetchFrameCount); + DEBUG_ASSERT(m_curFrameIndex == frameIndex); } - // decode and discard prefetch data - DEBUG_ASSERT(m_curFrameIndex <= frameIndex); - skipFrameSamples(frameIndex - m_curFrameIndex); - DEBUG_ASSERT(m_curFrameIndex == frameIndex); return m_curFrameIndex; } @@ -326,46 +334,76 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples) { + /* + qDebug() << "readSampleFrames():" + << "m_curFrameIndex =" << m_curFrameIndex + << "numberOfFrames =" << numberOfFrames; + */ + DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); + sample_type* pSampleBuffer = sampleBuffer; const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; while (numberOfFramesTotal > numberOfFramesRead) { if (0 >= m_madSynthCount) { + // all decoded output data has been consumed + DEBUG_ASSERT(0 == m_madSynthCount); + const unsigned char* const madThisFrame = m_madStream.this_frame; if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { - if (MAD_ERROR_LOSTSYNC == m_madStream.error) { - // Ignore LOSTSYNC due to ID3 tags - mad_skip_id3_tag(&m_madStream); - } else { + if ((NULL != pSampleBuffer) || + (MAD_ERROR_BADDATAPTR != m_madStream.error)) { qDebug() << "Recoverable MP3 decoding error:" << mad_stream_errorstr(&m_madStream); } + if (MAD_ERROR_LOSTSYNC == m_madStream.error) { + // Ignore LOSTSYNC due to ID3 tags + qDebug() << "Skipping ID3 tag data"; + mad_skip_id3_tag(&m_madStream); + } continue; } else { - if (MAD_ERROR_BUFLEN != m_madStream.error) { - qWarning() - << "Unrecoverable MP3 decoding error:" - << mad_stream_errorstr(&m_madStream); - } + DEBUG_ASSERT(MAD_ERROR_BUFLEN != m_madStream.error); + qWarning() + << "Unrecoverable MP3 decoding error:" + << mad_stream_errorstr(&m_madStream); break; } + } else { + // if not at the beginning of the stream + // the next MP3 frame must have been decoded + DEBUG_ASSERT((0 == m_curFrameIndex) || + (madThisFrame != m_madStream.this_frame)); + } + const mad_header* const pMadFrameHeader = &m_madFrame.header; + if (getChannelCount() != MAD_NCHANNELS(pMadFrameHeader)) { + qWarning() + << "Corrupt or unsupported MP3 file:" + << "Invalid number of channels in MP3 frame header" + << MAD_NCHANNELS(pMadFrameHeader) << "<>" + << getChannelCount(); + break; } - /* Once decoded the frame is synthesized to PCM samples. No ERRs - * are reported by mad_synth_frame(); - */ + // Once decoded the frame is synthesized to PCM samples mad_synth_frame(&m_madSynth, &m_madFrame); m_madSynthCount = m_madSynth.pcm.length; + if (getFrameRate() != m_madSynth.pcm.samplerate) { + qWarning() + << "Corrupt or unsupported MP3 file:" + << "Invalid sample rate in MP3 frame header" + << m_madSynth.pcm.samplerate << "<>" + << getFrameRate(); + break; + } } - const size_type madSynthOffset = - m_madSynth.pcm.length - m_madSynthCount; - const size_type framesRead = - math_min(m_madSynthCount, numberOfFramesTotal - numberOfFramesRead); - m_madSynthCount -= framesRead; - m_curFrameIndex += framesRead; - numberOfFramesRead += framesRead; + const size_type framesRead = math_min( + m_madSynthCount, + numberOfFramesTotal - numberOfFramesRead); if (NULL != pSampleBuffer) { + const size_type madSynthOffset = + m_madSynth.pcm.length - m_madSynthCount; if (isChannelCountMono()) { for (size_type i = 0; i < framesRead; ++i) { const sample_type sampleValue = madScale( @@ -391,6 +429,10 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( } } } + // consume decoded output data + m_madSynthCount -= framesRead; + m_curFrameIndex += framesRead; + numberOfFramesRead += framesRead; } return numberOfFramesRead; } From 61e04304ea8daf71cf9cf844cb2e96c735bc7489 Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 13 Jan 2015 19:45:55 +0100 Subject: [PATCH 080/481] Reformat source code (roughly K&R + spaces) --- plugins/soundsourcem4a/audiosourcem4a.cpp | 128 ++++--- plugins/soundsourcem4a/audiosourcem4a.h | 10 +- plugins/soundsourcem4a/soundsourcem4a.cpp | 8 +- plugins/soundsourcem4a/soundsourcem4a.h | 11 +- plugins/soundsourcewv/audiosourcewv.cpp | 17 +- plugins/soundsourcewv/audiosourcewv.h | 3 +- plugins/soundsourcewv/soundsourcewv.cpp | 8 +- src/metadata/audiotagger.cpp | 35 +- src/metadata/audiotagger.h | 5 +- src/metadata/trackmetadata.cpp | 14 +- src/metadata/trackmetadatataglib.cpp | 394 ++++++++++------------ src/metadata/trackmetadatataglib.h | 17 +- src/sources/audiosource.cpp | 36 +- src/sources/audiosource.h | 15 +- src/sources/audiosourcecoreaudio.cpp | 71 ++-- src/sources/audiosourcecoreaudio.h | 2 +- src/sources/audiosourceflac.cpp | 195 +++++------ src/sources/audiosourceflac.h | 18 +- src/sources/audiosourcemodplug.cpp | 24 +- src/sources/audiosourcemodplug.h | 6 +- src/sources/audiosourcemp3.cpp | 231 ++++++------- src/sources/audiosourcemp3.h | 23 +- src/sources/audiosourceoggvorbis.cpp | 30 +- src/sources/audiosourceoggvorbis.h | 9 +- src/sources/audiosourceopus.cpp | 39 +-- src/sources/audiosourceopus.h | 12 +- src/sources/audiosourcesndfile.cpp | 43 ++- src/sources/audiosourcesndfile.h | 6 +- src/sources/soundsource.cpp | 10 +- src/sources/soundsourceflac.cpp | 4 +- src/sources/soundsourceflac.h | 2 +- src/sources/soundsourcemodplug.cpp | 13 +- src/sources/soundsourcemp3.cpp | 28 +- src/sources/soundsourcemp3.h | 16 +- src/sources/soundsourceoggvorbis.cpp | 7 +- src/sources/soundsourceopus.cpp | 42 +-- src/sources/soundsourceplugin.cpp | 6 +- src/sources/soundsourceplugin.h | 13 +- src/sources/soundsourcesndfile.cpp | 9 +- 39 files changed, 752 insertions(+), 808 deletions(-) diff --git a/plugins/soundsourcem4a/audiosourcem4a.cpp b/plugins/soundsourcem4a/audiosourcem4a.cpp index bee45541817..d3d855c8da7 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.cpp +++ b/plugins/soundsourcem4a/audiosourcem4a.cpp @@ -20,8 +20,7 @@ typedef unsigned long SAMPLERATE_TYPE; namespace Mixxx { -namespace -{ +namespace { // AAC: "... each block decodes to 1024 time-domain samples." const AudioSource::size_type kFramesPerSampleBlock = 1024; @@ -52,8 +51,8 @@ MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { continue; } if (MP4_MPEG4_AUDIO_TYPE == audioType) { - const u_int8_t audioMpeg4Type = - MP4GetTrackAudioMpeg4Type(hFile, trackId); + const u_int8_t audioMpeg4Type = MP4GetTrackAudioMpeg4Type(hFile, + trackId); if (MP4_IS_MPEG4_AAC_AUDIO_TYPE(audioMpeg4Type)) { return trackId; } @@ -66,15 +65,15 @@ MP4TrackId findFirstAudioTrackId(MP4FileHandle hFile) { } -AudioSourceM4A::AudioSourceM4A() - : m_hFile(MP4_INVALID_FILE_HANDLE), - m_trackId(MP4_INVALID_TRACK_ID), - m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), - m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), - m_inputBufferOffset(0), - m_inputBufferLength(0), - m_hDecoder(NULL), - m_curFrameIndex(0) { +AudioSourceM4A::AudioSourceM4A() : + m_hFile(MP4_INVALID_FILE_HANDLE), + m_trackId(MP4_INVALID_TRACK_ID), + m_maxSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_curSampleBlockId(MP4_INVALID_SAMPLE_ID), + m_inputBufferOffset(0), + m_inputBufferLength(0), + m_hDecoder(NULL), + m_curFrameIndex(0) { } AudioSourceM4A::~AudioSourceM4A() { @@ -118,8 +117,8 @@ Result AudioSourceM4A::open(QString fileName) { // Determine the maximum input size (in bytes) of a // sample block for the selected track. - const u_int32_t maxSampleBlockInputSize = - MP4GetTrackMaxSampleSize(m_hFile, m_trackId); + const u_int32_t maxSampleBlockInputSize = MP4GetTrackMaxSampleSize(m_hFile, + m_trackId); m_inputBuffer.resize(maxSampleBlockInputSize, 0); // Initially the input buffer is empty m_inputBufferOffset = 0; @@ -143,20 +142,20 @@ Result AudioSourceM4A::open(QString fileName) { u_int8_t* configBuffer = NULL; u_int32_t configBufferSize = 0; - if (!MP4GetTrackESConfiguration( - m_hFile, m_trackId, &configBuffer, &configBufferSize)) { + if (!MP4GetTrackESConfiguration(m_hFile, m_trackId, &configBuffer, + &configBufferSize)) { /* failed to get mpeg-4 audio config... this is ok. * NeAACDecInit2() will simply use default values instead. */ - qWarning() - << "Failed to read the MP4 audio configuration." - << "Continuing with default values."; + qWarning() << "Failed to read the MP4 audio configuration." + << "Continuing with default values."; } SAMPLERATE_TYPE sampleRate; unsigned char channelCount; - if (0 > NeAACDecInit2( - m_hDecoder, configBuffer, configBufferSize, &sampleRate, &channelCount)) { + if (0 + > NeAACDecInit2(m_hDecoder, configBuffer, configBufferSize, + &sampleRate, &channelCount)) { free(configBuffer); qWarning() << "Failed to initialize the AAC decoder!"; return ERR; @@ -171,7 +170,8 @@ Result AudioSourceM4A::open(QString fileName) { // Allocate one block more than the number of sample blocks // that are prefetched const SampleBuffer::size_type prefetchSampleBufferSize = - (kNumberOfPrefetchSampleBlocks + 1) * frames2samples(kFramesPerSampleBlock); + (kNumberOfPrefetchSampleBlocks + 1) + * frames2samples(kFramesPerSampleBlock); m_prefetchSampleBuffer.resize(prefetchSampleBufferSize); m_curSampleBlockId = MP4_INVALID_SAMPLE_ID; @@ -205,31 +205,36 @@ void AudioSourceM4A::close() { } bool AudioSourceM4A::isValidSampleBlockId(MP4SampleId sampleBlockId) const { - return (sampleBlockId >= kMinSampleBlockId) && (sampleBlockId <= m_maxSampleBlockId); + return (sampleBlockId >= kMinSampleBlockId) + && (sampleBlockId <= m_maxSampleBlockId); } AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (m_curFrameIndex != frameIndex) { - const MP4SampleId sampleBlockId = - kMinSampleBlockId + (frameIndex / kFramesPerSampleBlock); + const MP4SampleId sampleBlockId = kMinSampleBlockId + + (frameIndex / kFramesPerSampleBlock); if (!isValidSampleBlockId(sampleBlockId)) { return m_curFrameIndex; } if ((frameIndex < m_curFrameIndex) || // seeking backwards? - !isValidSampleBlockId(m_curSampleBlockId) || // invalid seek position? - (sampleBlockId > (m_curSampleBlockId + kNumberOfPrefetchSampleBlocks))) { // jumping forward? + !isValidSampleBlockId(m_curSampleBlockId) || // invalid seek position? + (sampleBlockId + > (m_curSampleBlockId + kNumberOfPrefetchSampleBlocks))) { // jumping forward? // Restart decoding one or more blocks of samples backwards // from the calculated starting block to avoid audible glitches. // Implementation note: The type MP4SampleId is unsigned so we // need to be careful when subtracting! - if ((kMinSampleBlockId + kNumberOfPrefetchSampleBlocks) < sampleBlockId) { - m_curSampleBlockId = sampleBlockId - kNumberOfPrefetchSampleBlocks; + if ((kMinSampleBlockId + kNumberOfPrefetchSampleBlocks) + < sampleBlockId) { + m_curSampleBlockId = sampleBlockId + - kNumberOfPrefetchSampleBlocks; } else { m_curSampleBlockId = kMinSampleBlockId; } NeAACDecPostSeekReset(m_hDecoder, m_curSampleBlockId); - m_curFrameIndex = (m_curSampleBlockId - kMinSampleBlockId) * kFramesPerSampleBlock; + m_curFrameIndex = (m_curSampleBlockId - kMinSampleBlockId) + * kFramesPerSampleBlock; // discard input buffer m_inputBufferOffset = 0; m_inputBufferLength = 0; @@ -240,8 +245,7 @@ AudioSource::diff_type AudioSourceM4A::seekSampleFrame(diff_type frameIndex) { // prefetch (decode and discard) all samples up to the target position DEBUG_ASSERT(frames2samples(prefetchFrameCount) <= m_prefetchSampleBuffer.size()); readSampleFrames(prefetchFrameCount, &m_prefetchSampleBuffer[0]); - } - DEBUG_ASSERT(m_curFrameIndex == frameIndex); + } DEBUG_ASSERT(m_curFrameIndex == frameIndex); return frameIndex; } @@ -264,72 +268,64 @@ AudioSource::size_type AudioSourceM4A::readSampleFrames( // fill input buffer with next block of samples u_int8_t* pInputBuffer = &m_inputBuffer[0]; u_int32_t inputBufferLength = m_inputBuffer.size(); // in/out parameter - if (!MP4ReadSample( - m_hFile, m_trackId, m_curSampleBlockId, - &pInputBuffer, &inputBufferLength, - NULL, NULL, NULL, NULL)) { + if (!MP4ReadSample(m_hFile, m_trackId, m_curSampleBlockId, + &pInputBuffer, &inputBufferLength, + NULL, NULL, NULL, NULL)) { qWarning() - << "Failed to read MP4 input data for sample block" - << m_curSampleBlockId - << "(" - << "min =" << kMinSampleBlockId - << "," - << "max =" << m_maxSampleBlockId - << ")"; + << "Failed to read MP4 input data for sample block" + << m_curSampleBlockId << "(" << "min =" + << kMinSampleBlockId << "," << "max =" + << m_maxSampleBlockId << ")"; break; // abort } ++m_curSampleBlockId; m_inputBufferLength = inputBufferLength; } - } - DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); + } DEBUG_ASSERT(m_inputBufferOffset <= m_inputBufferLength); if (m_inputBufferOffset >= m_inputBufferLength) { // EOF - break; // done + break;// done } NeAACDecFrameInfo decFrameInfo; decFrameInfo.bytesconsumed = 0; decFrameInfo.samples = 0; // decode samples into sampleBuffer - const size_type decodeBufferCapacityInBytes = - frames2samples(numberOfFramesRemaining) * sizeof(*sampleBuffer); + const size_type decodeBufferCapacityInBytes = frames2samples( + numberOfFramesRemaining) * sizeof(*sampleBuffer); DEBUG_ASSERT(0 < decodeBufferCapacityInBytes); void* pDecodeBuffer = pSampleBuffer; // in/out parameter - void* pDecodeResult = NeAACDecDecode2( - m_hDecoder, &decFrameInfo, - &m_inputBuffer[m_inputBufferOffset], - m_inputBufferLength - m_inputBufferOffset, - &pDecodeBuffer, decodeBufferCapacityInBytes); + void* pDecodeResult = NeAACDecDecode2(m_hDecoder, &decFrameInfo, + &m_inputBuffer[m_inputBufferOffset], + m_inputBufferLength - m_inputBufferOffset, &pDecodeBuffer, + decodeBufferCapacityInBytes); // verify our assumptions about the decoding API DEBUG_ASSERT(pSampleBuffer == pDecodeBuffer); // verify the in/out parameter DEBUG_ASSERT(pSampleBuffer == pDecodeResult); // verify the result pointer // verify the decoding result if (0 != decFrameInfo.error) { - qWarning() - << "AAC decoding error:" - << NeAACDecGetErrorMessage(decFrameInfo.error); + qWarning() << "AAC decoding error:" + << NeAACDecGetErrorMessage(decFrameInfo.error); break; // abort } // verify the decoded sample data for consistency if (getChannelCount() != decFrameInfo.channels) { - qWarning() - << "Corrupt or unsupported AAC file:" - << "Unexpected number of channels" - << decFrameInfo.channels << "<>" << getChannelCount(); + qWarning() << "Corrupt or unsupported AAC file:" + << "Unexpected number of channels" << decFrameInfo.channels + << "<>" << getChannelCount(); break; // abort } if (getFrameRate() != decFrameInfo.samplerate) { - qWarning() - << "Corrupt or unsupported AAC file:" - << "Unexpected sample rate" - << decFrameInfo.samplerate << "<>" << getFrameRate(); + qWarning() << "Corrupt or unsupported AAC file:" + << "Unexpected sample rate" << decFrameInfo.samplerate + << "<>" << getFrameRate(); break; // abort } // consume input data m_inputBufferOffset += decFrameInfo.bytesconsumed; // consume decoded samples pSampleBuffer += decFrameInfo.samples; - const size_type numberOfFramesDecoded = samples2frames(decFrameInfo.samples); + const size_type numberOfFramesDecoded = samples2frames( + decFrameInfo.samples); numberOfFramesRemaining -= numberOfFramesDecoded; m_curFrameIndex += numberOfFramesDecoded; } diff --git a/plugins/soundsourcem4a/audiosourcem4a.h b/plugins/soundsourcem4a/audiosourcem4a.h index 781d0af957c..f2fecb79a41 100644 --- a/plugins/soundsourcem4a/audiosourcem4a.h +++ b/plugins/soundsourcem4a/audiosourcem4a.h @@ -5,9 +5,9 @@ #include "util/defs.h" #ifdef __MP4V2__ - #include +#include #else - #include +#include #endif #include @@ -21,10 +21,9 @@ #define MY_EXPORT #endif - namespace Mixxx { -class AudioSourceM4A : public AudioSource { +class AudioSourceM4A: public AudioSource { public: static AudioSourcePointer create(QString fileName); @@ -32,7 +31,8 @@ class AudioSourceM4A : public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer) /*override*/; private: AudioSourceM4A(); diff --git a/plugins/soundsourcem4a/soundsourcem4a.cpp b/plugins/soundsourcem4a/soundsourcem4a.cpp index c1d9c955ee0..69253eb0adf 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.cpp +++ b/plugins/soundsourcem4a/soundsourcem4a.cpp @@ -30,8 +30,8 @@ QList SoundSourceM4A::supportedFileExtensions() { return list; } -SoundSourceM4A::SoundSourceM4A(QString fileName) - : SoundSourcePlugin(fileName, "m4a") { +SoundSourceM4A::SoundSourceM4A(QString fileName) : + SoundSourcePlugin(fileName, "m4a") { } Result SoundSourceM4A::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { @@ -87,9 +87,9 @@ extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { extern "C" MY_EXPORT char** supportedFileExtensions() { const QList supportedFileExtensions( - Mixxx::SoundSourceM4A::supportedFileExtensions()); + Mixxx::SoundSourceM4A::supportedFileExtensions()); return Mixxx::SoundSourcePlugin::allocFileExtensions( - supportedFileExtensions); + supportedFileExtensions); } extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions) { diff --git a/plugins/soundsourcem4a/soundsourcem4a.h b/plugins/soundsourcem4a/soundsourcem4a.h index 14988412391..bf7af6e2f9f 100644 --- a/plugins/soundsourcem4a/soundsourcem4a.h +++ b/plugins/soundsourcem4a/soundsourcem4a.h @@ -1,8 +1,8 @@ /*************************************************************************** - soundsourcem4a.h - mp4/m4a decoder - ------------------- - copyright : (C) 2008 by Garth Dahlstrom - email : ironstorm@users.sf.net + soundsourcem4a.h - mp4/m4a decoder + ------------------- + copyright : (C) 2008 by Garth Dahlstrom + email : ironstorm@users.sf.net ***************************************************************************/ /*************************************************************************** @@ -27,10 +27,9 @@ #define MY_EXPORT #endif - namespace Mixxx { -class SoundSourceM4A : public SoundSourcePlugin { +class SoundSourceM4A: public SoundSourcePlugin { public: static QList supportedFileExtensions(); diff --git a/plugins/soundsourcewv/audiosourcewv.cpp b/plugins/soundsourcewv/audiosourcewv.cpp index 29b5cba112a..00880d96fd3 100644 --- a/plugins/soundsourcewv/audiosourcewv.cpp +++ b/plugins/soundsourcewv/audiosourcewv.cpp @@ -2,9 +2,9 @@ namespace Mixxx { -AudioSourceWV::AudioSourceWV() - : m_wpc(NULL), - m_sampleScale(0.0f) { +AudioSourceWV::AudioSourceWV() : + m_wpc(NULL), + m_sampleScale(0.0f) { } AudioSourceWV::~AudioSourceWV() { @@ -25,8 +25,8 @@ AudioSourcePointer AudioSourceWV::create(QString fileName) { Result AudioSourceWV::open(QString fileName) { char msg[80]; // hold possible error message m_wpc = WavpackOpenFileInput( - fileName.toLocal8Bit().constData(), msg, - OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); + fileName.toLocal8Bit().constData(), msg, + OPEN_2CH_MAX | OPEN_WVC | OPEN_NORMALIZE, 0); if (!m_wpc) { qDebug() << "SSWV::open: failed to open file : " << msg; return ERR; @@ -40,7 +40,8 @@ Result AudioSourceWV::open(QString fileName) { m_sampleScale = kSampleValuePeak; } else { const int bitsPerSample = WavpackGetBitsPerSample(m_wpc); - const uint32_t wavpackPeakSampleValue = uint32_t(1) << (bitsPerSample - 1); + const uint32_t wavpackPeakSampleValue = uint32_t(1) + << (bitsPerSample - 1); m_sampleScale = kSampleValuePeak / sample_type(wavpackPeakSampleValue); } @@ -69,13 +70,13 @@ AudioSource::size_type AudioSourceWV::readSampleFrames( size_type numberOfFrames, sample_type* sampleBuffer) { // static assert: sizeof(sample_type) == sizeof(int32_t) size_type unpackCount = WavpackUnpackSamples(m_wpc, - reinterpret_cast(sampleBuffer), numberOfFrames); + reinterpret_cast(sampleBuffer), numberOfFrames); if (!(WavpackGetMode(m_wpc) & MODE_FLOAT)) { // signed integer -> float const size_type sampleCount = frames2samples(unpackCount); for (size_type i = 0; i < sampleCount; ++i) { const int32_t sampleValue = - reinterpret_cast(sampleBuffer)[i]; + reinterpret_cast(sampleBuffer)[i]; sampleBuffer[i] = sample_type(sampleValue) * m_sampleScale; } } diff --git a/plugins/soundsourcewv/audiosourcewv.h b/plugins/soundsourcewv/audiosourcewv.h index a3cee18e8a4..f959aa206a9 100644 --- a/plugins/soundsourcewv/audiosourcewv.h +++ b/plugins/soundsourcewv/audiosourcewv.h @@ -22,7 +22,8 @@ class AudioSourceWV: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer) /*override*/; private: AudioSourceWV(); diff --git a/plugins/soundsourcewv/soundsourcewv.cpp b/plugins/soundsourcewv/soundsourcewv.cpp index 96838b8d343..cc85e710e85 100644 --- a/plugins/soundsourcewv/soundsourcewv.cpp +++ b/plugins/soundsourcewv/soundsourcewv.cpp @@ -13,8 +13,8 @@ QList SoundSourceWV::supportedFileExtensions() { return list; } -SoundSourceWV::SoundSourceWV(QString fileName) - : SoundSourcePlugin(fileName, "wv") { +SoundSourceWV::SoundSourceWV(QString fileName) : + SoundSourcePlugin(fileName, "wv") { } Result SoundSourceWV::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { @@ -70,9 +70,9 @@ extern "C" MY_EXPORT Mixxx::SoundSource* getSoundSource(QString fileName) { extern "C" MY_EXPORT char** supportedFileExtensions() { const QList supportedFileExtensions( - Mixxx::SoundSourceWV::supportedFileExtensions()); + Mixxx::SoundSourceWV::supportedFileExtensions()); return Mixxx::SoundSourcePlugin::allocFileExtensions( - supportedFileExtensions); + supportedFileExtensions); } extern "C" MY_EXPORT void freeFileExtensions(char** fileExtensions) { diff --git a/src/metadata/audiotagger.cpp b/src/metadata/audiotagger.cpp index 5eb8d617e7f..ad8bbd6e639 100644 --- a/src/metadata/audiotagger.cpp +++ b/src/metadata/audiotagger.cpp @@ -21,8 +21,8 @@ namespace { -inline -SecurityTokenPointer openSecurityToken(QFileInfo file, const SecurityTokenPointer& pToken) { +inline SecurityTokenPointer openSecurityToken(QFileInfo file, + const SecurityTokenPointer& pToken) { if (pToken.isNull()) { return Sandbox::openSecurityToken(file, true); } else { @@ -32,9 +32,9 @@ SecurityTokenPointer openSecurityToken(QFileInfo file, const SecurityTokenPointe } // anonymous namespace -AudioTagger::AudioTagger(const QString& file, SecurityTokenPointer pToken) - : m_file(file), - m_pSecurityToken(openSecurityToken(m_file, pToken)) { +AudioTagger::AudioTagger(const QString& file, SecurityTokenPointer pToken) : + m_file(file), + m_pSecurityToken(openSecurityToken(m_file, pToken)) { } AudioTagger::~AudioTagger() { @@ -44,51 +44,54 @@ bool AudioTagger::save(const Mixxx::TrackMetadata& trackMetadata) { QScopedPointer pFile; const QString filePath(m_file.canonicalFilePath()); - const QByteArray filePathByteArray(m_file.canonicalFilePath().toLocal8Bit()); + const QByteArray filePathByteArray( + m_file.canonicalFilePath().toLocal8Bit()); const char* filePathChars = filePathByteArray.constData(); if (filePath.endsWith(".mp3", Qt::CaseInsensitive)) { QScopedPointer pMpegFile( - new TagLib::MPEG::File(filePathChars)); + new TagLib::MPEG::File(filePathChars)); writeID3v2Tag(pMpegFile->ID3v2Tag(true), trackMetadata); // mandatory writeAPETag(pMpegFile->APETag(false), trackMetadata); // optional pFile.reset(pMpegFile.take()); // transfer ownership } else if (filePath.endsWith(".m4a", Qt::CaseInsensitive)) { QScopedPointer pMp4File( - new TagLib::MP4::File(filePathChars)); + new TagLib::MP4::File(filePathChars)); writeMP4Tag(pMp4File->tag(), trackMetadata); pFile.reset(pMp4File.take()); // transfer ownership } else if (filePath.endsWith(".ogg", Qt::CaseInsensitive)) { QScopedPointer pOggFile( - new TagLib::Ogg::Vorbis::File(filePathChars)); + new TagLib::Ogg::Vorbis::File(filePathChars)); writeXiphComment(pOggFile->tag(), trackMetadata); pFile.reset(pOggFile.take()); // transfer ownership } else if (filePath.endsWith(".flac", Qt::CaseInsensitive)) { QScopedPointer pFlacFile( - new TagLib::FLAC::File(filePathChars)); + new TagLib::FLAC::File(filePathChars)); writeXiphComment(pFlacFile->xiphComment(true), trackMetadata); // mandatory writeID3v2Tag(pFlacFile->ID3v2Tag(false), trackMetadata); // optional pFile.reset(pFlacFile.take()); // transfer ownership } else if (filePath.endsWith(".wav", Qt::CaseInsensitive)) { QScopedPointer pWavFile( - new TagLib::RIFF::WAV::File(filePathChars)); + new TagLib::RIFF::WAV::File(filePathChars)); writeID3v2Tag(pWavFile->tag(), trackMetadata); pFile.reset(pWavFile.take()); // transfer ownership - } else if (filePath.endsWith(".aif", Qt::CaseInsensitive) || - filePath.endsWith(".aiff", Qt::CaseInsensitive)) { + } else if (filePath.endsWith(".aif", Qt::CaseInsensitive) + || filePath.endsWith(".aiff", Qt::CaseInsensitive)) { QScopedPointer pAiffFile( - new TagLib::RIFF::AIFF::File(filePathChars)); + new TagLib::RIFF::AIFF::File(filePathChars)); writeID3v2Tag(pAiffFile->tag(), trackMetadata); pFile.reset(pAiffFile.take()); // transfer ownership #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) } else if (filePath.endsWith(".opus", Qt::CaseInsensitive)) { QScopedPointer pOpusFile( - new TagLib::Ogg::Opus::File(filePathChars)); + new TagLib::Ogg::Opus::File(filePathChars)); writeXiphComment(pOpusFile->tag(), trackMetadata); pFile.reset(pOpusFile.take()); // transfer ownership #endif } else { - qWarning() << "Unsupported file type! Could not update metadata of track " << filePath; + qWarning() + << "Unsupported file type! Could not update metadata of track " + << filePath; return false; } diff --git a/src/metadata/audiotagger.h b/src/metadata/audiotagger.h index 215b2f0ffb4..a26af068bc1 100644 --- a/src/metadata/audiotagger.h +++ b/src/metadata/audiotagger.h @@ -1,4 +1,3 @@ - #ifndef AUDIOTAGGER_H #define AUDIOTAGGER_H @@ -8,13 +7,13 @@ #include class AudioTagger { - public: +public: AudioTagger(const QString& file, SecurityTokenPointer pToken); virtual ~AudioTagger(); bool save(const Mixxx::TrackMetadata& trackMetadata); - private: +private: QFileInfo m_file; SecurityTokenPointer m_pSecurityToken; }; diff --git a/src/metadata/trackmetadata.cpp b/src/metadata/trackmetadata.cpp index 9f029f4530f..afcea616b2b 100644 --- a/src/metadata/trackmetadata.cpp +++ b/src/metadata/trackmetadata.cpp @@ -12,13 +12,13 @@ namespace Mixxx { /*static*/ const double TrackMetadata::REPLAYGAIN_MIN = 0.0f; // exclusive lower bound /*static*/ const double TrackMetadata::REPLAYGAIN_0DB = 1.0f; -TrackMetadata::TrackMetadata() - : m_channels(0), - m_sampleRate(0), - m_bitrate(0), - m_duration(0), - m_bpm(BPM_UNDEFINED), - m_replayGain(REPLAYGAIN_UNDEFINED) { +TrackMetadata::TrackMetadata() : + m_channels(0), + m_sampleRate(0), + m_bitrate(0), + m_duration(0), + m_bpm(BPM_UNDEFINED), + m_replayGain(REPLAYGAIN_UNDEFINED) { } double TrackMetadata::parseBpmString(const QString& sBpm) { diff --git a/src/metadata/trackmetadatataglib.cpp b/src/metadata/trackmetadatataglib.cpp index de3e0a59bb0..b79b4d2ef5f 100644 --- a/src/metadata/trackmetadatataglib.cpp +++ b/src/metadata/trackmetadatataglib.cpp @@ -26,8 +26,7 @@ const bool kDebugMetadata = false; // Taglib strings can be NULL and using it could cause some segfaults, // so in this case it will return a QString() -inline -QString toQString(const TagLib::String& tString) { +inline QString toQString(const TagLib::String& tString) { if (tString.isNull()) { return QString(); } else { @@ -37,15 +36,13 @@ QString toQString(const TagLib::String& tString) { // Concatenates the elements of a TagLib string list // into a single string. -inline -QString toQStringConcat(const TagLib::StringList& strList) { +inline QString toQStringConcat(const TagLib::StringList& strList) { return toQString(strList.toString()); } // Concatenates the frame list of an ID3v2 tag into a // single string. -inline -QString toQString(const TagLib::ID3v2::Frame& frame) { +inline QString toQString(const TagLib::ID3v2::Frame& frame) { return toQString(frame.toString()); } @@ -53,8 +50,8 @@ QString toQString(const TagLib::ID3v2::Frame& frame) { // single string. QString toQStringConcat(const TagLib::ID3v2::FrameList& frameList) { QString result; - for (TagLib::ID3v2::FrameList::ConstIterator - i(frameList.begin()); frameList.end() != i; ++i) { + for (TagLib::ID3v2::FrameList::ConstIterator i(frameList.begin()); + frameList.end() != i; ++i) { result += toQString(**i); } return result; @@ -62,8 +59,8 @@ QString toQStringConcat(const TagLib::ID3v2::FrameList& frameList) { // Returns the first ID3v2 frame that is not empty. QString toQStringFirst(const TagLib::ID3v2::FrameList& frameList) { - for (TagLib::ID3v2::FrameList::ConstIterator - i(frameList.begin()); frameList.end() != i; ++i) { + for (TagLib::ID3v2::FrameList::ConstIterator i(frameList.begin()); + frameList.end() != i; ++i) { const QString value(toQString(**i).trimmed()); if (!value.isEmpty()) { return value; @@ -74,16 +71,15 @@ QString toQStringFirst(const TagLib::ID3v2::FrameList& frameList) { // Concatenates the string list of an MP4 item // into a single string. -inline -QString toQStringConcat(const TagLib::MP4::Item& mp4Item) { +inline QString toQStringConcat(const TagLib::MP4::Item& mp4Item) { return toQStringConcat(mp4Item.toStringList()); } // Returns the first MP4 item string that is not empty. QString toQStringFirst(const TagLib::MP4::Item& mp4Item) { - const TagLib::StringList strList(mp4Item.toStringList()); - for (TagLib::StringList::ConstIterator - i(strList.begin()); strList.end() != i; ++i) { + const TagLib::StringList strList(mp4Item.toStringList()); + for (TagLib::StringList::ConstIterator i(strList.begin()); + strList.end() != i; ++i) { const QString value(toQString(*i).trimmed()); if (!value.isEmpty()) { return value; @@ -92,26 +88,22 @@ QString toQStringFirst(const TagLib::MP4::Item& mp4Item) { return QString(); } -inline -QString toQString(const TagLib::APE::Item& apeItem) { +inline QString toQString(const TagLib::APE::Item& apeItem) { return toQString(apeItem.toString()); } -inline -TagLib::String toTagLibString(const QString& str) { +inline TagLib::String toTagLibString(const QString& str) { const QByteArray qba(str.toUtf8()); return TagLib::String(qba.constData(), TagLib::String::UTF8); } template -inline -QString formatString(const T& value) { +inline QString formatString(const T& value) { return QString("%1").arg(value); } template -inline -QString formatString(const T& value, const T& emptyValue) { +inline QString formatString(const T& value, const T& emptyValue) { if (value == emptyValue) { return QString(); /// empty string } else { @@ -120,14 +112,14 @@ QString formatString(const T& value, const T& emptyValue) { } template -inline -QString formatBpmString(const T& bpm) { +inline QString formatBpmString(const T& bpm) { return formatString(bpm, TrackMetadata::BPM_UNDEFINED); } } // anonymous namespace -bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& file) { +bool readAudioProperties(TrackMetadata* pTrackMetadata, + const TagLib::File& file) { if (kDebugMetadata) { qDebug() << "Parsing" << file.name(); } @@ -141,12 +133,11 @@ bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& file pTrackMetadata->setBitrate(properties->bitrate()); if (kDebugMetadata) { - qDebug() - << "TagLib" - << "channels" << pTrackMetadata->getChannels() - << "sampleRate" << pTrackMetadata->getSampleRate() - << "bitrate" << pTrackMetadata->getBitrate() - << "duration" << pTrackMetadata->getDuration(); + qDebug() << "TagLib" << "channels" + << pTrackMetadata->getChannels() << "sampleRate" + << pTrackMetadata->getSampleRate() << "bitrate" + << pTrackMetadata->getBitrate() << "duration" + << pTrackMetadata->getDuration(); } return true; @@ -173,29 +164,25 @@ void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag) { } if (kDebugMetadata) { - qDebug() - << "TagLib" - << "title" << pTrackMetadata->getTitle() - << "artist" << pTrackMetadata->getArtist() - << "album" << pTrackMetadata->getAlbum() - << "comment" << pTrackMetadata->getComment() - << "genre" << pTrackMetadata->getGenre() - << "year" << pTrackMetadata->getYear() - << "trackNumber" << pTrackMetadata->getTrackNumber(); + qDebug() << "TagLib" << "title" << pTrackMetadata->getTitle() + << "artist" << pTrackMetadata->getArtist() << "album" + << pTrackMetadata->getAlbum() << "comment" + << pTrackMetadata->getComment() << "genre" + << pTrackMetadata->getGenre() << "year" + << pTrackMetadata->getYear() << "trackNumber" + << pTrackMetadata->getTrackNumber(); } } -void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) { +void readID3v2Tag(TrackMetadata* pTrackMetadata, + const TagLib::ID3v2::Tag& tag) { // Print every frame in the file. if (kDebugMetadata) { - for(TagLib::ID3v2::FrameList::ConstIterator - it(tag.frameList().begin()); it != tag.frameList().end(); ++it) { - qDebug() - << "ID3V2" - << (*it)->frameID().data() - << "-" - << toQString((*it)->toString()); + for (TagLib::ID3v2::FrameList::ConstIterator it( + tag.frameList().begin()); it != tag.frameList().end(); ++it) { + qDebug() << "ID3V2" << (*it)->frameID().data() << "-" + << toQString((*it)->toString()); } } @@ -203,129 +190,117 @@ void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag) const TagLib::ID3v2::FrameList bpmFrame(tag.frameListMap()["TBPM"]); if (!bpmFrame.isEmpty()) { - pTrackMetadata->setBpmString( - toQStringFirst(bpmFrame)); + pTrackMetadata->setBpmString(toQStringFirst(bpmFrame)); } const TagLib::ID3v2::FrameList keyFrame(tag.frameListMap()["TKEY"]); if (!keyFrame.isEmpty()) { - pTrackMetadata->setKey( - toQStringFirst(keyFrame)); + pTrackMetadata->setKey(toQStringFirst(keyFrame)); } // Foobar2000-style ID3v2.3.0 tags // TODO: Check if everything is ok. TagLib::ID3v2::FrameList textFrames(tag.frameListMap()["TXXX"]); - for (TagLib::ID3v2::FrameList::ConstIterator - it(textFrames.begin()); it != textFrames.end(); ++it) { + for (TagLib::ID3v2::FrameList::ConstIterator it(textFrames.begin()); + it != textFrames.end(); ++it) { TagLib::ID3v2::UserTextIdentificationFrame* replaygainFrame = dynamic_cast(*it); if (replaygainFrame && replaygainFrame->fieldList().size() >= 2) { const QString desc( - toQString(replaygainFrame->description()).toLower()); + toQString(replaygainFrame->description()).toLower()); if (desc == "replaygain_album_gain") { pTrackMetadata->setReplayGainDbString( - toQString(replaygainFrame->fieldList()[1])); + toQString(replaygainFrame->fieldList()[1])); } // Prefer track gain over album gain. if (desc == "replaygain_track_gain") { pTrackMetadata->setReplayGainDbString( - toQString(replaygainFrame->fieldList()[1])); + toQString(replaygainFrame->fieldList()[1])); } } } const TagLib::ID3v2::FrameList albumArtistFrame(tag.frameListMap()["TPE2"]); if (!albumArtistFrame.isEmpty()) { - pTrackMetadata->setAlbumArtist( - toQStringConcat(albumArtistFrame)); + pTrackMetadata->setAlbumArtist(toQStringConcat(albumArtistFrame)); } if (pTrackMetadata->getAlbum().isEmpty()) { - const TagLib::ID3v2::FrameList originalAlbumFrame(tag.frameListMap()["TOAL"]); - pTrackMetadata->setAlbum( - toQStringConcat(originalAlbumFrame)); + const TagLib::ID3v2::FrameList originalAlbumFrame( + tag.frameListMap()["TOAL"]); + pTrackMetadata->setAlbum(toQStringConcat(originalAlbumFrame)); } const TagLib::ID3v2::FrameList composerFrame(tag.frameListMap()["TCOM"]); if (!composerFrame.isEmpty()) { - pTrackMetadata->setComposer( - toQStringConcat(composerFrame)); + pTrackMetadata->setComposer(toQStringConcat(composerFrame)); } const TagLib::ID3v2::FrameList groupingFrame(tag.frameListMap()["TIT1"]); if (!groupingFrame.isEmpty()) { - pTrackMetadata->setGrouping( - toQStringConcat(groupingFrame)); + pTrackMetadata->setGrouping(toQStringConcat(groupingFrame)); } // ID3v2.4.0: TDRC replaces TYER + TDAT - const TagLib::ID3v2::FrameList recordingDateFrame(tag.frameListMap()["TDRC"]); + const TagLib::ID3v2::FrameList recordingDateFrame( + tag.frameListMap()["TDRC"]); if (!recordingDateFrame.isEmpty()) { - pTrackMetadata->setYear( - toQStringFirst(recordingDateFrame)); + pTrackMetadata->setYear(toQStringFirst(recordingDateFrame)); } } void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag) { if (kDebugMetadata) { - for(TagLib::APE::ItemListMap::ConstIterator - it(tag.itemListMap().begin()); it != tag.itemListMap().end(); ++it) { - qDebug() - << "APE" - << toQString((*it).first) - << "-" - << toQString((*it).second.toString()); + for (TagLib::APE::ItemListMap::ConstIterator it( + tag.itemListMap().begin()); it != tag.itemListMap().end(); + ++it) { + qDebug() << "APE" << toQString((*it).first) << "-" + << toQString((*it).second.toString()); } } readTag(pTrackMetadata, tag); if (tag.itemListMap().contains("BPM")) { - pTrackMetadata->setBpmString( - toQString(tag.itemListMap()["BPM"])); + pTrackMetadata->setBpmString(toQString(tag.itemListMap()["BPM"])); } if (tag.itemListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { pTrackMetadata->setReplayGainDbString( - toQString(tag.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); + toQString(tag.itemListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. if (tag.itemListMap().contains("REPLAYGAIN_TRACK_GAIN")) { pTrackMetadata->setReplayGainDbString( - toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); + toQString(tag.itemListMap()["REPLAYGAIN_TRACK_GAIN"])); } if (tag.itemListMap().contains("Album Artist")) { pTrackMetadata->setAlbumArtist( - toQString(tag.itemListMap()["Album Artist"])); + toQString(tag.itemListMap()["Album Artist"])); } if (tag.itemListMap().contains("Composer")) { - pTrackMetadata->setComposer( - toQString(tag.itemListMap()["Composer"])); + pTrackMetadata->setComposer(toQString(tag.itemListMap()["Composer"])); } if (tag.itemListMap().contains("Grouping")) { - pTrackMetadata->setGrouping( - toQString(tag.itemListMap()["Grouping"])); + pTrackMetadata->setGrouping(toQString(tag.itemListMap()["Grouping"])); } if (tag.itemListMap().contains("Year")) { - pTrackMetadata->setYear( - toQString(tag.itemListMap()["Year"])); + pTrackMetadata->setYear(toQString(tag.itemListMap()["Year"])); } } -void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag) { +void readXiphComment(TrackMetadata* pTrackMetadata, + const TagLib::Ogg::XiphComment& tag) { if (kDebugMetadata) { - for (TagLib::Ogg::FieldListMap::ConstIterator - it(tag.fieldListMap().begin()); it != tag.fieldListMap().end(); ++it) { - qDebug() - << "XIPH" - << toQString((*it).first) - << "-" - << toQString((*it).second.toString()); + for (TagLib::Ogg::FieldListMap::ConstIterator it( + tag.fieldListMap().begin()); it != tag.fieldListMap().end(); + ++it) { + qDebug() << "XIPH" << toQString((*it).first) << "-" + << toQString((*it).second.toString()); } } @@ -335,32 +310,31 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme // instead DESCRIPTION. If the comment field (correctly populated by TagLib // from DESCRIPTION) is still empty we will additionally read this field. // Reference: http://www.xiph.org/vorbis/doc/v-comment.html - if (pTrackMetadata->getComment().isEmpty() && - tag.fieldListMap().contains("COMMENT")) { + if (pTrackMetadata->getComment().isEmpty() + && tag.fieldListMap().contains("COMMENT")) { pTrackMetadata->setComment( - toQStringConcat(tag.fieldListMap()["COMMENT"])); + toQStringConcat(tag.fieldListMap()["COMMENT"])); } // Some tags use "BPM" so check for that. if (tag.fieldListMap().contains("BPM")) { - pTrackMetadata->setBpmString( - toQStringFirst(tag.fieldListMap()["BPM"])); + pTrackMetadata->setBpmString(toQStringFirst(tag.fieldListMap()["BPM"])); } // Give preference to the "TEMPO" tag which seems to be more standard if (tag.fieldListMap().contains("TEMPO")) { pTrackMetadata->setBpmString( - toQStringFirst(tag.fieldListMap()["TEMPO"])); + toQStringFirst(tag.fieldListMap()["TEMPO"])); } if (tag.fieldListMap().contains("REPLAYGAIN_ALBUM_GAIN")) { pTrackMetadata->setReplayGainDbString( - toQStringFirst(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); + toQStringFirst(tag.fieldListMap()["REPLAYGAIN_ALBUM_GAIN"])); } //Prefer track gain over album gain. if (tag.fieldListMap().contains("REPLAYGAIN_TRACK_GAIN")) { pTrackMetadata->setReplayGainDbString( - toQStringFirst(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); + toQStringFirst(tag.fieldListMap()["REPLAYGAIN_TRACK_GAIN"])); } /* @@ -372,39 +346,40 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme * or a "KEY" vorbis comment. */ if (tag.fieldListMap().contains("KEY")) { - pTrackMetadata->setKey( - toQStringFirst(tag.fieldListMap()["KEY"])); + pTrackMetadata->setKey(toQStringFirst(tag.fieldListMap()["KEY"])); } - if (pTrackMetadata->getKey().isEmpty() && - tag.fieldListMap().contains("INITIALKEY")) { + if (pTrackMetadata->getKey().isEmpty() + && tag.fieldListMap().contains("INITIALKEY")) { // try alternative field name pTrackMetadata->setKey( - toQStringFirst(tag.fieldListMap()["INITIALKEY"])); + toQStringFirst(tag.fieldListMap()["INITIALKEY"])); } if (tag.fieldListMap().contains("ALBUMARTIST")) { pTrackMetadata->setAlbumArtist( - toQStringConcat(tag.fieldListMap()["ALBUMARTIST"])); + toQStringConcat(tag.fieldListMap()["ALBUMARTIST"])); } - if (pTrackMetadata->getAlbumArtist().isEmpty() && - tag.fieldListMap().contains("ALBUM_ARTIST")) { + if (pTrackMetadata->getAlbumArtist().isEmpty() + && tag.fieldListMap().contains("ALBUM_ARTIST")) { // try alternative field name pTrackMetadata->setAlbumArtist( - toQStringConcat(tag.fieldListMap()["ALBUM_ARTIST"])); + toQStringConcat(tag.fieldListMap()["ALBUM_ARTIST"])); } - if (pTrackMetadata->getAlbumArtist().isEmpty() && - tag.fieldListMap().contains("ALBUM ARTIST")) { + if (pTrackMetadata->getAlbumArtist().isEmpty() + && tag.fieldListMap().contains("ALBUM ARTIST")) { // try alternative field name pTrackMetadata->setAlbumArtist( - toQStringConcat(tag.fieldListMap()["ALBUM ARTIST"])); + toQStringConcat(tag.fieldListMap()["ALBUM ARTIST"])); } if (tag.fieldListMap().contains("COMPOSER")) { - pTrackMetadata->setComposer(toQStringConcat(tag.fieldListMap()["COMPOSER"])); + pTrackMetadata->setComposer( + toQStringConcat(tag.fieldListMap()["COMPOSER"])); } if (tag.fieldListMap().contains("GROUPING")) { - pTrackMetadata->setGrouping(toQStringConcat(tag.fieldListMap()["GROUPING"])); + pTrackMetadata->setGrouping( + toQStringConcat(tag.fieldListMap()["GROUPING"])); } if (tag.fieldListMap().contains("DATE")) { @@ -412,15 +387,13 @@ void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComme } } -void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) { +void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag) { if (kDebugMetadata) { - for(TagLib::MP4::ItemListMap::ConstIterator - it(tag.itemListMap().begin()); it != tag.itemListMap().end(); ++it) { - qDebug() - << "MP4" - << toQString((*it).first) - << "-" - << toQStringConcat((*it).second); + for (TagLib::MP4::ItemListMap::ConstIterator it( + tag.itemListMap().begin()); it != tag.itemListMap().end(); + ++it) { + qDebug() << "MP4" << toQString((*it).first) << "-" + << toQStringConcat((*it).second); } } @@ -429,8 +402,7 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) // Get BPM if (tag.itemListMap().contains("tmpo")) { // Read the BPM as an integer value. - pTrackMetadata->setBpm( - tag.itemListMap()["tmpo"].toInt()); + pTrackMetadata->setBpm(tag.itemListMap()["tmpo"].toInt()); } if (tag.itemListMap().contains("----:com.apple.iTunes:BPM")) { // This is the preferred field for storing the BPM @@ -439,45 +411,46 @@ void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag) // BPM value that might have been read before is // overwritten. pTrackMetadata->setBpmString( - toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:BPM"])); + toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:BPM"])); } // Get Album Artist if (tag.itemListMap().contains("aART")) { pTrackMetadata->setAlbumArtist( - toQStringConcat(tag.itemListMap()["aART"])); + toQStringConcat(tag.itemListMap()["aART"])); } // Get Composer if (tag.itemListMap().contains("\251wrt")) { pTrackMetadata->setComposer( - toQStringConcat(tag.itemListMap()["\251wrt"])); + toQStringConcat(tag.itemListMap()["\251wrt"])); } // Get Grouping if (tag.itemListMap().contains("\251grp")) { pTrackMetadata->setGrouping( - toQStringConcat(tag.itemListMap()["\251grp"])); + toQStringConcat(tag.itemListMap()["\251grp"])); } // Get date/year as string if (tag.itemListMap().contains("\251day")) { - pTrackMetadata->setYear( - toQStringFirst(tag.itemListMap()["\251day"])); + pTrackMetadata->setYear(toQStringFirst(tag.itemListMap()["\251day"])); } // Get KEY (conforms to Rapid Evolution) if (tag.itemListMap().contains("----:com.apple.iTunes:KEY")) { pTrackMetadata->setKey( - toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:KEY"])); + toQStringFirst(tag.itemListMap()["----:com.apple.iTunes:KEY"])); } // Apparently iTunes stores replaygain in this property. - if (tag.itemListMap().contains("----:com.apple.iTunes:replaygain_album_gain")) { + if (tag.itemListMap().contains( + "----:com.apple.iTunes:replaygain_album_gain")) { // TODO(XXX) find tracks with this property and check what it looks } //Prefer track gain over album gain. - if (tag.itemListMap().contains("----:com.apple.iTunes:replaygain_track_gain")) { + if (tag.itemListMap().contains( + "----:com.apple.iTunes:replaygain_track_gain")) { // TODO(XXX) find tracks with this property and check what it looks } } @@ -487,26 +460,25 @@ QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag) { TagLib::ID3v2::FrameList covertArtFrame = tag.frameListMap()["APIC"]; if (!covertArtFrame.isEmpty()) { TagLib::ID3v2::AttachedPictureFrame* picframe = - static_cast( - covertArtFrame.front()); + static_cast(covertArtFrame.front()); TagLib::ByteVector data = picframe->picture(); coverArt = QImage::fromData( - reinterpret_cast(data.data()), data.size()); + reinterpret_cast(data.data()), data.size()); } return coverArt; } QImage readAPETagCover(const TagLib::APE::Tag& tag) { QImage coverArt; - if (tag.itemListMap().contains("COVER ART (FRONT)")) - { + if (tag.itemListMap().contains("COVER ART (FRONT)")) { const TagLib::ByteVector nullStringTerminator(1, 0); - TagLib::ByteVector item = tag.itemListMap()["COVER ART (FRONT)"].value(); + TagLib::ByteVector item = + tag.itemListMap()["COVER ART (FRONT)"].value(); int pos = item.find(nullStringTerminator); // skip the filename if (++pos > 0) { const TagLib::ByteVector& data = item.mid(pos); coverArt = QImage::fromData( - reinterpret_cast(data.data()), data.size()); + reinterpret_cast(data.data()), data.size()); } } return coverArt; @@ -515,28 +487,30 @@ QImage readAPETagCover(const TagLib::APE::Tag& tag) { QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag) { QImage coverArt; if (tag.fieldListMap().contains("METADATA_BLOCK_PICTURE")) { - QByteArray data(QByteArray::fromBase64( - tag.fieldListMap()["METADATA_BLOCK_PICTURE"].front().toCString())); + QByteArray data( + QByteArray::fromBase64( + tag.fieldListMap()["METADATA_BLOCK_PICTURE"].front().toCString())); TagLib::ByteVector tdata(data.data(), data.size()); TagLib::FLAC::Picture p(tdata); data = QByteArray(p.data().data(), p.data().size()); coverArt = QImage::fromData(data); } else if (tag.fieldListMap().contains("COVERART")) { - QByteArray data(QByteArray::fromBase64( - tag.fieldListMap()["COVERART"].toString().toCString())); + QByteArray data( + QByteArray::fromBase64( + tag.fieldListMap()["COVERART"].toString().toCString())); coverArt = QImage::fromData(data); } return coverArt; } -QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& tag) { +QImage readMP4TagCover(/*const*/TagLib::MP4::Tag& tag) { QImage coverArt; if (tag.itemListMap().contains("covr")) { TagLib::MP4::CoverArtList coverArtList = - tag.itemListMap()["covr"].toCoverArtList(); + tag.itemListMap()["covr"].toCoverArtList(); TagLib::ByteVector data = coverArtList.front().data(); coverArt = QImage::fromData( - reinterpret_cast(data.data()), data.size()); + reinterpret_cast(data.data()), data.size()); } return coverArt; } @@ -548,7 +522,8 @@ void replaceID3v2Frame(TagLib::ID3v2::Tag* pTag, TagLib::ID3v2::Frame* pFrame) { pTag->addFrame(pFrame); } -void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, const TagLib::ByteVector &id, const QString& text) { +void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, + const TagLib::ByteVector &id, const QString& text) { TagLib::String::Type textType; QByteArray textData; if (4 <= pTag->header()->majorVersion()) { @@ -560,7 +535,7 @@ void writeID3v2TextIdentificationFrame(TagLib::ID3v2::Tag* pTag, const TagLib::B textData = text.toLatin1(); } QScopedPointer pNewFrame( - new TagLib::ID3v2::TextIdentificationFrame(id, textType)); + new TagLib::ID3v2::TextIdentificationFrame(id, textType)); pNewFrame->setText(toTagLibString(text)); replaceID3v2Frame(pTag, pNewFrame.data()); pNewFrame.take(); // release ownership @@ -579,8 +554,8 @@ bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { pTag->setGenre(toTagLibString(trackMetadata.getGenre())); pTag->setComment(toTagLibString(trackMetadata.getComment())); bool yearValid = false; - uint year = trackMetadata.getYear().toUInt(&yearValid); - if (yearValid && (year > 0)) { + uint year = trackMetadata.getYear().toUInt(&yearValid); + if (yearValid && (year > 0)) { pTag->setYear(year); } bool trackNumberValid = false; @@ -592,7 +567,8 @@ bool writeTag(TagLib::Tag* pTag, const TrackMetadata& trackMetadata) { return true; } -bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) { +bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, + const TrackMetadata& trackMetadata) { if (NULL == pTag) { return false; } @@ -606,20 +582,19 @@ bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata) writeTag(pTag, trackMetadata); // additional tags - writeID3v2TextIdentificationFrame(pTag, - "TPE2", trackMetadata.getAlbumArtist()); - writeID3v2TextIdentificationFrame(pTag, - "TBPM", formatBpmString(trackMetadata.getBpm())); - writeID3v2TextIdentificationFrame(pTag, - "TKEY", trackMetadata.getKey()); - writeID3v2TextIdentificationFrame(pTag, - "TCOM", trackMetadata.getComposer()); - writeID3v2TextIdentificationFrame(pTag, - "TIT1", trackMetadata.getGrouping()); + writeID3v2TextIdentificationFrame(pTag, "TPE2", + trackMetadata.getAlbumArtist()); + writeID3v2TextIdentificationFrame(pTag, "TBPM", + formatBpmString(trackMetadata.getBpm())); + writeID3v2TextIdentificationFrame(pTag, "TKEY", trackMetadata.getKey()); + writeID3v2TextIdentificationFrame(pTag, "TCOM", + trackMetadata.getComposer()); + writeID3v2TextIdentificationFrame(pTag, "TIT1", + trackMetadata.getGrouping()); if (4 <= pHeader->majorVersion()) { // ID3v2.4.0: TDRC replaces TYER + TDAT - writeID3v2TextIdentificationFrame(pTag, - "TDRC", formatString(trackMetadata.getYear())); + writeID3v2TextIdentificationFrame(pTag, "TDRC", + formatString(trackMetadata.getYear())); } return true; @@ -634,20 +609,20 @@ bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata) { writeTag(pTag, trackMetadata); pTag->addValue("BPM", - toTagLibString(formatBpmString(trackMetadata.getBpm())), true); + toTagLibString(formatBpmString(trackMetadata.getBpm())), true); pTag->addValue("Album Artist", - toTagLibString(trackMetadata.getAlbumArtist()), true); - pTag->addValue("Composer", - toTagLibString(trackMetadata.getComposer()), true); - pTag->addValue("Grouping", - toTagLibString(trackMetadata.getGrouping()), true); - pTag->addValue("Year", - toTagLibString(trackMetadata.getYear()), true); + toTagLibString(trackMetadata.getAlbumArtist()), true); + pTag->addValue("Composer", toTagLibString(trackMetadata.getComposer()), + true); + pTag->addValue("Grouping", toTagLibString(trackMetadata.getGrouping()), + true); + pTag->addValue("Year", toTagLibString(trackMetadata.getYear()), true); return true; } -bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& trackMetadata) { +bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, + const TrackMetadata& trackMetadata) { if (NULL == pTag) { return false; } @@ -660,58 +635,49 @@ bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& track pTag->removeField("ALBUMARTIST"); pTag->addField("ALBUMARTIST", - toTagLibString(trackMetadata.getAlbumArtist())); + toTagLibString(trackMetadata.getAlbumArtist())); // Some tools use "BPM" so write that. pTag->removeField("BPM"); pTag->addField("BPM", - toTagLibString(formatBpmString(trackMetadata.getBpm()))); + toTagLibString(formatBpmString(trackMetadata.getBpm()))); pTag->removeField("TEMPO"); pTag->addField("TEMPO", - toTagLibString(formatBpmString(trackMetadata.getBpm()))); + toTagLibString(formatBpmString(trackMetadata.getBpm()))); pTag->removeField("INITIALKEY"); - pTag->addField("INITIALKEY", - toTagLibString(trackMetadata.getKey())); + pTag->addField("INITIALKEY", toTagLibString(trackMetadata.getKey())); pTag->removeField("KEY"); - pTag->addField("KEY", - toTagLibString(trackMetadata.getKey())); + pTag->addField("KEY", toTagLibString(trackMetadata.getKey())); pTag->removeField("COMPOSER"); - pTag->addField("COMPOSER", - toTagLibString(trackMetadata.getComposer())); + pTag->addField("COMPOSER", toTagLibString(trackMetadata.getComposer())); pTag->removeField("GROUPING"); - pTag->addField("GROUPING", - toTagLibString(trackMetadata.getGrouping())); + pTag->addField("GROUPING", toTagLibString(trackMetadata.getGrouping())); pTag->removeField("DATE"); - pTag->addField("DATE", - toTagLibString(trackMetadata.getYear())); + pTag->addField("DATE", toTagLibString(trackMetadata.getYear())); return true; } namespace { - template - inline void writeMP4Atom( - TagLib::MP4::Tag* pTag, - const TagLib::String& key, +template +inline void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, const T& value) { - pTag->itemListMap()[key] = value; - } + pTag->itemListMap()[key] = value; +} - void writeMP4Atom( - TagLib::MP4::Tag* pTag, - const TagLib::String& key, +void writeMP4Atom(TagLib::MP4::Tag* pTag, const TagLib::String& key, const QString& value) { - if (value.isEmpty()) { - pTag->itemListMap().erase(key); - } else { - writeMP4Atom(pTag, key, TagLib::StringList(toTagLibString(value))); - } + if (value.isEmpty()) { + pTag->itemListMap().erase(key); + } else { + writeMP4Atom(pTag, key, TagLib::StringList(toTagLibString(value))); } +} } // anonymous namespace @@ -723,24 +689,18 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata) { // Write common metadata writeTag(pTag, trackMetadata); - writeMP4Atom(pTag, "aART", - trackMetadata.getAlbumArtist()); - writeMP4Atom(pTag, "\251wrt", - trackMetadata.getComposer()); - writeMP4Atom(pTag, "\251grp", - trackMetadata.getGrouping()); - writeMP4Atom(pTag, "\251day", - trackMetadata.getYear()); + writeMP4Atom(pTag, "aART", trackMetadata.getAlbumArtist()); + writeMP4Atom(pTag, "\251wrt", trackMetadata.getComposer()); + writeMP4Atom(pTag, "\251grp", trackMetadata.getGrouping()); + writeMP4Atom(pTag, "\251day", trackMetadata.getYear()); if (trackMetadata.isBpmValid()) { - writeMP4Atom(pTag, "tmpo", - (int) round(trackMetadata.getBpm())); + writeMP4Atom(pTag, "tmpo", (int) round(trackMetadata.getBpm())); } else { pTag->itemListMap().erase("tmpo"); } writeMP4Atom(pTag, "----:com.apple.iTunes:BPM", - formatBpmString(trackMetadata.getBpm())); - writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", - trackMetadata.getKey()); + formatBpmString(trackMetadata.getBpm())); + writeMP4Atom(pTag, "----:com.apple.iTunes:KEY", trackMetadata.getKey()); return true; } diff --git a/src/metadata/trackmetadatataglib.h b/src/metadata/trackmetadatataglib.h index aa0f1c44bd9..138bf38b0f3 100644 --- a/src/metadata/trackmetadatataglib.h +++ b/src/metadata/trackmetadatataglib.h @@ -22,7 +22,8 @@ namespace Mixxx { // Read common audio properties of a file -bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& file); +bool readAudioProperties(TrackMetadata* pTrackMetadata, + const TagLib::File& file); // Read metadata // The general function readTag() is implicitly invoked @@ -30,15 +31,18 @@ bool readAudioProperties(TrackMetadata* pTrackMetadata, const TagLib::File& file void readTag(TrackMetadata* pTrackMetadata, const TagLib::Tag& tag); void readID3v2Tag(TrackMetadata* pTrackMetadata, const TagLib::ID3v2::Tag& tag); void readAPETag(TrackMetadata* pTrackMetadata, const TagLib::APE::Tag& tag); -void readXiphComment(TrackMetadata* pTrackMetadata, const TagLib::Ogg::XiphComment& tag); -void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/ TagLib::MP4::Tag& tag); +void readXiphComment(TrackMetadata* pTrackMetadata, + const TagLib::Ogg::XiphComment& tag); +void readMP4Tag(TrackMetadata* pTrackMetadata, /*const*/TagLib::MP4::Tag& tag); // Write metadata // The general function writeTag() is implicitly invoked // from the specialized tag writing functions! -bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, const TrackMetadata& trackMetadata); +bool writeID3v2Tag(TagLib::ID3v2::Tag* pTag, + const TrackMetadata& trackMetadata); bool writeAPETag(TagLib::APE::Tag* pTag, const TrackMetadata& trackMetadata); -bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, const TrackMetadata& trackMetadata); +bool writeXiphComment(TagLib::Ogg::XiphComment* pTag, + const TrackMetadata& trackMetadata); bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata); // Read cover art @@ -47,9 +51,8 @@ bool writeMP4Tag(TagLib::MP4::Tag* pTag, const TrackMetadata& trackMetadata); QImage readID3v2TagCover(const TagLib::ID3v2::Tag& tag); QImage readAPETagCover(const TagLib::APE::Tag& tag); QImage readXiphCommentCover(const TagLib::Ogg::XiphComment& tag); -QImage readMP4TagCover(/*const*/ TagLib::MP4::Tag& tag); +QImage readMP4TagCover(/*const*/TagLib::MP4::Tag& tag); } //namespace Mixxx - #endif diff --git a/src/sources/audiosource.cpp b/src/sources/audiosource.cpp index f3915350db1..5746baa46bc 100644 --- a/src/sources/audiosource.cpp +++ b/src/sources/audiosource.cpp @@ -7,15 +7,15 @@ namespace Mixxx { /*static*/const AudioSource::sample_type AudioSource::kSampleValueZero = - CSAMPLE_ZERO; + CSAMPLE_ZERO; /*static*/const AudioSource::sample_type AudioSource::kSampleValuePeak = - CSAMPLE_PEAK; + CSAMPLE_PEAK; -AudioSource::AudioSource() - : m_channelCount(kChannelCountDefault), - m_frameRate(kFrameRateDefault), - m_frameCount(kFrameCountDefault), - m_bitrate(kBitrateDefault) { +AudioSource::AudioSource() : + m_channelCount(kChannelCountDefault), + m_frameRate(kFrameRateDefault), + m_frameCount(kFrameCountDefault), + m_bitrate(kBitrateDefault) { } AudioSource::~AudioSource() { @@ -39,14 +39,14 @@ void AudioSource::reset() { } AudioSource::size_type AudioSource::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, - size_type sampleBufferSize) { + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize) { DEBUG_ASSERT((sampleBufferSize / 2) >= numberOfFrames); switch (getChannelCount()) { case 1: // mono channel { const AudioSource::size_type readFrameCount = readSampleFrames( - numberOfFrames, sampleBuffer); + numberOfFrames, sampleBuffer); SampleUtil::doubleMonoToDualMono(sampleBuffer, readFrameCount); return readFrameCount; } @@ -60,23 +60,23 @@ AudioSource::size_type AudioSource::readSampleFramesStereo( if (numberOfSamplesToRead <= sampleBufferSize) { // efficient in-place transformation const AudioSource::size_type readFrameCount = readSampleFrames( - numberOfFrames, sampleBuffer); + numberOfFrames, sampleBuffer); SampleUtil::copyMultiToStereo(sampleBuffer, sampleBuffer, - readFrameCount, getChannelCount()); + readFrameCount, getChannelCount()); return readFrameCount; } else { // inefficient transformation through a temporary buffer qDebug() << "Performance warning:" - << "Allocating a temporary buffer of size" - << numberOfSamplesToRead << "for reading stereo samples." - << "The size of the provided sample buffer is" - << sampleBufferSize; + << "Allocating a temporary buffer of size" + << numberOfSamplesToRead << "for reading stereo samples." + << "The size of the provided sample buffer is" + << sampleBufferSize; typedef std::vector SampleBuffer; SampleBuffer tempBuffer(numberOfSamplesToRead); const AudioSource::size_type readFrameCount = readSampleFrames( - numberOfFrames, &tempBuffer[0]); + numberOfFrames, &tempBuffer[0]); SampleUtil::copyMultiToStereo(sampleBuffer, &tempBuffer[0], - readFrameCount, getChannelCount()); + readFrameCount, getChannelCount()); return readFrameCount; } } diff --git a/src/sources/audiosource.h b/src/sources/audiosource.h index 933214eb872..1ac5c57dc0c 100644 --- a/src/sources/audiosource.h +++ b/src/sources/audiosource.h @@ -124,8 +124,7 @@ class AudioSource { // Conversion: #samples -> #frames template inline T samples2frames(T sampleCount) const { - DEBUG_ASSERT(isChannelCountValid()); - DEBUG_ASSERT(0 == (sampleCount % getChannelCount())); + DEBUG_ASSERT(isChannelCountValid()); DEBUG_ASSERT(0 == (sampleCount % getChannelCount())); return sampleCount / getChannelCount(); } @@ -134,8 +133,7 @@ class AudioSource { // points behind of the audio data stream, but is a valid // parameter for seeking. inline bool isValidFrameIndex(diff_type frameIndex) const { - return (0 <= frameIndex) && - (getFrameCount() >= size_type(frameIndex)); + return (0 <= frameIndex) && (getFrameCount() >= size_type(frameIndex)); } // Adjusts the current frame seek index: @@ -159,8 +157,7 @@ class AudioSource { // might be lower than the requested number of frames when the end // of the audio stream has been reached. The current frame seek // position is moved forward towards the next unread frame. - virtual size_type readSampleFrames( - size_type numberOfFrames, + virtual size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) = 0; // Specialized function for explicitly reading stereo (= 2 channels) @@ -194,10 +191,8 @@ class AudioSource { // They may also have reduced space requirements on sampleBuffer, // i.e. only the minimum size is required for an in-place // transformation without temporary allocations. - virtual size_type readSampleFramesStereo( - size_type numberOfFrames, - sample_type* sampleBuffer, - size_type sampleBufferSize); + virtual size_type readSampleFramesStereo(size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize); protected: AudioSource(); diff --git a/src/sources/audiosourcecoreaudio.cpp b/src/sources/audiosourcecoreaudio.cpp index 6f31725bee8..192799fff62 100644 --- a/src/sources/audiosourcecoreaudio.cpp +++ b/src/sources/audiosourcecoreaudio.cpp @@ -6,13 +6,12 @@ namespace Mixxx { -namespace -{ +namespace { AudioSource::size_type kChannelCount = 2; } -AudioSourceCoreAudio::AudioSourceCoreAudio() - : m_headerFrames(0) { +AudioSourceCoreAudio::AudioSourceCoreAudio() : + m_headerFrames(0) { } AudioSourceCoreAudio::~AudioSourceCoreAudio() { @@ -36,20 +35,22 @@ Result AudioSourceCoreAudio::open(QString fileName) { OSStatus err; /** This code blocks works with OS X 10.5+ only. DO NOT DELETE IT for now. */ - CFStringRef urlStr = CFStringCreateWithCharacters( - 0, reinterpret_cast(fileName.unicode()), fileName.size()); - CFURLRef urlRef = CFURLCreateWithFileSystemPath(NULL, urlStr, kCFURLPOSIXPathStyle, false); + CFStringRef urlStr = CFStringCreateWithCharacters(0, + reinterpret_cast(fileName.unicode()), + fileName.size()); + CFURLRef urlRef = CFURLCreateWithFileSystemPath(NULL, urlStr, + kCFURLPOSIXPathStyle, false); err = ExtAudioFileOpenURL(urlRef, &m_audioFile); CFRelease(urlStr); CFRelease(urlRef); /** TODO: Use FSRef for compatibility with 10.4 Tiger. - Note that ExtAudioFileOpen() is deprecated above Tiger, so we must maintain - both code paths if someone finishes this part of the code. - FSRef fsRef; - CFURLGetFSRef(reinterpret_cast(url.get()), &fsRef); - err = ExtAudioFileOpen(&fsRef, &m_audioFile); - */ + Note that ExtAudioFileOpen() is deprecated above Tiger, so we must maintain + both code paths if someone finishes this part of the code. + FSRef fsRef; + CFURLGetFSRef(reinterpret_cast(url.get()), &fsRef); + err = ExtAudioFileOpen(&fsRef, &m_audioFile); + */ if (err != noErr) { qDebug() << "SSCA: Error opening file " << fileName; @@ -58,20 +59,22 @@ Result AudioSourceCoreAudio::open(QString fileName) { // get the input file format UInt32 inputFormatSize = sizeof(m_inputFormat); - err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileDataFormat, &inputFormatSize, &m_inputFormat); + err = ExtAudioFileGetProperty(m_audioFile, + kExtAudioFileProperty_FileDataFormat, &inputFormatSize, + &m_inputFormat); if (err != noErr) { qDebug() << "SSCA: Error getting file format (" << fileName << ")"; return ERR; } // create the output format - m_outputFormat = CAStreamBasicDescription( - m_inputFormat.mSampleRate, kChannelCount, - CAStreamBasicDescription::kPCMFormatFloat32, true); + m_outputFormat = CAStreamBasicDescription(m_inputFormat.mSampleRate, + kChannelCount, CAStreamBasicDescription::kPCMFormatFloat32, true); // set the client format - err = ExtAudioFileSetProperty(m_audioFile, kExtAudioFileProperty_ClientDataFormat, - sizeof(m_outputFormat), &m_outputFormat); + err = ExtAudioFileSetProperty(m_audioFile, + kExtAudioFileProperty_ClientDataFormat, sizeof(m_outputFormat), + &m_outputFormat); if (err != noErr) { qDebug() << "SSCA: Error setting file property"; return ERR; @@ -80,7 +83,9 @@ Result AudioSourceCoreAudio::open(QString fileName) { //get the total length in frames of the audio file - copypasta: http://discussions.apple.com/thread.jspa?threadID=2364583&tstart=47 SInt64 totalFrameCount; UInt32 totalFrameCountSize = sizeof(totalFrameCount); - err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_FileLengthFrames, &totalFrameCountSize, &totalFrameCount); + err = ExtAudioFileGetProperty(m_audioFile, + kExtAudioFileProperty_FileLengthFrames, &totalFrameCountSize, + &totalFrameCount); if (err != noErr) { qDebug() << "SSCA: Error getting number of frames"; return ERR; @@ -92,13 +97,15 @@ Result AudioSourceCoreAudio::open(QString fileName) { AudioConverterRef acRef; UInt32 acrsize = sizeof(AudioConverterRef); - err = ExtAudioFileGetProperty(m_audioFile, kExtAudioFileProperty_AudioConverter, &acrsize, &acRef); + err = ExtAudioFileGetProperty(m_audioFile, + kExtAudioFileProperty_AudioConverter, &acrsize, &acRef); //_ThrowExceptionIfErr(@"kExtAudioFileProperty_AudioConverter", err); AudioConverterPrimeInfo primeInfo; UInt32 piSize = sizeof(AudioConverterPrimeInfo); memset(&primeInfo, 0, piSize); - err = AudioConverterGetProperty(acRef, kAudioConverterPrimeInfo, &piSize, &primeInfo); + err = AudioConverterGetProperty(acRef, kAudioConverterPrimeInfo, &piSize, + &primeInfo); if (err != kAudioConverterErr_PropertyNotSupported) { // Only if decompressing //_ThrowExceptionIfErr(@"kAudioConverterPrimeInfo", err); m_headerFrames = primeInfo.leadingFrames; @@ -120,19 +127,20 @@ void AudioSourceCoreAudio::close() { ExtAudioFileDispose(m_audioFile); } -AudioSource::diff_type AudioSourceCoreAudio::seekSampleFrame(diff_type frameIndex) { +AudioSource::diff_type AudioSourceCoreAudio::seekSampleFrame( + diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); OSStatus err = ExtAudioFileSeek(m_audioFile, frameIndex + m_headerFrames); //_ThrowExceptionIfErr(@"ExtAudioFileSeek", err); //qDebug() << "SSCA: Seeking to" << frameIndex; if (err != noErr) { - qDebug() << "SSCA: Error seeking to" << frameIndex;// << GetMacOSStatusErrorString(err) << GetMacOSStatusCommentString(err); + qDebug() << "SSCA: Error seeking to" << frameIndex; // << GetMacOSStatusErrorString(err) << GetMacOSStatusCommentString(err); } return frameIndex; } -AudioSource::size_type AudioSourceCoreAudio::readSampleFrames(size_type numberOfFrames, - sample_type* sampleBuffer) { +AudioSource::size_type AudioSourceCoreAudio::readSampleFrames( + size_type numberOfFrames, sample_type* sampleBuffer) { //if (!m_decoder) return 0; size_type numFramesRead = 0; @@ -142,14 +150,17 @@ AudioSource::size_type AudioSourceCoreAudio::readSampleFrames(size_type numberOf AudioBufferList fillBufList; fillBufList.mNumberBuffers = 1; fillBufList.mBuffers[0].mNumberChannels = getChannelCount(); - fillBufList.mBuffers[0].mDataByteSize = frames2samples(numFramesToRead) * sizeof(sampleBuffer[0]); - fillBufList.mBuffers[0].mData = sampleBuffer + frames2samples(numFramesRead); + fillBufList.mBuffers[0].mDataByteSize = frames2samples(numFramesToRead) + * sizeof(sampleBuffer[0]); + fillBufList.mBuffers[0].mData = sampleBuffer + + frames2samples(numFramesRead); UInt32 numFramesToReadInOut = numFramesToRead; // input/output parameter - OSStatus err = ExtAudioFileRead(m_audioFile, &numFramesToReadInOut, &fillBufList); + OSStatus err = ExtAudioFileRead(m_audioFile, &numFramesToReadInOut, + &fillBufList); if (0 == numFramesToReadInOut) { // EOF - break; // done + break;// done } numFramesRead += numFramesToReadInOut; } diff --git a/src/sources/audiosourcecoreaudio.h b/src/sources/audiosourcecoreaudio.h index 9b65007d2f6..ae1fbb9c849 100644 --- a/src/sources/audiosourcecoreaudio.h +++ b/src/sources/audiosourcecoreaudio.h @@ -21,7 +21,7 @@ namespace Mixxx { -class AudioSourceCoreAudio : public AudioSource { +class AudioSourceCoreAudio: public AudioSource { public: static AudioSourcePointer create(QString fileName); diff --git a/src/sources/audiosourceflac.cpp b/src/sources/audiosourceflac.cpp index 000cd0bce43..926cd8dbaf1 100644 --- a/src/sources/audiosourceflac.cpp +++ b/src/sources/audiosourceflac.cpp @@ -5,30 +5,30 @@ #include -namespace Mixxx -{ +namespace Mixxx { -namespace -{ +namespace { // begin callbacks (have to be regular functions because normal libFLAC isn't C++-aware) FLAC__StreamDecoderReadStatus FLAC_read_cb(const FLAC__StreamDecoder*, - FLAC__byte buffer[], size_t *bytes, void *client_data) { + FLAC__byte buffer[], size_t *bytes, void *client_data) { return static_cast(client_data)->flacRead(buffer, bytes); } FLAC__StreamDecoderSeekStatus FLAC_seek_cb(const FLAC__StreamDecoder*, - FLAC__uint64 absolute_byte_offset, void *client_data) { - return static_cast(client_data)->flacSeek(absolute_byte_offset); + FLAC__uint64 absolute_byte_offset, void *client_data) { + return static_cast(client_data)->flacSeek( + absolute_byte_offset); } FLAC__StreamDecoderTellStatus FLAC_tell_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *absolute_byte_offset, void *client_data) { - return static_cast(client_data)->flacTell(absolute_byte_offset); + FLAC__uint64 *absolute_byte_offset, void *client_data) { + return static_cast(client_data)->flacTell( + absolute_byte_offset); } FLAC__StreamDecoderLengthStatus FLAC_length_cb(const FLAC__StreamDecoder*, - FLAC__uint64 *stream_length, void *client_data) { + FLAC__uint64 *stream_length, void *client_data) { return static_cast(client_data)->flacLength(stream_length); } @@ -37,34 +37,34 @@ FLAC__bool FLAC_eof_cb(const FLAC__StreamDecoder*, void *client_data) { } FLAC__StreamDecoderWriteStatus FLAC_write_cb(const FLAC__StreamDecoder*, - const FLAC__Frame *frame, const FLAC__int32 * const buffer[], - void *client_data) { + const FLAC__Frame *frame, const FLAC__int32 * const buffer[], + void *client_data) { return static_cast(client_data)->flacWrite(frame, buffer); } void FLAC_metadata_cb(const FLAC__StreamDecoder*, - const FLAC__StreamMetadata *metadata, void *client_data) { + const FLAC__StreamMetadata *metadata, void *client_data) { static_cast(client_data)->flacMetadata(metadata); } void FLAC_error_cb(const FLAC__StreamDecoder*, - FLAC__StreamDecoderErrorStatus status, void *client_data) { + FLAC__StreamDecoderErrorStatus status, void *client_data) { static_cast(client_data)->flacError(status); } // end callbacks } -AudioSourceFLAC::AudioSourceFLAC(QString fileName) - : m_file(fileName), - m_decoder(NULL), - m_minBlocksize(0), - m_maxBlocksize(0), - m_minFramesize(0), - m_maxFramesize(0), - m_sampleScale(kSampleValueZero), - m_decodeSampleBufferReadOffset(0), - m_decodeSampleBufferWriteOffset(0) { +AudioSourceFLAC::AudioSourceFLAC(QString fileName) : + m_file(fileName), + m_decoder(NULL), + m_minBlocksize(0), + m_maxBlocksize(0), + m_minFramesize(0), + m_maxFramesize(0), + m_sampleScale(kSampleValueZero), + m_decodeSampleBufferReadOffset(0), + m_decodeSampleBufferWriteOffset(0) { } AudioSourceFLAC::~AudioSourceFLAC() { @@ -72,8 +72,7 @@ AudioSourceFLAC::~AudioSourceFLAC() { } AudioSourcePointer AudioSourceFLAC::create(QString fileName) { - QSharedPointer pAudioSource( - new AudioSourceFLAC(fileName)); + QSharedPointer pAudioSource(new AudioSourceFLAC(fileName)); if (OK == pAudioSource->open()) { // success return pAudioSource; @@ -96,19 +95,16 @@ Result AudioSourceFLAC::open() { } FLAC__stream_decoder_set_md5_checking(m_decoder, FALSE); const FLAC__StreamDecoderInitStatus initStatus( - FLAC__stream_decoder_init_stream(m_decoder, FLAC_read_cb, - FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, - FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); + FLAC__stream_decoder_init_stream(m_decoder, FLAC_read_cb, + FLAC_seek_cb, FLAC_tell_cb, FLAC_length_cb, FLAC_eof_cb, + FLAC_write_cb, FLAC_metadata_cb, FLAC_error_cb, this)); if (initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) { - qWarning() - << "SSFLAC: decoder init failed:" - << initStatus; + qWarning() << "SSFLAC: decoder init failed:" << initStatus; return ERR; } if (!FLAC__stream_decoder_process_until_end_of_metadata(m_decoder)) { - qWarning() - << "SSFLAC: process to end of meta failed:" - << FLAC__stream_decoder_get_state(m_decoder); + qWarning() << "SSFLAC: process to end of meta failed:" + << FLAC__stream_decoder_get_state(m_decoder); return ERR; } @@ -126,7 +122,8 @@ void AudioSourceFLAC::close() { reset(); } -Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame(diff_type frameIndex) { +Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame( + diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); // clear decode buffer before seeking m_decodeSampleBufferReadOffset = 0; @@ -139,77 +136,78 @@ Mixxx::AudioSource::diff_type AudioSourceFLAC::seekSampleFrame(diff_type frameIn } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); + size_type numberOfFrames, sample_type* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, + frames2samples(numberOfFrames), false); } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { - return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize) { + return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, + true); } Mixxx::AudioSource::size_type AudioSourceFLAC::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer, - size_type sampleBufferSize, - bool readStereoSamples) { + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize, bool readStereoSamples) { sample_type* outBuffer = sampleBuffer; const size_type numberOfFramesTotal = math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; while (numberOfFramesTotal > numberOfFramesRead) { DEBUG_ASSERT( - m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); // if our buffer from libflac is empty (either because we explicitly cleared // it or because we've simply used all the samples), ask for a new buffer if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { if (FLAC__stream_decoder_process_single(m_decoder)) { - if (m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset) { + if (m_decodeSampleBufferReadOffset + >= m_decodeSampleBufferWriteOffset) { // EOF break; } } else { - qWarning() - << "SSFLAC: decoder_process_single returned false (" - << m_file.fileName() << ")"; + qWarning() << "SSFLAC: decoder_process_single returned false (" + << m_file.fileName() << ")"; break; } - } - DEBUG_ASSERT( - m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + } DEBUG_ASSERT( + m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); const size_type decodeBufferSamples = m_decodeSampleBufferWriteOffset - m_decodeSampleBufferReadOffset; const size_type decodeBufferFrames = samples2frames( decodeBufferSamples); const size_type framesToCopy = - math_min(decodeBufferFrames, numberOfFramesTotal - numberOfFramesRead); + math_min(decodeBufferFrames, numberOfFramesTotal - numberOfFramesRead); const size_type samplesToCopy = frames2samples(framesToCopy); if (readStereoSamples && !isChannelCountStereo()) { if (isChannelCountMono()) { SampleUtil::copyMonoToDualMono(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy); + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy); } else { SampleUtil::copyMultiToStereo(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - framesToCopy, getChannelCount()); + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + framesToCopy, getChannelCount()); } outBuffer += framesToCopy * 2; // copied 2 samples per frame } else { SampleUtil::copy(outBuffer, - &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], - samplesToCopy); + &m_decodeSampleBuffer[m_decodeSampleBufferReadOffset], + samplesToCopy); outBuffer += samplesToCopy; } m_decodeSampleBufferReadOffset += samplesToCopy; numberOfFramesRead += framesToCopy; DEBUG_ASSERT( - m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); } return numberOfFramesRead; } // flac callback methods -FLAC__StreamDecoderReadStatus AudioSourceFLAC::flacRead( - FLAC__byte buffer[], size_t *bytes) { +FLAC__StreamDecoderReadStatus AudioSourceFLAC::flacRead(FLAC__byte buffer[], + size_t *bytes) { *bytes = m_file.read((char*) buffer, *bytes); if (*bytes > 0) { return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; @@ -239,7 +237,7 @@ FLAC__StreamDecoderTellStatus AudioSourceFLAC::flacTell(FLAC__uint64 *offset) { } FLAC__StreamDecoderLengthStatus AudioSourceFLAC::flacLength( - FLAC__uint64 *length) { + FLAC__uint64 *length) { if (m_file.isSequential()) { return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; } @@ -255,35 +253,30 @@ FLAC__bool AudioSourceFLAC::flacEOF() { } FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( - const FLAC__Frame *frame, const FLAC__int32 * const buffer[]) { + const FLAC__Frame *frame, const FLAC__int32 * const buffer[]) { // decode buffer must be empty before decoding the next frame DEBUG_ASSERT(m_decodeSampleBufferReadOffset >= m_decodeSampleBufferWriteOffset); // reset decode buffer m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; if (getChannelCount() != frame->header.channels) { - qWarning() - << "Corrupt or unsupported FLAC file:" - << "Invalid number of channels in FLAC frame header" - << frame->header.channels << "<>" - << getChannelCount(); + qWarning() << "Corrupt or unsupported FLAC file:" + << "Invalid number of channels in FLAC frame header" + << frame->header.channels << "<>" << getChannelCount(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } if (getFrameRate() != frame->header.sample_rate) { - qWarning() - << "Corrupt or unsupported FLAC file:" - << "Invalid sample rate in FLAC frame header" - << frame->header.sample_rate << "<>" - << getFrameRate(); + qWarning() << "Corrupt or unsupported FLAC file:" + << "Invalid sample rate in FLAC frame header" + << frame->header.sample_rate << "<>" << getFrameRate(); return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; } - const SampleBuffer::size_type maxBlocksize = samples2frames(m_decodeSampleBuffer.size()); + const SampleBuffer::size_type maxBlocksize = samples2frames( + m_decodeSampleBuffer.size()); if (maxBlocksize < frame->header.blocksize) { - qWarning() - << "Corrupt or unsupported FLAC file:" - << "Block size in FLAC frame header exceeds the maximum block size" - << frame->header.blocksize << ">" - << maxBlocksize; + qWarning() << "Corrupt or unsupported FLAC file:" + << "Block size in FLAC frame header exceeds the maximum block size" + << frame->header.blocksize << ">" << maxBlocksize; } switch (getChannelCount()) { case 1: { @@ -291,7 +284,7 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( DEBUG_ASSERT(1 <= frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[0][i] * m_sampleScale; + buffer[0][i] * m_sampleScale; } break; } @@ -300,9 +293,9 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( DEBUG_ASSERT(2 <= frame->header.channels); for (unsigned i = 0; i < frame->header.blocksize; ++i) { m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[0][i] * m_sampleScale; + buffer[0][i] * m_sampleScale; m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[1][i] * m_sampleScale; + buffer[1][i] * m_sampleScale; } break; } @@ -312,12 +305,11 @@ FLAC__StreamDecoderWriteStatus AudioSourceFLAC::flacWrite( for (unsigned i = 0; i < frame->header.blocksize; ++i) { for (unsigned j = 0; j < frame->header.channels; ++j) { m_decodeSampleBuffer[m_decodeSampleBufferWriteOffset++] = - buffer[j][i] * m_sampleScale; + buffer[j][i] * m_sampleScale; } } } - } - DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); + } DEBUG_ASSERT(m_decodeSampleBufferReadOffset <= m_decodeSampleBufferWriteOffset); return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; } @@ -327,27 +319,21 @@ void AudioSourceFLAC::flacMetadata(const FLAC__StreamMetadata *metadata) { setChannelCount(metadata->data.stream_info.channels); setFrameRate(metadata->data.stream_info.sample_rate); setFrameCount(metadata->data.stream_info.total_samples); - m_sampleScale = kSampleValuePeak / sample_type( - FLAC__int32(1) << metadata->data.stream_info.bits_per_sample); - qDebug() - << "FLAC file " - << m_file.fileName(); - qDebug() - << getChannelCount() << " ch," - << getFrameRate() << " Hz," - << getFrameCount() << " total," - << "bit depth" - << metadata->data.stream_info.bits_per_sample; + m_sampleScale = kSampleValuePeak + / sample_type( + FLAC__int32(1) + << metadata->data.stream_info.bits_per_sample); + qDebug() << "FLAC file " << m_file.fileName(); + qDebug() << getChannelCount() << " ch," << getFrameRate() << " Hz," + << getFrameCount() << " total," << "bit depth" + << metadata->data.stream_info.bits_per_sample; m_minBlocksize = metadata->data.stream_info.min_blocksize; m_maxBlocksize = metadata->data.stream_info.max_blocksize; m_minFramesize = metadata->data.stream_info.min_framesize; m_maxFramesize = metadata->data.stream_info.max_framesize; - qDebug() - << "Blocksize in [" - << m_minBlocksize << "," << m_maxBlocksize - << "], Framesize in [" - << m_minFramesize << "," << m_maxFramesize - << "]"; + qDebug() << "Blocksize in [" << m_minBlocksize << "," << m_maxBlocksize + << "], Framesize in [" << m_minFramesize << "," + << m_maxFramesize << "]"; m_decodeSampleBufferReadOffset = 0; m_decodeSampleBufferWriteOffset = 0; m_decodeSampleBuffer.resize(m_maxBlocksize * getChannelCount()); @@ -375,9 +361,8 @@ void AudioSourceFLAC::flacError(FLAC__StreamDecoderErrorStatus status) { error = "STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM"; break; } - qWarning() - << "SSFLAC got error" << error - << "from libFLAC for file" << m_file.fileName(); + qWarning() << "SSFLAC got error" << error << "from libFLAC for file" + << m_file.fileName(); // not much else to do here... whatever function that initiated whatever // decoder method resulted in this error will return an error, and the caller // will bail. libFLAC docs say to not close the decoder here -- bkgood diff --git a/src/sources/audiosourceflac.h b/src/sources/audiosourceflac.h index 29afabe3978..4f2b35c92fd 100644 --- a/src/sources/audiosourceflac.h +++ b/src/sources/audiosourceflac.h @@ -10,10 +10,9 @@ #include -namespace Mixxx -{ +namespace Mixxx { -class AudioSourceFLAC : public AudioSource { +class AudioSourceFLAC: public AudioSource { public: static AudioSourcePointer create(QString fileName); @@ -21,8 +20,10 @@ class AudioSourceFLAC : public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; // callback methods FLAC__StreamDecoderReadStatus flacRead(FLAC__byte buffer[], size_t *bytes); @@ -30,7 +31,8 @@ class AudioSourceFLAC : public AudioSource { FLAC__StreamDecoderTellStatus flacTell(FLAC__uint64 *offset); FLAC__StreamDecoderLengthStatus flacLength(FLAC__uint64 *length); FLAC__bool flacEOF(); - FLAC__StreamDecoderWriteStatus flacWrite(const FLAC__Frame *frame, const FLAC__int32 *const buffer[]); + FLAC__StreamDecoderWriteStatus flacWrite(const FLAC__Frame *frame, + const FLAC__int32 * const buffer[]); void flacMetadata(const FLAC__StreamMetadata *metadata); void flacError(FLAC__StreamDecoderErrorStatus status); @@ -41,7 +43,9 @@ class AudioSourceFLAC : public AudioSource { void close(); - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize, + bool readStereoSamples); QFile m_file; diff --git a/src/sources/audiosourcemodplug.cpp b/src/sources/audiosourcemodplug.cpp index 5e8f9a47831..7499a15e4dd 100644 --- a/src/sources/audiosourcemodplug.cpp +++ b/src/sources/audiosourcemodplug.cpp @@ -11,12 +11,10 @@ /* read files in 512k chunks */ #define CHUNKSIZE (1 << 18) -namespace Mixxx -{ +namespace Mixxx { // reserve some static space for settings... -namespace -{ +namespace { // identification of modplug module type enum ModuleTypes { NONE = 0x00, @@ -32,15 +30,16 @@ enum ModuleTypes { unsigned int AudioSourceModPlug::s_bufferSizeLimit = 0; -void AudioSourceModPlug::configure( - unsigned int bufferSizeLimit, +void AudioSourceModPlug::configure(unsigned int bufferSizeLimit, const ModPlug::ModPlug_Settings &settings) { s_bufferSizeLimit = bufferSizeLimit; ModPlug::ModPlug_SetSettings(&settings); } -AudioSourceModPlug::AudioSourceModPlug() - : m_pModFile(NULL), m_fileLength(0), m_seekPos(0) { +AudioSourceModPlug::AudioSourceModPlug() : + m_pModFile(NULL), + m_fileLength(0), + m_seekPos(0) { } AudioSourceModPlug::~AudioSourceModPlug() { @@ -89,11 +88,11 @@ Result AudioSourceModPlug::open(QString fileName) { // (((milliseconds << 1) >> 10 /* to seconds */) // div 11 /* samples to chunksize ratio */) // << 19 /* align to chunksize */ - unsigned int estimate = ((ModPlug::ModPlug_GetLength(m_pModFile) >> 8) / 11) << 18; + unsigned int estimate = ((ModPlug::ModPlug_GetLength(m_pModFile) >> 8) / 11) + << 18; estimate = math_min(estimate, s_bufferSizeLimit); m_sampleBuf.reserve(estimate); - qDebug() << "[ModPlug] Reserved " << m_sampleBuf.capacity() - << " #samples"; + qDebug() << "[ModPlug] Reserved " << m_sampleBuf.capacity() << " #samples"; // decode samples to sample buffer int samplesRead = -1; @@ -146,7 +145,8 @@ AudioSource::size_type AudioSourceModPlug::readSampleFrames( const size_type readSamples = frames2samples(readFrames); const size_type readOffset = frames2samples(m_seekPos); for (size_type i = 0; i < readSamples; ++i) { - sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) / sample_type(SAMPLE_MAX); + sampleBuffer[i] = SAMPLE_clampSymmetric(m_sampleBuf[readOffset + i]) + / sample_type(SAMPLE_MAX); } m_seekPos += readFrames; diff --git a/src/sources/audiosourcemodplug.h b/src/sources/audiosourcemodplug.h index fef79b7d113..04dd22c20c6 100644 --- a/src/sources/audiosourcemodplug.h +++ b/src/sources/audiosourcemodplug.h @@ -10,8 +10,7 @@ namespace ModPlug { #include -namespace Mixxx -{ +namespace Mixxx { // Class for reading tracker files using libmodplug. // The whole file is decoded at once and saved @@ -31,7 +30,8 @@ class AudioSourceModPlug: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer) /*override*/; private: static unsigned int s_bufferSizeLimit; // max track buffer length (bytes) diff --git a/src/sources/audiosourcemp3.cpp b/src/sources/audiosourcemp3.cpp index a8034b0b642..01b852d5b89 100644 --- a/src/sources/audiosourcemp3.cpp +++ b/src/sources/audiosourcemp3.cpp @@ -4,54 +4,48 @@ #include -namespace Mixxx -{ +namespace Mixxx { namespace { - // In the worst case up to 29 MP3 frames need to be prefetched: - // http://www.mars.org/mailman/public/mad-dev/2002-May/000634.html - const AudioSource::size_type kSeekFramePrefetchCount = - 4; // required for synchronization +// In the worst case up to 29 MP3 frames need to be prefetched: +// http://www.mars.org/mailman/public/mad-dev/2002-May/000634.html +const AudioSource::size_type kSeekFramePrefetchCount = 4; // required for synchronization - const AudioSource::sample_type kMadScale = - AudioSource::kSampleValuePeak / AudioSource::sample_type(MAD_F_ONE); +const AudioSource::sample_type kMadScale = AudioSource::kSampleValuePeak + / AudioSource::sample_type(MAD_F_ONE); - inline - AudioSource::sample_type madScale(mad_fixed_t sample) { - return sample * kMadScale; - } +inline AudioSource::sample_type madScale(mad_fixed_t sample) { + return sample * kMadScale; +} - // Optimization: Reserve initial capacity for seek frame list - const AudioSource::size_type kMinutesPerFile = - 10; // enough for the majority of files (tunable) - const AudioSource::size_type kSecondsPerMinute = - 60; // fixed - const AudioSource::size_type kMaxMp3FramesPerSecond = - 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 - const AudioSource::size_type kSeekFrameListCapacity = - kMinutesPerFile * kSecondsPerMinute * kMaxMp3FramesPerSecond; - - bool mad_skip_id3_tag(mad_stream* pStream) { - long tagsize = id3_tag_query( - pStream->this_frame, pStream->bufend - pStream->this_frame); - if (0 < tagsize) { - mad_stream_skip(pStream, tagsize); - return true; - } else { - return false; - } +// Optimization: Reserve initial capacity for seek frame list +const AudioSource::size_type kMinutesPerFile = 10; // enough for the majority of files (tunable) +const AudioSource::size_type kSecondsPerMinute = 60; // fixed +const AudioSource::size_type kMaxMp3FramesPerSecond = 39; // fixed: 1 MP3 frame = 26 ms -> ~ 1000 / 26 +const AudioSource::size_type kSeekFrameListCapacity = kMinutesPerFile + * kSecondsPerMinute * kMaxMp3FramesPerSecond; + +bool mad_skip_id3_tag(mad_stream* pStream) { + long tagsize = id3_tag_query(pStream->this_frame, + pStream->bufend - pStream->this_frame); + if (0 < tagsize) { + mad_stream_skip(pStream, tagsize); + return true; + } else { + return false; } +} } // anonymous namespace -AudioSourceMp3::AudioSourceMp3(QString fileName) - : m_file(fileName), - m_fileSize(0), - m_pFileData(NULL), - m_avgSeekFrameCount(0), - m_curFrameIndex(0), - m_madSynthCount(0) { +AudioSourceMp3::AudioSourceMp3(QString fileName) : + m_file(fileName), + m_fileSize(0), + m_pFileData(NULL), + m_avgSeekFrameCount(0), + m_curFrameIndex(0), + m_madSynthCount(0) { initDecoding(); m_seekFrameList.reserve(kSeekFrameListCapacity); } @@ -61,8 +55,7 @@ AudioSourceMp3::~AudioSourceMp3() { } AudioSourcePointer AudioSourceMp3::create(QString fileName) { - QSharedPointer pAudioSource( - new AudioSourceMp3(fileName)); + QSharedPointer pAudioSource(new AudioSourceMp3(fileName)); if (OK == pAudioSource->open()) { // success return pAudioSource; @@ -100,20 +93,17 @@ Result AudioSourceMp3::open() { // Ignore LOSTSYNC due to ID3 tags mad_skip_id3_tag(&m_madStream); } else { - qDebug() - << "Recoverable MP3 header error:" - << mad_stream_errorstr(&m_madStream); - } - mad_header_finish(&madHeader); + qDebug() << "Recoverable MP3 header error:" + << mad_stream_errorstr(&m_madStream); + } mad_header_finish(&madHeader); continue; } else { if (MAD_ERROR_BUFLEN == m_madStream.error) { // EOF - break; // done + break;// done } else { - qWarning() - << "Unrecoverable MP3 header error:" - << mad_stream_errorstr(&m_madStream); + qWarning() << "Unrecoverable MP3 header error:" + << mad_stream_errorstr(&m_madStream); mad_header_finish(&madHeader); return ERR; // abort } @@ -128,10 +118,9 @@ Result AudioSourceMp3::open() { } else { // check for consistent number of channels if ((0 < madChannelCount) && getChannelCount() != madChannelCount) { - qWarning() - << "Differing number of channels in some headers:" - << m_file.fileName() << getChannelCount() << "<>" - << madChannelCount; + qWarning() << "Differing number of channels in some headers:" + << m_file.fileName() << getChannelCount() << "<>" + << madChannelCount; } } const size_type madSampleRate = madHeader.samplerate; @@ -166,18 +155,17 @@ Result AudioSourceMp3::open() { madUnits = MAD_UNITS_48000_HZ; break; default: - qWarning() << "Invalid sample rate:" - << m_file.fileName() << madSampleRate; + qWarning() << "Invalid sample rate:" << m_file.fileName() + << madSampleRate; return ERR; // abort } setFrameRate(madSampleRate); } else { // check for consistent frame/sample rate if ((0 < madSampleRate) && (getFrameRate() != madSampleRate)) { - qWarning() - << "Differing sample rate in some headers:" - << m_file.fileName() - << getFrameRate() << "<>" << madSampleRate; + qWarning() << "Differing sample rate in some headers:" + << m_file.fileName() << getFrameRate() << "<>" + << madSampleRate; return ERR; // abort } } @@ -203,8 +191,8 @@ Result AudioSourceMp3::open() { setBitrate(avgBitrate / 1000); } else { // This is not a working MP3 file. - qWarning() - << "SSMP3: This is not a working MP3 file:" << m_file.fileName(); + qWarning() << "SSMP3: This is not a working MP3 file:" + << m_file.fileName(); return ERR; // abort } @@ -232,9 +220,8 @@ void AudioSourceMp3::finishDecoding() { void AudioSourceMp3::restartDecoding(const SeekFrameType& seekFrame) { m_madSynthCount = 0; - mad_stream_buffer( - &m_madStream, - seekFrame.pFileData, m_fileSize - (seekFrame.pFileData - m_pFileData)); + mad_stream_buffer(&m_madStream, seekFrame.pFileData, + m_fileSize - (seekFrame.pFileData - m_pFileData)); m_curFrameIndex = seekFrame.frameIndex; // Calling mad_synth_mute() and mad_frame_mute() is not // necessary, because we will prefetch (decode and skip) @@ -257,47 +244,47 @@ void AudioSourceMp3::close() { reset(); } -AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex(diff_type frameIndex) const { - DEBUG_ASSERT(!m_seekFrameList.empty()); - DEBUG_ASSERT(0 < m_avgSeekFrameCount); +AudioSourceMp3::SeekFrameList::size_type AudioSourceMp3::findSeekFrameIndex( + diff_type frameIndex) const { + DEBUG_ASSERT(!m_seekFrameList.empty()); DEBUG_ASSERT(0 < m_avgSeekFrameCount); // Guess position of frame in m_seekFrameList based on average frame size - AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = - frameIndex / m_avgSeekFrameCount; + AudioSourceMp3::SeekFrameList::size_type seekFrameIndex = frameIndex + / m_avgSeekFrameCount; if (seekFrameIndex >= m_seekFrameList.size()) { seekFrameIndex = m_seekFrameList.size() - 1; } // binary search starting at seekFrameIndex AudioSourceMp3::SeekFrameList::size_type lowerBound = 0; - AudioSourceMp3::SeekFrameList::size_type upperBound = m_seekFrameList.size(); + AudioSourceMp3::SeekFrameList::size_type upperBound = + m_seekFrameList.size(); while ((upperBound - lowerBound) > 1) { - DEBUG_ASSERT(seekFrameIndex >= lowerBound); - DEBUG_ASSERT(seekFrameIndex < upperBound); + DEBUG_ASSERT(seekFrameIndex >= lowerBound); DEBUG_ASSERT(seekFrameIndex < upperBound); if (m_seekFrameList[seekFrameIndex].frameIndex > frameIndex) { upperBound = seekFrameIndex; } else { lowerBound = seekFrameIndex; } seekFrameIndex = lowerBound + (upperBound - lowerBound) / 2; - } - DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); - DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); - DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || - (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); + } DEBUG_ASSERT(m_seekFrameList.size() > seekFrameIndex); DEBUG_ASSERT(m_seekFrameList[seekFrameIndex].frameIndex <= frameIndex); DEBUG_ASSERT(((seekFrameIndex + 1) >= m_seekFrameList.size()) || + (m_seekFrameList[seekFrameIndex + 1].frameIndex > frameIndex)); return seekFrameIndex; } AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { - //qDebug() << "seekSampleFrame(): frameIndex =" << frameIndex; +// qDebug() << "seekSampleFrame(): frameIndex =" << frameIndex; DEBUG_ASSERT(isValidFrameIndex(frameIndex)); if (!m_seekFrameList.empty()) { - const SeekFrameList::size_type curSeekFrameIndex = findSeekFrameIndex(m_curFrameIndex); - //qDebug() << "curSeekFrameIndex" << curSeekFrameIndex; + const SeekFrameList::size_type curSeekFrameIndex = findSeekFrameIndex( + m_curFrameIndex); +// qDebug() << "curSeekFrameIndex" << curSeekFrameIndex; DEBUG_ASSERT(m_seekFrameList.size() > curSeekFrameIndex); - SeekFrameList::size_type nextSeekFrameIndex = findSeekFrameIndex(frameIndex); + SeekFrameList::size_type nextSeekFrameIndex = findSeekFrameIndex( + frameIndex); DEBUG_ASSERT(m_seekFrameList.size() > nextSeekFrameIndex); - //qDebug() << "nextSeekFrameIndex" << nextSeekFrameIndex; +// qDebug() << "nextSeekFrameIndex" << nextSeekFrameIndex; if ((frameIndex < m_curFrameIndex) || // seeking backwards? - (nextSeekFrameIndex > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jumping forward? + (nextSeekFrameIndex + > (curSeekFrameIndex + kSeekFramePrefetchCount))) { // jumping forward? // Implementation note: The type size_type is unsigned so we // need to be careful when subtracting! if (nextSeekFrameIndex <= kSeekFramePrefetchCount) { @@ -305,7 +292,7 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { } else { nextSeekFrameIndex -= kSeekFramePrefetchCount; } - //qDebug() << "restarting at nextSeekFrameIndex" << nextSeekFrameIndex; +// qDebug() << "restarting at nextSeekFrameIndex" << nextSeekFrameIndex; restartDecoding(m_seekFrameList[nextSeekFrameIndex]); DEBUG_ASSERT(findSeekFrameIndex(m_curFrameIndex) == nextSeekFrameIndex); } @@ -320,30 +307,29 @@ AudioSource::diff_type AudioSourceMp3::seekSampleFrame(diff_type frameIndex) { } AudioSource::size_type AudioSourceMp3::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); + size_type numberOfFrames, sample_type* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, + frames2samples(numberOfFrames), false); } AudioSource::size_type AudioSourceMp3::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { - return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize) { + return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, + true); } AudioSource::size_type AudioSourceMp3::readSampleFrames( - size_type numberOfFrames, - sample_type* sampleBuffer, - size_type sampleBufferSize, - bool readStereoSamples) { - /* - qDebug() << "readSampleFrames():" - << "m_curFrameIndex =" << m_curFrameIndex - << "numberOfFrames =" << numberOfFrames; - */ + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize, bool readStereoSamples) { +// qDebug() << "readSampleFrames():" +// << "m_curFrameIndex =" << m_curFrameIndex +// << "numberOfFrames =" << numberOfFrames; DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex)); sample_type* pSampleBuffer = sampleBuffer; const size_type numberOfFramesTotal = - math_min(numberOfFrames, samples2frames(sampleBufferSize)); + math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; while (numberOfFramesTotal > numberOfFramesRead) { if (0 >= m_madSynthCount) { @@ -352,11 +338,10 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( const unsigned char* const madThisFrame = m_madStream.this_frame; if (0 != mad_frame_decode(&m_madFrame, &m_madStream)) { if (MAD_RECOVERABLE(m_madStream.error)) { - if ((NULL != pSampleBuffer) || - (MAD_ERROR_BADDATAPTR != m_madStream.error)) { - qDebug() - << "Recoverable MP3 decoding error:" - << mad_stream_errorstr(&m_madStream); + if ((NULL != pSampleBuffer) + || (MAD_ERROR_BADDATAPTR != m_madStream.error)) { + qDebug() << "Recoverable MP3 decoding error:" + << mad_stream_errorstr(&m_madStream); } if (MAD_ERROR_LOSTSYNC == m_madStream.error) { // Ignore LOSTSYNC due to ID3 tags @@ -366,48 +351,44 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( continue; } else { DEBUG_ASSERT(MAD_ERROR_BUFLEN != m_madStream.error); - qWarning() - << "Unrecoverable MP3 decoding error:" - << mad_stream_errorstr(&m_madStream); + qWarning() << "Unrecoverable MP3 decoding error:" + << mad_stream_errorstr(&m_madStream); break; } } else { // if not at the beginning of the stream // the next MP3 frame must have been decoded DEBUG_ASSERT((0 == m_curFrameIndex) || - (madThisFrame != m_madStream.this_frame)); + (madThisFrame != m_madStream.this_frame)); } const mad_header* const pMadFrameHeader = &m_madFrame.header; if (getChannelCount() != MAD_NCHANNELS(pMadFrameHeader)) { - qWarning() - << "Corrupt or unsupported MP3 file:" - << "Invalid number of channels in MP3 frame header" - << MAD_NCHANNELS(pMadFrameHeader) << "<>" - << getChannelCount(); + qWarning() << "Corrupt or unsupported MP3 file:" + << "Invalid number of channels in MP3 frame header" + << MAD_NCHANNELS(pMadFrameHeader) << "<>" + << getChannelCount(); break; } // Once decoded the frame is synthesized to PCM samples mad_synth_frame(&m_madSynth, &m_madFrame); m_madSynthCount = m_madSynth.pcm.length; if (getFrameRate() != m_madSynth.pcm.samplerate) { - qWarning() - << "Corrupt or unsupported MP3 file:" - << "Invalid sample rate in MP3 frame header" - << m_madSynth.pcm.samplerate << "<>" - << getFrameRate(); + qWarning() << "Corrupt or unsupported MP3 file:" + << "Invalid sample rate in MP3 frame header" + << m_madSynth.pcm.samplerate << "<>" << getFrameRate(); break; } } const size_type framesRead = math_min( - m_madSynthCount, - numberOfFramesTotal - numberOfFramesRead); + m_madSynthCount, + numberOfFramesTotal - numberOfFramesRead); if (NULL != pSampleBuffer) { - const size_type madSynthOffset = - m_madSynth.pcm.length - m_madSynthCount; + const size_type madSynthOffset = m_madSynth.pcm.length + - m_madSynthCount; if (isChannelCountMono()) { for (size_type i = 0; i < framesRead; ++i) { const sample_type sampleValue = madScale( - m_madSynth.pcm.samples[0][madSynthOffset + i]); + m_madSynth.pcm.samples[0][madSynthOffset + i]); *(pSampleBuffer++) = sampleValue; if (readStereoSamples) { *(pSampleBuffer++) = sampleValue; @@ -416,15 +397,15 @@ AudioSource::size_type AudioSourceMp3::readSampleFrames( } else if (isChannelCountStereo() || readStereoSamples) { for (size_type i = 0; i < framesRead; ++i) { *(pSampleBuffer++) = madScale( - m_madSynth.pcm.samples[0][madSynthOffset + i]); + m_madSynth.pcm.samples[0][madSynthOffset + i]); *(pSampleBuffer++) = madScale( - m_madSynth.pcm.samples[1][madSynthOffset + i]); + m_madSynth.pcm.samples[1][madSynthOffset + i]); } } else { for (size_type i = 0; i < framesRead; ++i) { for (size_type j = 0; j < getChannelCount(); ++j) { *(pSampleBuffer++) = madScale( - m_madSynth.pcm.samples[j][madSynthOffset + i]); + m_madSynth.pcm.samples[j][madSynthOffset + i]); } } } diff --git a/src/sources/audiosourcemp3.h b/src/sources/audiosourcemp3.h index e230953ad54..578444287ec 100644 --- a/src/sources/audiosourcemp3.h +++ b/src/sources/audiosourcemp3.h @@ -5,10 +5,10 @@ #include "util/defs.h" #ifdef _MSC_VER - // So mad.h doesn't try to use inline assembly which MSVC doesn't support. - // Notably, FPM_64BIT does not require a 64-bit machine. It merely requires a - // compiler that supports 64-bit types. - #define FPM_64BIT +// So mad.h doesn't try to use inline assembly which MSVC doesn't support. +// Notably, FPM_64BIT does not require a 64-bit machine. It merely requires a +// compiler that supports 64-bit types. +#define FPM_64BIT #endif #include @@ -16,10 +16,9 @@ #include -namespace Mixxx -{ +namespace Mixxx { -class AudioSourceMp3 : public AudioSource { +class AudioSourceMp3: public AudioSource { public: static AudioSourcePointer create(QString fileName); @@ -27,8 +26,10 @@ class AudioSourceMp3 : public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; private: explicit AudioSourceMp3(QString fileName); @@ -40,7 +41,9 @@ class AudioSourceMp3 : public AudioSource { inline size_type skipFrameSamples(size_type numberOfFrames) { return readSampleFrames(numberOfFrames, NULL); } - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize, bool readStereoSamples); + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize, + bool readStereoSamples); QFile m_file; quint64 m_fileSize; diff --git a/src/sources/audiosourceoggvorbis.cpp b/src/sources/audiosourceoggvorbis.cpp index ec8bcc2fa0d..5fe91801b20 100644 --- a/src/sources/audiosourceoggvorbis.cpp +++ b/src/sources/audiosourceoggvorbis.cpp @@ -1,7 +1,6 @@ #include "sources/audiosourceoggvorbis.h" -namespace Mixxx -{ +namespace Mixxx { AudioSourceOggVorbis::AudioSourceOggVorbis() { memset(&m_vf, 0, sizeof(m_vf)); @@ -12,8 +11,7 @@ AudioSourceOggVorbis::~AudioSourceOggVorbis() { } AudioSourcePointer AudioSourceOggVorbis::create(QString fileName) { - QSharedPointer pAudioSource( - new AudioSourceOggVorbis); + QSharedPointer pAudioSource(new AudioSourceOggVorbis); if (OK == pAudioSource->open(fileName)) { // success return pAudioSource; @@ -64,7 +62,7 @@ void AudioSourceOggVorbis::close() { } AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( - diff_type frameIndex) { + diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); const int seekResult = ov_pcm_seek(&m_vf, frameIndex); if (0 != seekResult) { @@ -74,21 +72,21 @@ AudioSource::diff_type AudioSourceOggVorbis::seekSampleFrame( } AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { - return readSampleFrames(numberOfFrames, sampleBuffer, frames2samples(numberOfFrames), false); + size_type numberOfFrames, sample_type* sampleBuffer) { + return readSampleFrames(numberOfFrames, sampleBuffer, + frames2samples(numberOfFrames), false); } AudioSource::size_type AudioSourceOggVorbis::readSampleFramesStereo( - size_type numberOfFrames, - sample_type* sampleBuffer, - size_type sampleBufferSize) { - return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, true); + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize) { + return readSampleFrames(numberOfFrames, sampleBuffer, sampleBufferSize, + true); } AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( - size_type numberOfFrames, - sample_type* sampleBuffer, size_type sampleBufferSize, - bool readStereoSamples) { + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize, bool readStereoSamples) { DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); sample_type* nextSample = sampleBuffer; @@ -98,10 +96,10 @@ AudioSource::size_type AudioSourceOggVorbis::readSampleFrames( float** pcmChannels; int currentSection; const long readResult = ov_read_float(&m_vf, &pcmChannels, - numberOfFramesTotal - numberOfFramesRead, ¤tSection); + numberOfFramesTotal - numberOfFramesRead, ¤tSection); if (0 == readResult) { // EOF - break; // done + break;// done } if (0 < readResult) { if (isChannelCountMono()) { diff --git a/src/sources/audiosourceoggvorbis.h b/src/sources/audiosourceoggvorbis.h index 207aa68a9d1..394d7c6ef0a 100644 --- a/src/sources/audiosourceoggvorbis.h +++ b/src/sources/audiosourceoggvorbis.h @@ -7,8 +7,7 @@ #define OV_EXCLUDE_STATIC_CALLBACKS #include -namespace Mixxx -{ +namespace Mixxx { class AudioSourceOggVorbis: public AudioSource { public: @@ -18,8 +17,10 @@ class AudioSourceOggVorbis: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; private: AudioSourceOggVorbis(); diff --git a/src/sources/audiosourceopus.cpp b/src/sources/audiosourceopus.cpp index fd67dcc1c86..bcc80d764fe 100644 --- a/src/sources/audiosourceopus.cpp +++ b/src/sources/audiosourceopus.cpp @@ -1,10 +1,9 @@ #include "sources/audiosourceopus.h" -namespace Mixxx -{ +namespace Mixxx { -AudioSourceOpus::AudioSourceOpus() - : m_pOggOpusFile(NULL) { +AudioSourceOpus::AudioSourceOpus() : + m_pOggOpusFile(NULL) { } AudioSourceOpus::~AudioSourceOpus() { @@ -28,9 +27,8 @@ Result AudioSourceOpus::open(QString fileName) { const QByteArray qbaFilename(fileName.toLocal8Bit()); m_pOggOpusFile = op_open_file(qbaFilename.constData(), &errorCode); if (!m_pOggOpusFile) { - qDebug() - << "Failed to open OggOpus file:" << fileName - << "errorCode" << errorCode; + qDebug() << "Failed to open OggOpus file:" << fileName << "errorCode" + << errorCode; return ERR; } @@ -77,18 +75,17 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( size_type readCount = 0; while (readCount < numberOfFrames) { int readResult = op_read_float(m_pOggOpusFile, - sampleBuffer + frames2samples(readCount), - frames2samples(numberOfFrames - readCount), NULL); + sampleBuffer + frames2samples(readCount), + frames2samples(numberOfFrames - readCount), NULL); if (0 == readResult) { // EOF - break; // done + break;// done } if (0 < readResult) { readCount += readResult; } else { - qWarning() - << "Failed to read sample data from OggOpus file:" - << readResult; + qWarning() << "Failed to read sample data from OggOpus file:" + << readResult; break; // abort } } @@ -96,26 +93,26 @@ AudioSource::size_type AudioSourceOpus::readSampleFrames( } AudioSource::size_type AudioSourceOpus::readSampleFramesStereo( - size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) { + size_type numberOfFrames, sample_type* sampleBuffer, + size_type sampleBufferSize) { DEBUG_ASSERT(isValidFrameIndex(getCurrentFrameIndex())); const size_type numberOfFramesTotal = - math_min(numberOfFrames, samples2frames(sampleBufferSize)); + math_min(numberOfFrames, samples2frames(sampleBufferSize)); size_type numberOfFramesRead = 0; while (numberOfFramesTotal > numberOfFramesRead) { int readResult = op_read_float_stereo(m_pOggOpusFile, - sampleBuffer + (numberOfFramesRead * 2), - (numberOfFramesTotal - numberOfFramesRead) * 2); + sampleBuffer + (numberOfFramesRead * 2), + (numberOfFramesTotal - numberOfFramesRead) * 2); if (0 == readResult) { // EOF - break; // done + break;// done } if (0 < readResult) { numberOfFramesRead += readResult; } else { - qWarning() - << "Failed to read sample data from OggOpus file:" - << readResult; + qWarning() << "Failed to read sample data from OggOpus file:" + << readResult; break; // abort } } diff --git a/src/sources/audiosourceopus.h b/src/sources/audiosourceopus.h index 5d6432d5b9d..d52de9ff4de 100644 --- a/src/sources/audiosourceopus.h +++ b/src/sources/audiosourceopus.h @@ -7,8 +7,7 @@ #define OV_EXCLUDE_STATIC_CALLBACKS #include -namespace Mixxx -{ +namespace Mixxx { class AudioSourceOpus: public AudioSource { public: @@ -21,8 +20,10 @@ class AudioSourceOpus: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; - size_type readSampleFramesStereo(size_type numberOfFrames, sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer) /*override*/; + size_type readSampleFramesStereo(size_type numberOfFrames, + sample_type* sampleBuffer, size_type sampleBufferSize) /*override*/; private: AudioSourceOpus(); @@ -39,6 +40,7 @@ class AudioSourceOpus: public AudioSource { OggOpusFile *m_pOggOpusFile; }; -}; +} +; #endif diff --git a/src/sources/audiosourcesndfile.cpp b/src/sources/audiosourcesndfile.cpp index efb1850f00b..66bcc6fc93e 100644 --- a/src/sources/audiosourcesndfile.cpp +++ b/src/sources/audiosourcesndfile.cpp @@ -1,10 +1,9 @@ #include "sources/audiosourcesndfile.h" -namespace Mixxx -{ +namespace Mixxx { -AudioSourceSndFile::AudioSourceSndFile() - : m_pSndFile(NULL) { +AudioSourceSndFile::AudioSourceSndFile() : + m_pSndFile(NULL) { memset(&m_sfInfo, 0, sizeof(m_sfInfo)); } @@ -29,20 +28,19 @@ Result AudioSourceSndFile::open(QString fileName) { LPCWSTR lpcwFilename = (LPCWSTR)fileName.utf16(); m_pSndFile = sf_wchar_open(lpcwFilename, SFM_READ, &m_sfInfo); #else - m_pSndFile = sf_open(fileName.toLocal8Bit().constData(), SFM_READ, &m_sfInfo); + m_pSndFile = sf_open(fileName.toLocal8Bit().constData(), SFM_READ, + &m_sfInfo); #endif if (m_pSndFile == NULL) { // sf_format_check is only for writes - qWarning() - << "Error opening libsndfile file:" << fileName - << sf_strerror(m_pSndFile); + qWarning() << "Error opening libsndfile file:" << fileName + << sf_strerror(m_pSndFile); return ERR; } if (sf_error(m_pSndFile) > 0) { - qWarning() - << "Error opening libsndfile file:" << fileName - << sf_strerror(m_pSndFile); + qWarning() << "Error opening libsndfile file:" << fileName + << sf_strerror(m_pSndFile); return ERR; } @@ -57,9 +55,8 @@ void AudioSourceSndFile::close() { if (m_pSndFile) { const int closeResult = sf_close(m_pSndFile); if (0 != closeResult) { - qWarning() - << "Failed to close libsnd file:" << closeResult - << sf_strerror(m_pSndFile); + qWarning() << "Failed to close libsnd file:" << closeResult + << sf_strerror(m_pSndFile); } m_pSndFile = NULL; memset(&m_sfInfo, 0, sizeof(m_sfInfo)); @@ -68,29 +65,27 @@ void AudioSourceSndFile::close() { } AudioSource::diff_type AudioSourceSndFile::seekSampleFrame( - diff_type frameIndex) { + diff_type frameIndex) { DEBUG_ASSERT(isValidFrameIndex(frameIndex)); const sf_count_t seekResult = sf_seek(m_pSndFile, frameIndex, SEEK_SET); if (0 <= seekResult) { return seekResult; } else { - qWarning() - << "Failed to seek libsnd file:" << seekResult - << sf_strerror(m_pSndFile); + qWarning() << "Failed to seek libsnd file:" << seekResult + << sf_strerror(m_pSndFile); return sf_seek(m_pSndFile, 0, SEEK_CUR); } } AudioSource::size_type AudioSourceSndFile::readSampleFrames( - size_type numberOfFrames, sample_type* sampleBuffer) { - const sf_count_t readCount = - sf_readf_float(m_pSndFile, sampleBuffer, numberOfFrames); + size_type numberOfFrames, sample_type* sampleBuffer) { + const sf_count_t readCount = sf_readf_float(m_pSndFile, sampleBuffer, + numberOfFrames); if (0 <= readCount) { return readCount; } else { - qWarning() - << "Failed to read from libsnd file:" - << readCount << sf_strerror(m_pSndFile); + qWarning() << "Failed to read from libsnd file:" << readCount + << sf_strerror(m_pSndFile); return 0; } } diff --git a/src/sources/audiosourcesndfile.h b/src/sources/audiosourcesndfile.h index 08d8e6da8f0..0f34c25f5ec 100644 --- a/src/sources/audiosourcesndfile.h +++ b/src/sources/audiosourcesndfile.h @@ -12,8 +12,7 @@ #endif #include -namespace Mixxx -{ +namespace Mixxx { class AudioSourceSndFile: public AudioSource { public: @@ -23,7 +22,8 @@ class AudioSourceSndFile: public AudioSource { diff_type seekSampleFrame(diff_type frameIndex) /*override*/; - size_type readSampleFrames(size_type numberOfFrames, sample_type* sampleBuffer) /*override*/; + size_type readSampleFrames(size_type numberOfFrames, + sample_type* sampleBuffer) /*override*/; private: AudioSourceSndFile(); diff --git a/src/sources/soundsource.cpp b/src/sources/soundsource.cpp index 81c97e3c098..dd2100d182f 100644 --- a/src/sources/soundsource.cpp +++ b/src/sources/soundsource.cpp @@ -20,12 +20,14 @@ namespace Mixxx { -SoundSource::SoundSource(QString sFilename) - : m_sFilename(sFilename), m_sType(m_sFilename.section(".", -1).toLower()) { +SoundSource::SoundSource(QString sFilename) : + m_sFilename(sFilename), + m_sType(m_sFilename.section(".", -1).toLower()) { } -SoundSource::SoundSource(QString sFilename, QString sType) - : m_sFilename(sFilename), m_sType(sType) { +SoundSource::SoundSource(QString sFilename, QString sType) : + m_sFilename(sFilename), + m_sType(sType) { } SoundSource::~SoundSource() { diff --git a/src/sources/soundsourceflac.cpp b/src/sources/soundsourceflac.cpp index 0b57a4226d5..c8e3322757a 100644 --- a/src/sources/soundsourceflac.cpp +++ b/src/sources/soundsourceflac.cpp @@ -28,8 +28,8 @@ QList SoundSourceFLAC::supportedFileExtensions() { return list; } -SoundSourceFLAC::SoundSourceFLAC(QString fileName) - : SoundSource(fileName, "flac") { +SoundSourceFLAC::SoundSourceFLAC(QString fileName) : + SoundSource(fileName, "flac") { } Result SoundSourceFLAC::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/sources/soundsourceflac.h b/src/sources/soundsourceflac.h index 4a1bd3b951f..45e1f6ba510 100644 --- a/src/sources/soundsourceflac.h +++ b/src/sources/soundsourceflac.h @@ -20,7 +20,7 @@ #include "sources/soundsource.h" -class SoundSourceFLAC : public Mixxx::SoundSource { +class SoundSourceFLAC: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); diff --git a/src/sources/soundsourcemodplug.cpp b/src/sources/soundsourcemodplug.cpp index f817e6b2b94..4fd1444f843 100644 --- a/src/sources/soundsourcemodplug.cpp +++ b/src/sources/soundsourcemodplug.cpp @@ -9,8 +9,7 @@ #include -namespace -{ +namespace { QString getTypeFromFilename(QString fileName) { const QString fileExt(fileName.section(".", -1).toLower()); if (fileExt == "mod") { @@ -47,17 +46,19 @@ QList SoundSourceModPlug::supportedFileExtensions() { return list; } -SoundSourceModPlug::SoundSourceModPlug(QString fileName) - : SoundSource(fileName, getTypeFromFilename(fileName)) { +SoundSourceModPlug::SoundSourceModPlug(QString fileName) : + SoundSource(fileName, getTypeFromFilename(fileName)) { } -Result SoundSourceModPlug::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { +Result SoundSourceModPlug::parseMetadata( + Mixxx::TrackMetadata* pMetadata) const { QFile modFile(getFilename()); modFile.open(QIODevice::ReadOnly); const QByteArray fileBuf(modFile.readAll()); modFile.close(); - ModPlug::ModPlugFile* pModFile = ModPlug::ModPlug_Load(fileBuf.constData(), fileBuf.length()); + ModPlug::ModPlugFile* pModFile = ModPlug::ModPlug_Load(fileBuf.constData(), + fileBuf.length()); if (NULL != pModFile) { pMetadata->setComment(QString(ModPlug::ModPlug_GetMessage(pModFile))); pMetadata->setTitle(QString(ModPlug::ModPlug_GetName(pModFile))); diff --git a/src/sources/soundsourcemp3.cpp b/src/sources/soundsourcemp3.cpp index 67522916b68..5bf286664b5 100644 --- a/src/sources/soundsourcemp3.cpp +++ b/src/sources/soundsourcemp3.cpp @@ -1,18 +1,18 @@ /*************************************************************************** - soundsourcemp3.cpp - description - ------------------- - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : -***************************************************************************/ + soundsourcemp3.cpp - description + ------------------- + copyright : (C) 2002 by Tue and Ken Haste Andersen + email : + ***************************************************************************/ /*************************************************************************** -* * -* This program is free software; you can redistribute it and/or modify * -* it under the terms of the GNU General Public License as published by * -* the Free Software Foundation; either version 2 of the License, or * -* (at your option) any later version. * -* * -***************************************************************************/ + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ #include "sources/soundsourcemp3.h" @@ -27,8 +27,8 @@ QList SoundSourceMp3::supportedFileExtensions() { return list; } -SoundSourceMp3::SoundSourceMp3(QString qFilename) - : SoundSource(qFilename, "mp3") { +SoundSourceMp3::SoundSourceMp3(QString qFilename) : + SoundSource(qFilename, "mp3") { } Result SoundSourceMp3::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { diff --git a/src/sources/soundsourcemp3.h b/src/sources/soundsourcemp3.h index 33bf0800daf..b45aeb7828d 100644 --- a/src/sources/soundsourcemp3.h +++ b/src/sources/soundsourcemp3.h @@ -1,9 +1,9 @@ /*************************************************************************** - soundsourcemp3.h - description - ------------------- - begin : Wed Feb 20 2002 - copyright : (C) 2002 by Tue and Ken Haste Andersen - email : + soundsourcemp3.h - description + ------------------- + begin : Wed Feb 20 2002 + copyright : (C) 2002 by Tue and Ken Haste Andersen + email : ***************************************************************************/ /*************************************************************************** @@ -21,10 +21,10 @@ #include "sources/soundsource.h" /** - *@author Tue and Ken Haste Andersen - */ + *@author Tue and Ken Haste Andersen + */ -class SoundSourceMp3 : public Mixxx::SoundSource { +class SoundSourceMp3: public Mixxx::SoundSource { public: static QList supportedFileExtensions(); diff --git a/src/sources/soundsourceoggvorbis.cpp b/src/sources/soundsourceoggvorbis.cpp index ac77a355fd8..1abefcdf2a5 100644 --- a/src/sources/soundsourceoggvorbis.cpp +++ b/src/sources/soundsourceoggvorbis.cpp @@ -27,14 +27,15 @@ QList SoundSourceOggVorbis::supportedFileExtensions() { return list; } -SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) - : SoundSource(qFilename, "ogg") { +SoundSourceOggVorbis::SoundSourceOggVorbis(QString qFilename) : + SoundSource(qFilename, "ogg") { } /* Parse the the file to get metadata */ -Result SoundSourceOggVorbis::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { +Result SoundSourceOggVorbis::parseMetadata( + Mixxx::TrackMetadata* pMetadata) const { TagLib::Ogg::Vorbis::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { diff --git a/src/sources/soundsourceopus.cpp b/src/sources/soundsourceopus.cpp index 2a30aec31e6..d425dd36717 100644 --- a/src/sources/soundsourceopus.cpp +++ b/src/sources/soundsourceopus.cpp @@ -14,26 +14,26 @@ QList SoundSourceOpus::supportedFileExtensions() { return list; } -SoundSourceOpus::SoundSourceOpus(QString qFilename) - : SoundSource(qFilename, "opus") { +SoundSourceOpus::SoundSourceOpus(QString qFilename) : + SoundSource(qFilename, "opus") { } -namespace -{ - class OggOpusFileOwner { - public: - explicit OggOpusFileOwner(OggOpusFile* pFile): m_pFile(pFile) { - } - ~OggOpusFileOwner() { - op_free(m_pFile); - } - operator OggOpusFile*() const { - return m_pFile; - } - private: - OggOpusFileOwner(const OggOpusFileOwner&); // disable copy constructor - OggOpusFile* const m_pFile; - }; +namespace { +class OggOpusFileOwner { +public: + explicit OggOpusFileOwner(OggOpusFile* pFile) : + m_pFile(pFile) { + } + ~OggOpusFileOwner() { + op_free(m_pFile); + } + operator OggOpusFile*() const { + return m_pFile; + } +private: + OggOpusFileOwner(const OggOpusFileOwner&); // disable copy constructor + OggOpusFile* const m_pFile; +}; } /* @@ -43,12 +43,14 @@ Result SoundSourceOpus::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { const QByteArray qbaFilename(getFilename().toLocal8Bit()); int error = 0; - OggOpusFileOwner l_ptrOpusFile(op_open_file(qbaFilename.constData(), &error)); + OggOpusFileOwner l_ptrOpusFile( + op_open_file(qbaFilename.constData(), &error)); pMetadata->setChannels(op_channel_count(l_ptrOpusFile, -1)); pMetadata->setSampleRate(Mixxx::AudioSourceOpus::kFrameRate); pMetadata->setBitrate(op_bitrate(l_ptrOpusFile, -1) / 1000); - pMetadata->setDuration(op_pcm_total(l_ptrOpusFile, -1) / pMetadata->getSampleRate()); + pMetadata->setDuration( + op_pcm_total(l_ptrOpusFile, -1) / pMetadata->getSampleRate()); // If we don't have new enough Taglib we use libopusfile parser! #if (TAGLIB_MAJOR_VERSION > 1) || ((TAGLIB_MAJOR_VERSION == 1) && (TAGLIB_MINOR_VERSION >= 9)) diff --git a/src/sources/soundsourceplugin.cpp b/src/sources/soundsourceplugin.cpp index 1d814ec69f4..4c910683fdc 100644 --- a/src/sources/soundsourceplugin.cpp +++ b/src/sources/soundsourceplugin.cpp @@ -2,9 +2,11 @@ namespace Mixxx { -char** SoundSourcePlugin::allocFileExtensions(const QList& supportedFileExtensions) { +char** SoundSourcePlugin::allocFileExtensions( + const QList& supportedFileExtensions) { //Convert to C string array. - char** fileExtensions = (char**)malloc((supportedFileExtensions.count() + 1) * sizeof(char*)); + char** fileExtensions = (char**) malloc( + (supportedFileExtensions.count() + 1) * sizeof(char*)); for (int i = 0; i < supportedFileExtensions.count(); i++) { QByteArray qba = supportedFileExtensions[i].toUtf8(); fileExtensions[i] = strdup(qba.constData()); diff --git a/src/sources/soundsourceplugin.h b/src/sources/soundsourceplugin.h index b6c3075781d..14d51ba177a 100644 --- a/src/sources/soundsourceplugin.h +++ b/src/sources/soundsourceplugin.h @@ -6,17 +6,18 @@ namespace Mixxx { // Common base class for SoundSource plugins -class SoundSourcePlugin : public SoundSource { +class SoundSourcePlugin: public SoundSource { public: - static char** allocFileExtensions(const QList& supportedFileExtensions); + static char** allocFileExtensions( + const QList& supportedFileExtensions); static void freeFileExtensions(char** fileExtensions); protected: - inline explicit SoundSourcePlugin(QString sFilename) - : SoundSource(sFilename) { + inline explicit SoundSourcePlugin(QString sFilename) : + SoundSource(sFilename) { } - inline SoundSourcePlugin(QString sFilename, QString sType) - : SoundSource(sFilename, sType) { + inline SoundSourcePlugin(QString sFilename, QString sType) : + SoundSource(sFilename, sType) { } }; diff --git a/src/sources/soundsourcesndfile.cpp b/src/sources/soundsourcesndfile.cpp index 283704f950b..acf9b8ec216 100644 --- a/src/sources/soundsourcesndfile.cpp +++ b/src/sources/soundsourcesndfile.cpp @@ -17,11 +17,12 @@ QList SoundSourceSndFile::supportedFileExtensions() { return list; } -SoundSourceSndFile::SoundSourceSndFile(QString qFilename) - : SoundSource(qFilename) { +SoundSourceSndFile::SoundSourceSndFile(QString qFilename) : + SoundSource(qFilename) { } -Result SoundSourceSndFile::parseMetadata(Mixxx::TrackMetadata* pMetadata) const { +Result SoundSourceSndFile::parseMetadata( + Mixxx::TrackMetadata* pMetadata) const { if (getType() == "flac") { TagLib::FLAC::File f(getFilename().toLocal8Bit().constData()); if (!readAudioProperties(pMetadata, f)) { @@ -101,7 +102,7 @@ QImage SoundSourceSndFile::parseCoverArt() const { std::list::iterator it = covers.begin(); TagLib::FLAC::Picture* cover = *it; coverArt = QImage::fromData( - QByteArray(cover->data().data(), cover->data().size())); + QByteArray(cover->data().data(), cover->data().size())); } } } else if (getType() == "wav") { From f5ecb8a02cc6398ce57b8ae2f73eee4411d0208d Mon Sep 17 00:00:00 2001 From: Uwe Klotz Date: Tue, 13 Jan 2015 21:28:07 +0100 Subject: [PATCH 081/481] Add AAC file for testing purposes --- src/test/id3-test-data/cover-test.m4a | Bin 0 -> 491942 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/test/id3-test-data/cover-test.m4a diff --git a/src/test/id3-test-data/cover-test.m4a b/src/test/id3-test-data/cover-test.m4a new file mode 100644 index 0000000000000000000000000000000000000000..90488159821b7c6438f10f567f36a1c70eab70c0 GIT binary patch literal 491942 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ℑa9og}}sFv({z%ipzG5!ODYgVIA z{!#&6wsNGcN##H__D`z<;rR`i^@(&L?)~ym8($O;F+UsG_a9MZA0>@?BO;Uh|A5aA z#|Rz)S^%I=ICwTk8Qkadv7M&zS_=Oj&ho^L@g%nYi!VqTvy{9CNVeDw zWJJe>^Yblx&};V=Yiy;Eu-1NUce=wcP-2-4WHS89(m*G^UpyQ>HIs~y%z~_fSDtB^ zRr^)7LVFQSlD5Kn%Zxh3I^Mro$FKd5vA{}6k4sx*aKM1oc+mTu2dhjQT>Z@|_o z-6lS`NmGtJ4SQ=TqL66Tl1J-OVPLo!hUw3YArC}TBHWRZQ%oHr%>Z@b1M|Jv z!0&Hb-Kv@jnIy_XIzB~1PN-3cJTN~k_;lP-hnp92R<0D2^2gZgj-WbNX&HRtΞ(E!q6b-jh_dnG86(Jl<1zC_l;MI zi~MH{jPa-Pv{0}nePf<}4NEP>BJ%`~bPUbbA~A(;fOzP|$y;X-58BZb4usWnS~!XE z9vT79Xrhb`?CO`*zh9kfGhc7GZohXrtVW0hRPL#3Cq-TlQ_U%<$ynA2*Ha6z36F`? zU9jI(I(b>I=RqzBYvG>9)PGwbrNT^?o?TJf3e5doFIiuDU#@uNO7rG+tTPv#{X918 zZn54nx^$s2Rq2xhkCgpBUo4YZUMPh7q*1^|$4kIHyZ_6KKEts={zA2Om?dmEddRD_ ztdpYo>5*7h(;-3Ka_0`hqsiqe1-B@I(hPsN?*!55OYPjPSg-u~%F0@gi&5dsJm{`n(^0*<%*CpKFF0f@w`OTDk1UKkP#Oh+6 zHE$|K4f_bPaEt<%Q-f-6Ub`b|PechX3g*~<%QlH#-mpyeE{GjG$Wjo(Nh!fBL05{l zvo2e5KV3+k__GTy3$yHZx7;PP3AuJ@IHNE+J{4>@9T47af(2JZ!3Ibj#u~!Np5UyP z>qL``px07Ld6^~PTYrwtI*^dAS zTNwE=1*YkTYixxL&5-u!Y8{r)1CBTmjgdpdqh)$Nf$#GyKSr*HBwf1riRAy6rF>YF zf8(5w-8V}TM0mln$L_=a|Ba5BQR(BA1uG}XkzIt=RM2=*d%Aq?-T@X2$Aquy@LsAn zMZ8nmje-^ItB8%$ab%WM9Kv)jdZZie7aH-e$-agOyxg}jJiorGJu1|e9zo2I?a1X+ z2L5?BQs0ptFZ|8ZbC{o>@r0o(xCBa7mZ2Oqj$jVwX9_g&NH`Oe$@}4C#O4m5ckl5= zgC`esPH&Ug?n`&}$Ck9DhEb-2t0kjmN9n^XiILethW;W$w-M)Yu#m!%S?L2ug< zAj{ou@~ZN1XH#PY5x>B|7uYO+trD|PPN;HZW`CejGiO%Mi&U7txJvX>P zyeF~n2puuy=M3~&*ciX5Sh1_g#D3v0{9%cna>b)|clPb~zwqI`!+2w0-RFw$zZL&D zVj6zykflLPmao{i2#{|eAANRmWFdk4(C_~=rjno13LYnqW!-H0AJxx)eo!4A7bG7b zDS??8KO|fpefb1;b1o78jQ`Re=9~BRAqn+`$W5_eFYP3uON11ZyI2M$#V$R7KtJI{ z2j7q1+(i46tPYneh~O0xN^7z5g}jbpB6pt9@9v$I4lfoX^s}qf zVuwu2tJgCt(V;8rV#~Tzwpj1jmL!w2N`G3@S79ldUz4Q*mG_P$f0c2}#w(Ev$z;VL z6Fg$)7Z2~EmiUphm6)ug;_0D7lW-7va_%rshEf6j_zZ*Q*obh1CQp0M{ z_FGNoa}anO%@N)Tf&Hq#2iT{gpW!MIuMzw!^U??k4~2*S@}|!wecF!hYKFCGu6yZM z`r7N|sxAJ`-JgEGFDZ^xy(-zKHagN^7Pg{Wt_aT8o#)_S(NTs^gvMLxbN)?kIGAT+ zHJx@Hd)~s7N38dPPn=q-G=i)Yjm8m#Mtb%X0jy@6k3yonUe&rBguX?(1bW&JG=Q;rSllff(3zJ3){a(Vyp^Gm1T;Z<4F3Q*g=Faix)3>P~ih z*o*sBMqi5U4BAj<((TNZ&UXgsgwTe>mDeU?TF?3K&y6k_wwHNhrx1>+wU`wnsrK{I z!=fwP@6?;_=cwXM&`HKgGHo^@tc5@cHrAi10bEu2JY|$n)?fAD;7dp=G_B+HadP1H1e)go1NuQ))#!C1Rj8~EZZ^R@-Mh-I(o(`9{7Alt8 zG!F~ot`;iwLqA}|h8>8`EE8-0)@5+KJv(-3b$jHZhGrkU#6f%3Vwwqq z@%H|RIeX!{(LC~B^Z7CJu|k3jfT|X2H``jTz106W8dxOpb0SC}m3V1%fyf_lSjJ5` zU50wV_a-#Hi@|qspsyhJwf_ZvT%@i<3~)jCXD_gX6VA7vY}`9U>?rldlJw0gb{ARb zoFR^OOo3@x*bpuuFj*`%JXYeZ99pm8dEDK>&dR~oa9gK0-|3b`&am1#KUly(rLNYo z%vIQ7iLWp^p)ucXSFpLZ;4~2vt=2_6mjU97dVRk{n(+_;DfcsN`es_D^S&}>lppr( zG{C*AXHft>KQESGV6g5crA(fZ9}KIW6jDz!N@ksb&TkwjNPDc_Q;2BKxkq<3aZOvU$0AzrGaa?5h;_ zbo+83{+@&KaEkT<@AkWJ_bG$okZ^y6pp75muD$JJ0%!5Jx#$VZ>z}*A_KNWWYK38& z;iml>^(slY>muI>quj0)HKGb;2~o_LffQU7RDmJDq6iyARrz_M`7mXy8F$eu$Y?>L z(^LH!QnewHMwNwHS;Ws3Ns0yaB+TWnBXTMoh>#EuO-KzUX|YGlg?Z)CNe34?CCvY) z2o=!JK^nafH>GIGaY_@ZjuAq^;tM?!EBSBNkql{7*cxmj4dpA3wu=__Onvw&?vwjo zZGnR7kxwKy@TlPq)4T*L;|}gU$(cdl;6obp5_vn{dH+LkOGa^3JcaXa#x7G3Fh!z3 zLJWh9BvqiNdaBpx5OXxeJzRZNHMrG~IZRdSgmbbd5iZBJZ-mIt z4h)i%o)UH0DID@B13hbMaLkyA;8XI)9~H<~fR2L|>_#Q3FeZRc#(0T#m56u(lyH`; zsu|QQly#_L88g?TrmJsSf2Hyh^u3vOucnRL5o|!&K7&*-o2huO0wFL!*GY%@+4eOF zwMPb+pJf}*c(e@5`_j|a5=zReEp)4b{=xQR3T`&ZsG6)iUn|^3HN&qZEQKke3dzDp z6LeDwV)1sjaKVBEOEaHbMXcgZ+G6UM5%#3FWCv%cxjN zpsJGm6SkoQ=1@~vF}sUIa+LUg3eJy77ZM1F<3GWId$#^>4`uiR0&!BH0=l@vk;8RK zV&7CN+}%nzaYu1?0`bY$yG4|~QxA;dBX?U+EwWy;ph$AQ@~;#@x!)D zKAP}8bndi&r&(+czqcuSmk+DBHtz;bbguL6PH?pk_+)zyUys?iPhAcDUa}CV$hNYY zv@k4?T?~;RlmRIUE=(#EWLi`ZkQBlC`yz$c&@O=QW-_EKu=n-yywXwR{Y$T-%!-wx z(G)Cugk!Xf(j=8;-7(R!(}(=h4=R&V>pN3X!ML)RlnUwiZOuxE90b3^QX-j3#^_UIfoJKNEEes!PwXAb7%b@J?2qZvdzV~4v#4Uo6ZV@Z!3!Q3eA#C8k;=5MP{h*> zN_FV50)A#lrVg$hRk;(oWr&Th648!(!Jk@e0Z#zDU z2*72Y&H1-cH_2)IGhFTHa{<5aovmLK4yTe*I zYReKcOnIHFtFvtQ&DcqP6rGGKELNi*Y58R?-V!r#JTJj|v$&T8G14c(1StHJ(om*; zzpKDan(z2+Q#OWm`A=q0GhA_)&439wW@kUW$jEzbLL0G@-*eQZ_$g2NEZ1kbeRv3(V82vygv=~(l!RTb5g?e~9 z1wHV0#5sR9EqRh`8h>_fwY2)VqpOf=;(5vZ2`l+2zJTk93O<-t*XQ2p&4Al7>GDT5 z%gxTR+8DhDHycD4B$VR+#Y3Oc5eHHL@N-7gYx6$!LtY3zbmwDP!cpwsvIMMfXGbF5 zVhDK7t4->3{W6i_EwgHUlCL|}q4e=A2-tz{R&#>b0)bOjp2!dsYc&`Dg5nK9`qnib zAH;{3_NY|i?43;i`?qn~5tT9mx&8!6x<7xQodeO>PS$6)S#{Zvzm{bpuUMhq42DV* z$nA?}rw)=M0~jKrPU(VJam$*k{*W|+L^ zlCIsMeOo$okHBKuIEbHh;8ZWfT5$_XC(V2&IqnRSlmL`!&zVmOG8_TMt<&2fa6tqo zJUnFUR0(5LmqPkDM{$rCr26Xp;sZPxg#{o(Xnk4`WbT$EB5j;I_1;J#uWhHzuf=r; zq$CXX?&ze6kVK+19P2is1v@sk?^omqKKg>Ve>O84avopx=uu|o3-t~T>Qb1dWI-7< z+k9ze0lNnN)JI=@Q_7rF4x$O%Oplp-ado<@NT5D$)PE;}Zblvx9Q6S~90?{I3^bo4 z?>(Q{F;tr$0AP0(emD}M?I(SA@9LLB{Q!{L*Soy~zX=iV3riiZr=FeBq>96UdZu6; zm{|Qu|GZoxt@i?nvfd#}iT5yGUB#ahXC9D6R>_u-2X{%(mrMtPTRJsAx!lJv=r25m z5TjA}b!>jwQ};aOh!JK_!iopwXUD4vpUa}57Qopof5sC<1ED68;H(#L?;m0(mWPqk z#z~QeVaUi;LsO%N1mAYb!{J#Q@j}b?h)p-C%SS;Q%~Uje;x*5cQ;kwloK*R7NvUyq zcYS5KCUd*&{DY>r;mr3(gaox&iVj1SZEU3V>(xSGpLvB-Tb4$Za(q#Sqi4k;je@u2 zp7lIe;S78{94*Pv(jVSz)w|Wj<5^y$CDCCvadtkaSjfod?~QBB*w{h%@!!Q+ZA||* z%nS4)w+a3?m}9)d155I{^Ah;P_I}jyL#qdY&I#YbvI7cT}1oz8hTK2-)Tqg57{nrVe) z^^1Z>)duzES#~)!)q+2kTtIe3t3>ikX;IU~fRd^RP#w36f@sS_Aty~gewn?l6L_F3 zRI>KoYe>Y8w4Ta9ho4WHO-Y^Skp z+jbh;wi~NSW7|%H^R(}G&UOC6thHyaxittu?P%1SJqZwWwzs&-=r=#SNp&j{2_MW_`IU+A`fUJq)D$|EH6y zK_JE_i46iH!4UHex?+0t-OK!|tR2-o+q>PSzx7p;jzmRjJzyoW?~Mc{dI0ymko+Lx z6EmJ=X92R+;&Rf&@#y~FD-`;Y_VYks-*!I+0KKCQZ+{|ujP7hUhU%xrASoq ze~i;QqnAqXaa^6TBPK3AMR2(ur0yS(D)O9_du)*>j_Jgg^m?toCm$_|_n@mx_vB(| z{!9^*L1fd#T6MUo=#U&^FeG9BBU^=DM;~b;lBp8K9|ILeyJ3_gPtuNh#(nHDOV|A} z51RFZ1w@2wxqPkQ%7Vx0mZhMN)w<)%6!rNEC1O#`_G3shO2?m#_@CR54T!jCBf@~1^>#ipE#I{4 z9A0>>s$Q8|tS?0#A6cs!<8hmvGdMXmHHKTHmMo#I@_^rv>Ghj5T_3~DRUP39&pkx@SI@3-aBsu+qZW^{?WbC* zk-?c-zd*XFk9Sa=P^3SH8NVPm*3Y#=tDEcJL5wHH)Q|zVtQ$9PAgA0lD2KExfYCNN zN1uCO)j*KP{(RB7O%s1ryr&+2TnqjL%gv$q7~G;md$W_=@eD2KX3Gs;T!WTS?I&4$ zTNk}(y+tPbNXgqllDKZ%)}qQaJ9kSYb2pBw@h<3mS>V~hbU6t*b2_tb>wKH2 z8edA;D83TsL(jKr#16ZYX-+`*5-Kk=OJWoPf!N+5t~`zr+xC$VDvn&aQd_bbMo=|Q z!pTNk6e0#oLkT9y#VlZ%6DG)K%u?E0C;iRsWy3r{P8Mz?Doe%Tfg(Ce2{~6GMjwa~(*8XLNitHf?U4?(_zdU3tPW-Qr;$Kkq)(^> zrr%+Y;1v5zDv33)E%LINS)P!Er;!MQ_Yh5=XyaadZxF$sg)!tQNn{q+_7|Q=BH<{_nk}sJrZ_rb~dtvN2h^m z5WZkHr!ldGazYv`xlAo*R4lGmqD#=x3B>O?1Imy~&j{S~hFRY%Yr)FI-|7#M$9j5q?o6 z3H$-RVOZmy;A9JJj{w0nIg^C*wZ*h2>lzo4PK6}ovEQ;>^9lzNQ*Tn);o`lr`S;!| zyEYHNV#ncLQP;9l-CUcMfl6W~M6B1bRclz5@m*Q7JRZCTSoOaeE=Hil0voHJ?#TR% zsPPFJC@lviMTvxw5+x1T+fSf9?feSxLXMDp-+KQ<`BaxU%@5o4DxM?2y6Y=iX|Seo ziMGc5ngynbDzG#({sHv~O5VGI`B0U%C#b~W{C*~>sB|!f&UX;3D4Kz|<|DedVO-Jy z&1I7H`#Qc><256`paX&P6VpT5`?RhN+=laaokoZ-!SM2rk@-Cu>Xs#94OCw#wJvI5 z%o@^NHWDbPda@~edn|O$7ntNU><>PX`KSQNOXK(Q62*QI9Nd%&X~Y3@74Ptk9dTyz;i48BT*J3&TfRTUD}@i&NwKKvxzC-^s_6P>+?M?Io_q?WV07`_*7k3TTcSS{ zXumusx0zB z0R87BdWqV(2o80wR84>ctR)zH(Qyf$H~9mP7hf&RKq9=b`=2?flkO7Blm<}4e#55< z&2>b@+zSZt7%duDR@k^+3Tl_ja`X-d`QEC{&IcCe-tvqPSq)+i;Dh>~2@^>Bz@h-n z#;T%6NxKgJnJ__6rD3Uyl?x>_v9c3Fh*5Z8o%9{hUd7&}$3IgCU%Zq4ce%6OF|Qzz=jQy^!dv{Y?|)&EWb(rPTHUku?o0NF#Kb!RR|I7PUpb z67h{K*|JtClZ(93B>N*zdPRD3BN=-+z_R4$#qE-@3_FI&{+d*ZN{O;XejB(Xg}OM* zn^U#J59{J5Wi7l~@;cm(;zg};+NFy0<5f{yZ9ww8qn62yJqHbU-w)g`MP=jL!MhE7 z8LEjClRp$=QSwg~EOv6hA@T>P`@Q$4+sP2`>!-QvEY32u*$k!( z$UWHRgOv(~zf=clob8o0tR?2~2dihAZaP+JG+h!t{7CdyR)&+NKGyc(n!#1PD>{pw z)Y7W&0gIj`CGr&h{Qm!i9n^qmXd^;^3i~To{Z#`2Et|e|z|R4^l;hEKlS!vJIc73N}6%#?hDsOo*KHPd#Gq;4TqVZx0F4dH4Ib z@Hf}{9`MTCLKB4bOFW@GwN@jy1G>XiVl8Ic<>`6&O^7DYN z$I+1PQwqu|!MfQ;v04XRi)^GK;gU5yq)R;zdbB-;QsnZr&}hnA7|P`%H;^#b6ndhq z`&wV=6u}bXa9}`%sSuKq;bvo!NV($+2P~!&U?Z2WXs|23s!ng@!d&@eTTBK zsSfpaUZ%1j7q>tj-+z2-w7$H9F6LLG#EWR$?r=_b4SW=#zqvX}`9>nvyjUDtU?&`I ztpug_=iR505yY|Ye|`r?7>FQHVj)@7tJSB4|0)fO2_au7DuvdTm<~8>x$T5G^QA_8 zf_{bz_fzmYy%ycM`cZ%A%SSGeKnKfrQ)K**g~z8faQvB#dX{w6Cj*iC^?$}cNPI^ODZgFMU8>P>hz_} zu&h`VK|J@*nOxa%f-uI9O@{5vP6$8rJXm`60h4ytlala2D_Y%6$U>?o5#L6P%4BQ1 ztv{@p;djwrka?e{HG=gjpwrF8q>%9Xs8Dg?zAShn$Ke~8(vi@we~AZ2{mUi)Rp--0 zhAZYLi#`(CdiS*bZR^mhy-vbN_4G|~0}L{vB)^~gk^9WI zH2oHC2PkVaj(uAaU?`_@4`7`=*(A52Rv1ymSKgnfO8rqCWIF^$I-L){<+!F__m*TIl;cs>!ijBy%Rci z2dYm)JaV+aGi65ULB1*uJ{N0l1PKizoWyHk`Ky>t>aqfM+OZ=B+g%mqylg@@gc%m3 z=TCH#F`OghDz(d&w>1+L11loVZE80hT%oE~;6*I1`08B<9j?9nvTEf#e~BC?8e+T# zblNW!4*pN<%D~yyma0%<5I4#HB!gf^35>w%AU7KQ*+6nSa;goZ?(+)MqY{&d8*$ z=7=1Vqo$u29_hBpkJI!H+s{3aI?$_t-|qn%n77^9!>dJO->)DcW?v<%sZY^HF$6RL z#JIaiBLPktUJ&%9%!mX|W2*Q>FJ~xe6j)JP zxXY=_PS-Xm!N!9fsS;=`00hMYz83ataGBanriQCsW%IS)CUzsGlaOc%-kG1IxvdV_*8s zelq#1kF1uc<;za)36xA}{ERLpHGz87u^BWvNfNwpr$%&b^+7^qfDxVW^9HV=V;ezV zht)Ul6_&y|Y;mpmU&@f?=DZK{%#pu^&J)STDTx9GD^4lIxC(?TtzSw*yh$k@IJ04k z-NO)Re&;N@>#z(P(=*-DWOSw>ykp$I(Sncr|2>eu)?*$p;2TC!2n!n$Ah7mtfd8k} zVm4SSdY}>Gn72;u91AXfrW$PULlx{wV7k(M7l`m~x)%`mZPW@t2!oRipzbdt6{rA7 z9D38H*tffuPWr&=T@lL5>(-9DR)P*EC^J zalgXV*2ghbTn+75+rlm0qU|{Hnv_4s^=%J8kxejfpbzxIt1c#rx7L5vY2A~} zBBewB(XT}<1392@ z*8e%rn=Yx}S8_c&M~kU?;$LVQFS>BA7^E|Kr6~t*{AvAj%DyWY>@{H+>I;0jqJ*pZ~FYaNQ4zXieTv`i*5qm$I!E6Aof<`~RV_z``?x2Z7ZINduD&o?Fk~ z`?rLEbQ>#7_I_+2tuRG)Bo0ZE;@G`{4(jXRJ7~CHPS6V&${U1x$IyYRCUqDY3g8EE zbYfyG9){y~#N(LBCxmWQ+8&FUy7TNbizbAhZ&F}3>@qDxqfziGj*A9 zJ?`e+bDVKzpc?I3FSXT35|?Y!rDeVeUgVyfF7nS<3h9XFKFpQ;ZYowGM$*}RXVTPp zKH&jBHJ*ZkS=ETojuXn@bOBW{TozPrvkm2G?0amN9Ae~>t=KBgP1&&{UWD70B+if; z+awHsIM}5Gwzu^QDt~SpkX-!pD)D|zNf=zq0)p{qB0V*KCsd7Tfc)**CELOFg5W#4 z$8A>Ey|w0Dnes#0KvSO(!9<`C9e%(!^Er}a&x3i`ThNJhLEGr9@_2ETB8EQg3Jg%^ zm`iJw7o)`Cw!gReS`h*0id9UPe;-BHyAtU8OP_&(7xCy5Lc{mNoYx167QSwWEi)a1 zNxYqZj)BB&lMT1p(J$%i-zzBmIdsEIZHMK{`x5c|?G1Ck_j~V3a$3Sy!?=pRZ34Ra z?9nO)=9$4HvHiTPT&B*wvj>4De$UA6u+ydWi|fMp^TyxYMDgKSCI^%(STZ(Ps?N&N z!m|+;$u9_6P4+)CPl~J0IJWpG%jr*AMC?JHrQDguvg5CK&<=aixPNB#awk?EleR>( zvhNgID$mx|7wQqSlf(I2&P#t)HQFF`ZoK-IouvwrYN-kNwh2GH)GK6jQQv3TyC6@ z3C7Lx6MFY;ZbtR<+Dbt}+b<4(EMchgo%&pnVt}Uo79V!n8U-(85Z{fBh3#+?YaNW~ zlN~W-RIzliQj+#zhfhf}2kNDzC$b`opfmq}q#KarAqE=&sZ*u!JpXe7oQVGArbHr1 zL6fQ`ia1q_Gqdb~RHcDGyUOemJ*85;ANQ873a09X^*uV0pm8TCi(`<=@L=^-R5B-= z3w$Aa8hla~eb?Y(5x zOfTGfNGeR0GO1t);@#RIc9uO%Tlrk=4)lQ*5aB7wQWo#dGhT z^B0-lTt=|PoYCh}zsC*v{X@&k#n;Wro3(pl)oT;U$Ih^N8RD#*cwgvmyAlxs>y zH+@2qD#uj4o*Q8Cwlc%~@ru+my8J4OlF}$ms+b&cl*vYk^30+HQz~COiiS% z04&hLfhySEidqAuE5adGGK@|*E!?VLfIzaJkupBplKb_| zw5zL{B}qT&7c@3&ML)%~ua7KD^9fwlx-*d9ZJ4e+=)%Gc*me~2yz+VjzvVHE%9yMNK{PuT8 z`IJcQ1sF9HPhbyQB?zRH+_c|N(zxMARxn95FwNmJhCmdeF@Uz?e9(#|EHDFI19PUs zCl{CM>;FAr7(zo7fkB7N@DKw)3GTti%rda{AP|9Qy;LmoDM`}tmPPXFMI75Qm20Mw^hB`%!{PQFqNCa!Qrm53jp8R5knfiJoPi>70 z%)rcPps{AY8qndZf__CFrZ%H1JV;+!`=MmuJ z+nZ1NR;ia#lkMZE)Z1f)^m^w-qR->7I!H}kPoC=+uus7>bUK5F35IwJc5|CVc2nqj zu~47QrPhnz4np65_FSqDRk7L3_<+u7WWR0=`Rd(>1s z-Q@$K#74!}G4e)P@_dBiUtDY#-qjz$od3u6Q2Fv zcP0H?YEQL9555%N9FBMug(vW6>`T&PbKk4gna@79X5XhqOla4*46V1zt0hdAdK@;> zR$-<_4z$ZjOOig8cU;kV%Ql5)iD&H7Q zMVygHj-oR%Z({5_^c_nd4N@p0!VDCPe9l9W{kMuqy}!1)xMJBnZe2j8g}QE%Ds%B1 z8|`+4HODsQ2)hNbGIL;evT>5V6`$lWiINu*#IAv9QVX6VJV*tZfXm$Xdx$p^zzX7|sZW|(lF&IFH6d|MHB=raJ|%4+KR@ORHB&NsfHVchUV)mY*s%p2(hV`PkDAg# zx=QAHWdlo(HvJ#g&%zDSjY5jmyTlEi^Ncs<0g5a{KP@wX&a2A?&CVauzhZH(uMo}- zOdjMO&t;LKLe;A+PZx}|{q^v0B>Vh)5)6)7-;+A?>-M z^mj9>y0wJ@gj zBjC2|b?f)#n$Ed?Zyx>dW0&ea?lln@W;}CyNAdV){F@u`axuiMp#!)$B{t_i(GLZG z3iLe^M$r#MQorDt^H7D{*BPSjcKqqt{d@VaWGMmgV;}9H%bMZ&c}DRp50DmwZPV&^ zHTeoUO`b&)HY_tJ3!KV%@_@0K=m>NN^aP7&Xp$3`-Nc2^k0mG-9%27r6#Q`rj7Hcd zz-~cy)+;wyCOKqFQSza6^f*RZnY#m3oyTb_{k!o zjkg$(73qGioyZ^j0YFLTXv-$ihFia)Ya|Dziz!-BFq1Kaowo z5zR@lE(%}5FKk|l%={p+p0F=0<62jSWMpMojX*doh2l7e=^+o{WA}ua|JhhPrAmP^ z1n#oP5}d$WS}dj}Z38c)O90Rq^i{xTljW~WDVWw`_I>^CHFdG-d6fo$PUXrxP|KpO zR4NI*6c6k@>K&L~{X<4^@gO>LcsYOXR^4@0Vp$s4Gf6e4GI0*km5iUTWD`jq-_u9;ofPt>1qGXZR)aFO=f$XWx3|U%$O+W5wFKebS3@}gb-MEZs5Na zNb8d^Lj-~pV2h*YB|JFY{|&eSfv5AI7Cv%R9xZt0p{9s z{6HkT;T5=7$J8T?Gm_*j((5R)6ZM;l0pCn0{7GQ|DhZseXe)EO803n4d(?p`SyB`o zFtgXn=UMV=U?Q(0RGacn0(lzS@gO(OCdJSfD=tLn&`sQ(Kz;e_tB>7HRI~Ocw8$w% za&;DhpQma1`4BggI&NfhXPWK)$Q5Jugx3)RroNh^Elk5prEJxzm zcB0eXFnb7VM1~?iV(1N0?OKeIBDqMk?0ua|Hi{&jV)Vv_FLtxFWdI(MD-`VdffXj} z>05_W8RExVO+|{9FWBR!=z^_=p~ABRs_}vc3&y3^C!5cLM1*z`9%S+Vr8S5CYcB8* z-nZUXpXR%Re*-MQC}~Ks!xJUpNh#29Dk5&kYrHKfwzsY=liu_E3|M`I9|Qy|^-Wcd zOi;mL5dmrt@sKLPao=v|h}wEbP{Ljvd;KGj2hClpcs|i{!}B4c`5~I)YZg}q8-?HP zQqCD|_N(#I8?JDSfWwoi1w#$@$+|;|A_W6H1)b9RbQE=JgI44lvnY;J%^H3QQxY91 zcNjxOW{K)Simn7seTNUKg?$Q7qCbss*S*J;bYi}IiREH7A>~U^0%e474_0w9_qB5C zP(K?cEh{YTrVFyoYHFU(U3X{0^5`(tt<0e7&wPr~p2(xG?Mk(-kH4C(U`8_e86KZ3 zvu)QO=IqtK*;ZerM~0H|)h>YtfRb~@UeFJL0(UK3dzLaY=CrI6Rh)`+QJnQj#y0k+ zgP7NC+RoMpg~ds_3miT%kzmPORhJwm#$E)*ue}$dw}H;-YqXOtS^|yCK>GCWqdh`7 zZpDZDf9`PvPdx5aiz*Y@GRpXx0w|f54T@*X2PMHrB9G|2Jx{gjMGl0rNCq|1?@lNN z5%_+NB;!7BqDO*{;A9_PYmWvb&IB{hH>qOFZy`yT(La$d;z9WIF9^(A*>sgWc9S<) zLe98dQXf;_w5Xc`h$8(m2*029t0JK;!u6G$bB~H~2n6K&h>&8t1@N>jj~j-z2*Ik6 z;L1TF6{Ahq2P)g3Wf0ubMtI=Xa89I%<)fy>c{nAZ?n@OC}tz+-HTu&a8)=P-UZhdt@ff|v{cpuotcWmmq06?bXSGD$AI_Q~pWAjDEInpvgY zCnLMndJm!Z>`k>*cO%}b931?%DM5Mkusc;hlI&hYCWeq50iBxQ(bjBASOtE#H&s3Z z9xALM$ckc-7kUjHcGhvPIXk=Skeu6Tdt}zw?J~UQ#5LD#alur`vQi~`&qtf}u5pka zF}Zo!9j=)Hg6;oblL5LZj1f=*K&gRliodKs@Y0(bqJlcr*QxLCzA^HVVkJ$y8BiSw zUdy0;2owDRq}6nb#?@=YERib31&NW;K!mR!JPqhHZUTDzKD7h=_B}tG$v^r%VoP^l z3vk^E)XBR>l3G zMsyNW8Nh5KZCYHPiE5=_+l|MhR~!yWqR`~wXeT5i3i*Y(5)UN3>48TQzx}4S$Dfyvq7_MZfz>)ZaihD8-FTE5C4h~Z(pewqE((V#nyoytEpdNk zgJHr4xdi%aA8}~{1DPv6#M-WZwEznV0(z#e^l(-7sTBd|Ne}S+l>+lV@IeD1xCs8I zljPa^LRCVr`!O|RMJp_=6RI9mp!~4G&sH$5pXt8U4_ZV&WD8VD=T1=#ORuc>5UKSP z!7W>G%ff5xaq3s7Z9F-B0#+Q~hi%W1gi4Ic;OyN>oP=)Q)c|$sa+XD7wL@-)*(x?s zjuSor6AG<(lT zZnG?BlX&tuGL$vVQR-~M4J7sO3Y&*T$$DS`U!caS{qJ}&HImCQ^tEf#c5Kz&-VTND zKo{;RV@1LJDg$?jqk`uYxG#n4A0!9femf9qS)I%)(}g0_ijjFJO?;;r1v(a*A?Bh+ zm+zuM(&+1hUq_{*NATQ~;xH!A0WDGWfnK*ub3pCt&{bQ&YxWeV_R`T8QjuL#Rxw)iV=t;{4eEc#4^u>`^ zKO)#C;RCFJXtwlq4dt2c6d#-|Y(;JS=2~E{UBLAe;S!1UhQMdgG3BnXHkIy3-hW}` zdq)=&6}Y7wj}2P@KH3py)|f`U6Sf2OL}*t${ZR2;dW~c>dvTN8)8O$JPj<~1K@XE! z3C7oVPiZ@dpYbA-ULz;)NZV&=INdl-*?1wHE`)TfJ2~FvkK#FKmU|ZsS#RK!nqY;3 zrMk+~o|S**o?G+gtyXzfi+sGki0LTUq#1_FG(To7nJYwhoDHZ|@^p+J*sKgmpL*8R z572vnVZ~PkBxq7QFhvdFBfRpe`jXIBE|^9RGv(zFCp(Ye@=Bnf*2Gqq>Kd?YK`7W)USgkU@7i5E& ztj4B_*kD|CGTu?{lB>)GNK*-<#QW+}q((0qcqfs-SaG95jH|7J^7dPXytL2gND^g~ z4b&-tr8=P?P1A*qBP|L$()PuDC~A&#?ryIVp(Gbe-%kF=>iN<9M^CEklZo8X(HODn zj{U0LLhC|>&^FsEky2-(Q8QSD@-YA-p?t#_QT`H1I-RN-38CElugIzf#np;I#E|a^ zTDNo*SSk<z6c;0ci1TpX}zHcLg@oE#YnFd!_>^2flTB5zS5a1H?fly==WGGnk&AA-}TmB58%m`)Y$i zGNKTTL^*9F(+K0)W-9O;RCa3>Xwf`JOVhaH3+gaCb~~R#z>M2T1x4Vxg(-mkHjgJS zNce)-ik0$|5?(?^HHq{4yDhCORltse)k5Jd9u@yVTe*HoJ!;70tta>NACUe3F>Q(n z6p_(?g@)y|s}Ftu2@OL@RnbT#6HLMfsLbN}7bRG3oZ|`w`{6`iq(6072^a1^Kw}ce zVIicA#Dy^t60rfG8Bj8f_q+Q3GgF3w55GRxq&}FQlY19BfFNjwQDP)?)5>6))a@4A z_J&hyM`^D!;roQk1+>+~cV#|Tl^l+#D*AF99rGW8l7kEtoUEKLye#w8QUsTerwnQC z$i&cRXr$RIJHaWcGYBP0{td=b1}VS|2FDl}1-vY~q6nbV&IV~Dz-BHpD1(T#KdEEc zxW5Ik(&D=N5og0F{(%=i_p6dmyvzB{m)q521*&bSCDQJXQaWL1B@>yv=Tz_NVPdBB z$BVX{j~`)I3s!7WKc2RtN`9`UIj+^~VNPu^yTCZz!v+RekvI!W76gR#dTbglWs-}8 zZl(>qcxl*olfXd!Ip3<>`MbstY!Nnvy-{wkiR8UjKN^hRCAq#>oRoit{hY6M!4mt> zdSVGxzjehvPRjc{Ol>~Db7f@BaK`v7NAk3Qc=&_-*TR8~1M~072*nCY^RYLgw7)bQ z2y$kXs0ws9591F=3hU@Fn=zC(*f$2UkHI(#)HgfzG|w*U82Y$anjc zg=Hrj`U1i zS|2WLt3%IM-qCwD`Tbg7_UEjEC=aswe1|8Yu7QJ{rODdnW>giw3g0DaMlup?zwXCVo7*cP^P z6qGLNm%Y-@ROr2@ANq?RDX;ijwyE!5vW=~ofNJV#-<<`IExS5VdXH`}ssnu`Rf6hs0t zabH5!b;3z*3fZIJOJe+|NZ5@d$ur#zV#X9#553qKx$@pZqOjmkH|S-*ua9|pm$_Ce z9Wu-}O&se7Zer#YuAZ(V>WE)u=Pk8r97|T+uZGuHlX;6x8-iEA{Q$V&R=6Y59!zaf z@MeDh5mGHvWZlk|G6g~N)M~w7YEV3#g+`fnhU2S`; zV@ky;F%r&mGs)Qu70dhhD3MfJbAO{pY~*H4Gs7~0muAw;URA8dqvtkJ=TgWp0)$N@l{4-tV#R2tC(*DF~!Vz_EsDrOg!+HPe?tM&YD~I0q z**nN9RRxEO%bl2Nx|W3S+N^~OPnXKBCHXwglVcWDX?Lw%O&hTvu+HXFa%^)nq^WPo z^njm279G$XMc$RXX%U{uS^Evik!5A{6dKzGVYA>xbH(DMQdRr3{)K9b69J)2iDIRg z;B`^xG@Ers*&g;8W|Dit-`96^l&`d~%^(bT3uc7WBa=Sp*ew}_6w?Ryz`kcc1H#L? z6-n=8qsbZLapj@MLnd<^W?6&Bdw8Q#t&Kc>Yx0BN7X6H;Tv%S@@<0^(m70>i0Uh0e z3p*H~bqre3Fn>;kliG!nX#$24YgJvDBxahP+2zGV2A|EX)#?Wa&$Pl7*n>QMjvEMn`o9+PPeg$>0F3Tj`@aJWa2bax=?%?dL&5>; zz*v0nbLoJxAL(c73)A(?r^K$li1%H9J^exw@uGdHWYXDX>EoAsSO>o#h#n=s?_hqr zpdaz%A5kyQdC(C3q=^uuBsSk!i5U^#7WHdx^0e0Z{z#tdxKSjpwd$NgdH%FrF)y~C zbkL%l;ZQ*vFzAVpo~W0CVVaDQ95qwVU5lWqs$d|Rs4~u+H-PKE%@?2i@-Am(W3<;|b)EBbv-_*%&q|LTB1>~byzu&<{W#W0y zh(>u8K$RhaijB`|xRuwDBt&rkgo}(q%eJfe;5DCfQqi72HD9w&+y`;#`={7q0m4-> z7~Z#ko64vU5I00AmUon(!5~jm6uT~AwICsMx@ZuT`7A9!zJv6mZy<8wk1=wjqfbsr zh@Dr8a2?MtOaRcR0$!gYBi;``AcuOFlwPkWo)b@#zlkqmiN*dVMVPMWThuHA`Q5ig zC^pMq-Q?t3a7=B~FtH$uJ04|$dONjPy%giYfL)P%LCIap{^h4+XpQoqKumK-qgB22 zKq7=mozac8vH=4d@7HHb{te9F7MYM>?GjnCK9*kpZ)&RAB>v*fBGD*<6kLbj)JAUz zB`~$e71adc~@`S-R9kRWxYdMq;_N7;=?WGhmAH~Sp4KYcx#$Y=!$Tl3!7ssiB2ilh}u0grCs#@zV$CDYY zk{Sg7BKTcfT%!cbMf}KTIcufN_KEQ(admZPT=&(%b|R5E{{fjl59iA7`@bBRhiah- zu?}P`N=1a6{>>qObMYwxL513)pH^>xuo3~3$t5$F_Xs@gZw zu-~Y~Rz;Ng7A@IQ;LWYAVziH^3!`y4nkmOqD|gCmvLM?QTI$PM6oOR^*7(82DIzi} zHPP`BU+ClC%fC1GM4H-JjJw>I=f`4DsmDg{-Dt6JN-qq-y+xV2*8WQVcG%*~h0D0H zo%1CxecC8iWLd5#SHK#!fy?v!7~Q;RZGfZ^mg6y+O@S@!Bmq;W-t&DXIxcyIl@?3r zaNl!CD0DMl6N(qg5x1lN2#__(~ow7bO9!+I+5a_TskWcAW9moDvsfpoE)VHY*>*87R^Jn-W<=EDnH-4g@8^ z_yM&4=>YfN{mbkmV!N3yW3XR_$_RGVB@y^FKK=pbheqD}Yww9WZBN3S$@!vC7>v>& zo>(e87)3-3xe}%K#yu>ow_jD_N%TEj@;ocSGdO+>{l z^^KJgT@>5-8dotzp3%WD>4~>qz>B~CNu{QpD}OkdZK0-stCYA0Nstfoj2lViop+*m z?8(+dk!xKQA3t*K$<@pz5_-vIRaiUPeNu%)L;H?}6pPES?)kL@bcHYkR6Y!+MszGj zCdHw4MbVaZ)MBK5`x!#-&P{ZRxdgGVo5_0%=t1DCI(;;GjLTpLqt93PGjYxh zJue3wE8r#tx5G4Cw@Qbuhfy)P_XKvH=?cAwc|=K&zZmu3E>k0f1p|dOBikH3)=BPB;eZLn%^a)dQ*e;^;dY0Fi3BP#A8^)qtR5>!h>pIb@LSD8|XdEn!4)cD9uJzSlfa(mWES#>s2ymTw{sZj@gv?WGfq z4m;qA45}oorOdJKpBN!2jbQU)oozlqZZ5B<75q%*z(@*TJ85UTe+E zqg&}kVBtv5Xq)q2DQ5rWNFwNRF`2RxTpt(%2?at> z66XT9pXO)CghId*G2S+yAQwXArawd_LHW5y_M5kzsP{9+k?5@G%{Knc$bmTs9gzxwTtu)YX;&Y5{4M5Vle~9e@11ts`{SMvvX2R;E)&`eoha#*sC6!{^>A`D zoh>IpVIG~(;CtYdT~^U<-f*@&d-`$*G5HpXEnNZrIT>{;?Ok#TOz{mwCz^aRdabf| z^=)Y4R{043{zBqWu^t&5W@LDf8HbE;XC9W$I?N!1XuQZkV7%HpZDMoDY*AtB&BbhD zbTT<1CF6jgOl4Ea8mJJ6>;jXfmr0nXw|;!1>fI5xy|M^`uVdPM~W$&0gyhCsL^2^!2NL z`$o%)9%>GHPi!LzC?6~^h6?CMUuacsHPMWkon$k-{laN-*bUkm-rGI;c}w!FY-lg{I`EG=)!z zRbePriBy&eP#hDAkO(niQ@hFtZV|@T3jcm<&{n8}jUThVwwROrZjf&XiTI^*r2C6A zbixNj2xU>Bw#-w_m2J&-Zr3@;AP){hs#XtRV{I&7QEtc&C;+YsH1z+oH72}0I z?Nz_p+@0gGM$9c=IVl;4Hj-uEk-3N`AL+;gFBqiZS*}{pWp%(cQ$@?bk?6$*?)b)2 zgRs*E<}5Kd7}m3bbq$38<3^niOCl6XY0IMwDR9pa%F7dPfcvKpB-1?hL;DpnZ;RJ{ z;7FCGcBR%141@59WeUlTi8Gf-S$tzOz~74b3Ubne_MKKn{=wU!r_9jyF^9U!5KgQo zynNQ3iO0Y1*3^InFh+bCyk|~=_v>CVkXi6_4ic;Dr)?>UjN;6>Gn!h5{s0f%;BYFh764IUJ^k5nnl2Uq<)0 z?gb90Rl2c!l8LiIR3@wl=s3Gvblr@zQJa`scvkuU)E>cu~gh;xZ$T-S!K8v|4G$<{Zir8-N zKeFdhL6cGg(%}A_rwrwxlOUpjlb}iesZ7;7{%dX{m&-Oge z`}O?4UH5&i`@#==t{h>&WXh}|4yZE?v%B1y5;HO$D3u>EL%=P=g;@<*T8fR6}`>wqWIjj)O{!psJWKR1ds0B#>~YC)Vx*O4yq-bCQ1`0! z#t7AsvWmT?Tik0I@(L149p8rV@1EoBnDN_ePNv%peB(-yYUr7e=*nG_JY|@bertq$ z^r_y)o~z>^TM$Hii}KUoY+K8$dOoY}UY!?jI61dIla|L#E%#>vZKkvKyt&7tv*uej zdicz(V}yLceHLkkI~}uU4CHbCar9~mUT*w@&axdvH>tM;dD0tw-M0GXOb8m_M!U*v zein`cCL9d03JFc~WCVZM4Ne_L_#smqoL{`Bs}Ms@I$NYN$}9nHEanK~^MgtPIW(U~ zrKY5!IGZU?+0&>pehT|Oc{(B#btM`w4I%^77$PH6a4Sa{4YI? zGbmaG>+-KgnBX%ksLcl1>lRUnp#fYPZYlssvKS3AB9EJLQ6Irec|V0-P?LV=R*as< zHrN2b);Prz$>;ziv3j^ft8L_ugwI7{Z$N*L!VoU^{m(_9vsA;0*z8nKvMYXaL@^m#`)Sb=BO_y4%A1l z+WelNV}U#oI9TbKW(Y1ENd;jUz9t$5e6;@h_(6T=k+*LsAa?gAHh^qN_#?<1u`Ebf z$r^R`Rhjf;_KE`S-F|14Bd)t!A>e>;LC{rD49N=FzZG)LUwPl#GI+?TlN#3S&KoAC zD0}~P*am+ABpI`9KG(T`VcprtdtC9bCSZ1>(BQk0-{vz$dnEB7tX}nK@n%rv-TZjF z`>e1n%14yJaQocl-053-wrgplV83P?lVdY2#?TKWk>TZ)p{;93P^FcYp#F`6g%N7H zpL=DE!z*s5Qo-k}8a$5f;b!pO+DQ*h6|JDBkxRHyP6r>NIK@97>IR5OsZ#IoGhp9v zKD$;*6R%Ya1g0GaySv4`JaH6sfl+smkE~>RDLzj%s_b$LyYj#pcKh;+q2S;0wL zf9a?gl~U(&_j8*)Mg6Jz)OJVG{q37=sw$u3R~y4a6lRj!*)(50 z8dSo3;aB`b-0b3LY?o!LySG*l_=Q}UZLz+*;P6ArSB`* z{Q0nE`J}W-I;f9mFmFSTgp*bHE~{=2-N3%7w7c9;q;wx6{sg<0RY_;0>UQ*G52$B-a zCJUPgvE14HJjL(+AQZ7tux6uvbL7zTd*i5QcwV+RW!X%eyerY3g7a0%Hne$lkxF)- zPg*^DF(E7UTsgAx8`z!2jukr*RG@m9WAY^iZ`jIN?uadh9uKWnDz|%57ST7-?_)6cY4oN2_JM1wq_%$v2y2W#-fk&eybCNP)m@~0%R@*S( zB@+J=FXKboxpX_`(&yTcCtfNuE7e4Om@0F&AZV_iH$lBWyC=Wk z^vcdQsB%u-c{@vAM41fIYZP%G&nK##Q8w&Kvr{}_CTa2+d%W1n5-fYN?J3gyUyigC zb;HfQ!vn!I>B`NOoaKf%&iBcGjk;elUnf3lYu$G3EpT>3;|bp2j?UO-A$-@p=H@HR zH84p}wt{((Vr+Buxg+w&`huLGZ}4WoDU83lDyQKTxZ`r5vt`T~){KR-sfju%hzY!q z2CoIB;D7Vizg8s>;D6ggNjSh?jwPr)+*Jla`)F9n;v*ELN68u!(b;yvQPc1qKg%6x z{b`_nr#3wDQ#$s~u6aknB*z7QkB&B3nybX2h|h$h;H}LV^X+`+r|@5de*HA|{^{in za+b?YQxvU|Yceupo(X*w*21y&*Dw9mnRZyotD19sw~=2jx>R`B)#$J5-&5e-T9(lh z>>}2eS7M@3IBdjQ(N*1^)snzZYm6eyaikXud3PBAY{#6MPEoY|Monhog%nVt)W!AC zzntnrI_{;U9jEpe)K1JmxwW9=bbQlMH z0sDH$%y8O!f&qy-Pe%QN-$3bN_DYKKy#i&Gm3gRFxvGO2jf4yXY>ZSh!&c1F!cRFh zY0sM3=p$e%@Am1v75EP06?-`~OsiMU{#0rFt~p0hz^r(>FK z18Me`B^^BUAa(GGVyH*Tv6?hOs;@Gh@X+XF4gh&E3AlWlvF?nvfdiy93b*U|9=QYe z(6fV`1+t#p-MRG(KIf8Xa|5h>#bo_)T=8BYPCjX}t< zf2an~#zl+}wI7@&?4J_yeE{ePAVg@K*Cj=)&v>B*ZCWmTwFfso!u6ncN zO|?UvQGFHf-5EmLSo=JvRD+%KIP!&|@>t}F@vR11Q=U#k4$7=`e$suq+!~D4x zo?dElRO9tjEs1^Aof;>+&I4H1S2q%ZL9okN{ktYP&a?ywNV(W2@ zy^pgU#o;pNW&9Us_McrpNGI_M6{xG^@^qLxUR`cZRbKd#-)bui(`Ikw>?GwBd4xyZKKENvZNwb ziPbUdaqRYT`A73OViHsC4jw{JknPC!&3U@O{_ff8E5*8;^bC=FFRdnrI>#8{&rh3G zyKkRxD;hVn_2}Fv{ENg+)F{x%ng;MaC8M}H;;bJy;hv7K zn7{m^xbAnzk3Uq5xNU7oMw9ZAzDUQCQEm+t3`sS)kqHI#&gh`rFZ%I@`!RbIl`r1$ z4{Mhai{9e5bHI6Lo>P0e)l#}RY|nL6GW}-g3=#WUGBu2(s}sh=X?M&^u3R4H{(03O zFrJdCo_IV90bY@r@C*A@Er>r0P>pAuIySPoaBf&aM-~NcTkI%NmTD54uO=&PUt3Ht z{watv3zi`iRSKel8or-SVpupSlx<~$f7 z7fmpmJFCng{OPgvgi!s3PWTixmrg}bTkx&9HK^dn&BMJr_UF>dl*6f;Eb-Bt!5l;W zqr@SXrXG-gRL#va*J8ylD3Y${3uy~sSP&iRe_92EvH$KnDm)xS0dcmFHpt+_5p+Oc z5irt5eX`QQWZ#V>{M5rJSNRj=E|jjGsox6?H|B+gm`%PvBNt_uD9@&mY+X4Fp`-P5 ziMd^!g$?*$8c4n9eyA@9k@_-!JGCr73p9|ICSmo>+ZXUR`KHg)a2{>oJgYr(jfWS5 zllX({YewbkdF)RcBX`dG#WDB|iG~ND^N1w+8dPM4qmUlk1ph`Mrzi`q1_!Y-&4SCd zbQT@uF~--OeS!;~0E+aDO@$FtSLm(BsSa3!%IZnGL`_4djrp^vhAf{RK<`UuPVzE! zY@>ImL6Xv&KWS-%-RYQFwJB2dXTx$Hf^SE1QFCo~QF84i?7`j$_%FC^%}EAHhSVM}%;8&r1+c4c7NlSjDil`r=0t(kLEhiG=1c?04*RHYhHAVzK4R%62zd-> zoC`^~ciedN{VJlDrFiHKosVpk!A3#x{4e>#h6LiTBk6`EXqmIg^Aq$zFhw`01G9kV zN3XLfnK4Ke6H?}N`h7yYcVzPQW!zWmRsu8zuFfa&k8l+sgH9y)3?DDvVOaw3jeNH> z`Y{I4QK9B>-rtB}5vq*PZUoO%93E+$oJP4Nlf!tYT`E@+oZk z>MOrGY3@+K+80(KklpV0qxw_^;A1gDWFKW_v9T0y`g z5S=xYj1r9;7y%?JkcnRn4+ zFpX4fc)1I;DU&VY!Zw@O#m$u&HJu9+5_Abzx+`kptTDi(EpvQ1{C#ry2Wj-5UtK@A z>Qs1&-pU6p2%u#zG2|qb0;??51}EX7ZYQwRCl~m8HmFEEh5s|#x)R3Z42n<%ZPNa# zphW#=la_TWne<$G{vODQ$wp{64JEkdPiP`^oc0bQ?vEs}U3}Q3E)O7)F#D_*nKS#A zx0&3e^ewWiJAQ)(y!*eUlETH zMm+O63OvI|2N_jQUH6M-D@<{T{q(f7;vZJOhe<>(PTynA+(F}oUnu%$$m|ICMzX+6ytprelSiNFim)auqe zG4pQ0ks%8a_sGSLJ9PY<(6d_-Yd$^;MS6xRlM_v-;KKDxG|Yn6M{YiQkprwyy8xJ zH{V&A%IoYF2>g`=)NGIskT|166E}`k4Pk~0GrZP-DfBQ`^bXrAlsgXkj4bK@vlD#3} zJL2_WQYz8fHc5YEXi&h;SJPm^|8|D|5wi78TsWO=Sr(6&g;qHkzBV z&c9pkk8voRaKo>VzE_AS$~dZpoA3C6Q4w?rg-W%~=mr??am;?o!;k2Xw`|Y7GG?4< z<_GH?kfGhF2KjEJ_e^`oiBAo+iZ)9Nd6I2iO{$ug-k{=3&6-hVS&@KYfyw%9O~ zbD=+}OSpm?h18GePbPA2As?s!KQ0#MRpoU-+(&Pgyh9Q;mj@#B!B>6Ceg7+^q<6U9 zA7An^yY($M!cBcQ%HQw{%m#gwR@*JZZ22N%?#tejp7it((ix0u`Xc1T!*qJvvUN!< z;KPd9fVQX78B4B8I37gF(g1QF-GQae@LLQ;rzD5IG8B0|5ynW6lkg3kGYy-tAt4WX zeAD;LuZ(TV*`EGZ4i>37?pa9 zygNM>DY2|q8TrkxxU$Bc58~@Eb?thcW_{5YIr>+{Aap8Ftjq?3vRT!>Oq9;2c^EwP zkpXx~`ZEVViRX|BU5+v(e`+=W98VyhUPr*pwcmH zmKyYs_5}aMOn}V{kfA4njm5MB7Q(k4WMvi}SCp#!gOk$^ZC?P1S11)ucjK*mVq~!B zV$wxL;mrBF@JcOkc|W5h<=(>A@DQ%+ZQU5zrDdm5q?nO`(3rYVhKv(}jIC~(^DE6n z`m2tbc(zEBjJZvVs>wq)FnR<9#RZ8lC-<5m6^!{Ytl@Z)U)^)GV^o&o_3kGj$t#8UkdlHvc2OFyVjYZR7@yu)8+)~PE+6@`rXhfeM{>1F{Iveyi!AdJO41=GtjyJ z25qhAK~=02F>^qIp>BME;{GGk(80tB7@Lp32zt9tkl}p)MV_!L=;V_D<%fkB%98KM8DU8IQ4B@JeX#$ zkYX{bJTE9aI3WV7taYy7Nj(gBohZS!vgi!2w`5aY;3Uc*t4d?vk2+Vg3N~(3VqBXb zc)!u(dbLGXA*N7hsGh`7wwX~1z+o|Eu$5B%$|Cv@pV)L_V#bIX%07?7%a^oiT<-9baSxAvsWf%m|YOg)W8QK74wjc+oss;Di<9PiWg&I-E0> zSeWf1HS}!HhS(WZvy#Bb5xC3T{S3^mH+9=$<0 z);6`87cU=h#X`nq2??Ka6_spi`pjQC05Bhfwp^Zs>H&Zj5gl-~7s^bKiwkSayshQ) z?&T%*hCybm-FejK&%&Q4;_5jRGD4u(!5yaE5Fl~oeKE$pUn?QEtv!d z4xS(soD}Epmj3Sy40Iq6l(eK!kq|PJWI;yaK>a|URs2174+z!*~Wi zIS-xM#vI|E%P$=eQo-Be+caBEHZ8S9BF9NoYD%0a7|)FT{uE zv!8}O>nDVaU~DO$hJM~beJuW#(7~daqQ~Q$VSNhMJUm^W9eqC}QSguk{fbi@*w#?5 z$80WLl)`#rF$o?jA5kAvat6+mI+VqznL$WE-$)Oxf=W!K=cNDDo7uJ;X2g}Zm@pS+y+dE6rfgddMuW-QR_~p)o zA|)~9Y1b?}U!Pj7!v|@hZyYiCbZ~VABUjeTrNoPl@g_l`mxsM4b1EGBl9}wT)xTLT zNVW|v83^KOOLOA9(Y(#By@UvZg1)e>^i$Ap4y#5Sn-*}lqmc9h`#KZhpD*-So%o9L z=rHm-TRhMR1H;)2e#@sq{*eKCFq2-Yf7#?Eokai>k;QHMf|DfF z5Egw-7rmpyn-sqzext<&BQLc|GY_ zY-&);TL};>7Pg3{gfX76v6+yTh6VKjFR76FxhyQ`m^7=eL~hG6!}V@V_od8IuPu zpEz6a8T4v`egvT)#KlcPMtqowG8}f2r=*0fLy1BVLcRGYXP@3w!+mV;V!=ynDM%88 zDXpN?b!SLXhhD}7laBZNjs3I0neSje@KgEz(3z>^Mml&1Wj0$k`duFL_Qj7=r z=w?NzCAY%$iv6$?iP48-{SlB(FCw~aeHffd*kCdmq(0lL=w;%kT*_+VO1amYnYwKdx2RgUO z7J95@iwera@6grzz%Kp?(fozc;=uuO|C+4_U;==CgCIcA^WfhDfy`{zsyb3$F@LjK zxQ6>yV<&cn^!P;n-aXq-N<89lV}9IKrC>r5gu&|!PWKt4S@p~P0~zj>?fte$sEztw zkn~1rp~YgvG$A~K2W};z%a$Z~sC#v&Z6vkM+U6THxQT`5xh(Way##V+uVh4P3-8XK zKl|>pPxD{a)cA!;5#q%1I+;$3GUa_yjun>3zWMlGZFO->{~VlaK4U`JsBn1qou`Kh z@8#LvyZ?4iuYY?fR6;D+GyMlT$MK5qbo99DExz&ST*7!);(k>~;qG#mRe>^RO~&Hm zPi<`WeM|V&^%~_u!GlbSm<7FO+s#r}E}j+MHCO?%ED0IF1}u^~Y?BCrpLw_;kjJyA zLnwyw7DHl2XM92#$v&PP>RdST{HQrw{@mkOo^?L*V2|mTg}?9k%Ik+o@K`Elr=Y3! zZc=9V2f2GsGvt4|A5iCy00jwcmJd?1f(Hy36TLNGJA!OB5C2|&GLb|$X&^8(kzD@= zomYni;JORwc;%0FHT#E#Txi)-y+b9e}3+bR-UKVpkZ?P~`9JhGXNz z!>fd;UJhAtWw5e``4YCsAxKs5sJ_jse!b?;e{v91)@%85J8g0_iZqgIYO9Y2aWtx9 zGgQctS?fI zZAY)U1W(V&RH+?xJKx$9^{%HoIa0=Nw;3jnbnmP4=vI5 zMA5pnE{_^Ed)YN8LjAh}SB`VLEuSNQk2vJ;yCt6zv8>*aRP~Dm(eMBaH595f3T!AI zMV2Wgt2mQeLB+R+dmMwal2*OTi+TgF8zV>%FYPa4+?iF}@$B!8ZtpuN=EMh`fhJiH zjOb*dsEee4Z#D69SI|#x)bE{_L)JngQt)Zd7Wd_(>gQEB@(>wBGNH!<%6@Ft+g%V{BZ+TG&df4E^sh-zxsPDef?<%ENd6}^IlBygO?hDLlVu+ zmYUXQY#0Ojob_2+UWNCs51v&g6-~Xi`?GwGvPS0lu7|ABHG;O}ZXBw|p$!vT8}pFN z`j0#M)IdGIxL7^sG@1hUmT%TsZbt$J7?W}4=K9;_I}Vpzt`Wy|-Vg}8gR6p}m3}_m zM0&p)?1?TaBel=*k@j-TW$%}48szQVE#OhCzn=`Y!-wE8a+Pv4gZ1Oz6$B7AhQ7s9 z6d&vwYE9X4-2P5%&HusJ17ovaOi+QLITnxOgxEpRK@3&i?>ZPOGxe2O^UtD5{;hu# z7E!feT&6N*99*0JTMCwrOXPYNhC=&KFh|gJ{s+*PM#The@l@klW;g$iTnzd^A=m4h zC~OT=&Me@X+AtFr^_~p}zN38jq8qNaKBu8Sp&Xqd=m95C+3X-hMbK&Du~NhsP}A&o z&|i_?JgMLNFLT8|@N~-k%Wlj(nR(6STVz~0P!HlCTfu)-2gf!s_(~7gPjiwiuZ0Xf zMTux;3DII8sW0PpBs<8J>m-u#5z#h~CFAdAXI)WZrOcJ`XbQ`P8C+%XC|yfN;E@2b zMWp-1%WcK%Jz8Z*s<~+*#IKo12U!s~DZ=+278t;De=PWvQ!yWgh958L%4z&KUI(a7 zO7XVOt~t+Aa-K@mSqxb}e;TYEtgO}pcK83vdyRvwg0~MG$ygSt9eXqh!G{$+9XYY_ zyw_Cv()?XPW>OSwDDJQ2ac12qnzy0FZwA zSz2$|s6zDmRD&Ih$feb@#5%{}*~#t8#U{@oE=$Cg+Q%iV0f8CSz&)ayM zL48H@h%Ea3sR!x=x=FWK3L^l5lvy!p?p2BUROpHX{oUnb6(_B?TQ1PtxIU1L9*VPs zarf0<`$R*~T@D4Xa9~JZaIwH2?Pj zgeOH$d1GmGB0q)x?c_+AqW0@#vE!2J64cS4qy#zckAaFo@02b=Mg7V=nUkHi3zk4U4o2d=|WP}vi|15}DwYRXI z_}27;icQGd$Hn3%22mcZ!Ni_JFSyJ|-|qX6ROM4Z+$PGtjo2GUZ{JneZdUgk!G~T6 znaZJ=-^vJdq+f2m#Fc(heIs(qs#}%9rFHJeT9IMxwXA{!l;#2|SB+__U=t01CA@K4 zf}{;C;IpbWyA?VrcV^RMdFf}?@L^syc=c6&9PNJ7p5qFO`miyhU{<;ieKh?Yg`%I* z8bMT>7Ht|L%E@I#N)CFwX}=kQ$MZ;O2nHF;lYb`z>MLxt8RECFk|+?R!_j7%;i0k1 zWYNz7Bv7ruridQB3&yV$+Qj&ml?@M1x5e#;jsq9_T_Dqwqw5C5C6o9%V1QO6^7OS8 zVgRK^4f{Lf-+D}mj)s=XEFeS~v{sQ6t z;RQhUcUXy$swfL_-LoM6J?LjJG}3V>aamDG5p;S!LDBbJrcYO6X(Nso-<4j`KNjKg zrgwz5UeZa?VTrM)4PYG5%mVy3P!EWnNN)$9ZxTL89&X3T?-!>(J7QLpJA)Hi5NrSV zSbFx`EO~}`NVdI?eA5)T$vsv(<7d>5JcwI`dmO$mwlbtF`a!lGF}W7Yy^qc+&vd6V z@3<20bS7QMOWAolG!9N@pKZ61`m4sKm`riPZ$;!F&MV4V#qQMflT%G`$IIYFo8l8H_n$i5*E35 zaAx5fJRe_`mPAW&M-x3Vx-qY|TbC(JsUqQ@Rqn!R>oeEU-uaUjuG^+Qc&+Bc)8)zA z58M64Q|v-!2Np$OCjSfn6$@xt4U%RC4cGs|Us6H8LnjRz;O^fEQ??RjfRM`W3;sU- zUxs>DVxD>_9~%#vKFJ)aBC&E%^HLQm*v$@uq*3V}+L?2AR*mwWko|Lxg=*d{J0DxV zX32n4C^MXil2N~FlnGHDbyr6oFi3^`@ye;`G6T-oRM3iZ4E=AQc0t!vI9=L{_sRhmD@^{zv$Ps!9}nQsUZ*DNNSNmbxU2^=tjf>$ z-Fm>(e&+KyN&Ozg-9}S++uOL7oZ91g_VTXap+z}b)k_9zkrn z4+xbuyu8(Pr)#fHyzJl?e*je-?RQIB>}D;~H@`*z=(HV8!TZS$GuD`e{UXsw_=U$%+jC64rM#U z6F?K7`|XJ&PjymY!!9>2k%X=;yE|1#&@R6a#j9#{LTWT1x#lEL-Z+6Zv2@~9wp^$n zQM-7y{p>0!m~*|}Zs&J`$G9rct3)|g``ZBDwCN%*hxJC{YDfG)?i0SHCGk^(KI}4e z=mlQvu%3{!d{yjg*R#{jTo#9^sB+HoI0Y~^ag1|W1Eqqnhk8mS8(e&WnSnl2l6QU0 z3;O94S5<~Xw_X?v%7vm^pTqYPF-3>LicLv(_`tVd%*gruPt68Ibwe~7 zowmDcmzG?uhv9lHe%l;=1>-CF&!V~p3=(yJhwKWtW*-lq|Bfy)j8ZoZqTI2p%EBrtS{)YkOc}C~L-xR1trACr5Rt&Y+5} z93X0r>rqw#EHGn5wM#+_QQ>h5OKhnLqZD=(esONCt>Y8`3jW{5)VKGH8ZkO!{K+QuTwF1S+y zQlIOblCy+0cJHgNm~oj$KQ`WVugPQ8lLh>{d6q3*c@RL9Bj=Nr=n0xfO9BS z-Uk`2prP~~t~m>x;XPwZT^HtnbU%)Mb*e1#yioM(MY^&Ve$JEvx9 z>NRzAY;|T?$2P%GeO=R`Om|k{p!eHmXVl$?22Q4{8?X^k!NwEZMMZ(1p4tR{8#0T; z1X?CeiX{+Z)lotw^sK4RA#vD`^)s+Q$VV`TIBLOLu;P9C*(D+#sMThzK*m8z$7br- zBL}BN>^QqNlD1qN@LFF_bw1Y3NG+~CQZ(R?4A^jcW{PCa^KozT#J77w<5_6WfMRq~ z+a#lWu@*?lI79X7V=2bWRUGx!mYH>D3S}yh3A-JLm$AL(EjS?C;hvpm(8{+NaiGnc zhhb<*JtP?0pWehB((82bTD|F>Zp_D8WNp}w zfbohq5MAUk;YPGC z#El#m`4>tXGU1}&d$eF1dMVS_ijAGXTatebYW0#bB3c}jc%8B=>9Y!GJ$h~QZ#KZW z3Y`>48>EU=gBu495H!6+_ZJnaa$>#-_~0lowCpOkbm|Uv6>VCxCTrHz)JmI&pj>-y zXYSKF1i8*0`jY@ZKKWQ!9`Br+ZH-n*h`^f@Lg5YJ(wjAiU5~i-TJA_2|0q08f zM42d022*g*em_%F6zE3zg0w;~$C|1+hpB8^$|RVIRaLXhQau`*cc765!|E(Nd&?H( zJ2ymHW&^pZX|51iPqVBpnPC!B6XS3$4hJ52#W7uaUGGb^=w#ZaQwTb!)Mc2=9Jg$K!5{>=ZM1w5f)`ci_fmcXa7aXPUl6jZ>2EB?h>$_pLJFa-sHW z%y=)u!Nm!;KBP!Wf+^H{lGG~*kkV^;(bAsY$(kvJpLXl?2sS?qi(^>UR~y^67Y^SAf9bV4^>Qe>W}j`QV$1j8Dv6^Hyz%3Di*Up8v-q zis3LHvx&Jt1nD*raed(lR+|!QgJXKLO6~ISsHNl;l8Q$ANNgd?qjSFf63)zAW+|0=(EsNtA|r>tJ;;I`&WGalIuV9&&RBt z7l>-Ei3~iVX0pO;-zq(ygOrP?odA%DvtF&Jm+u&b)PID&wpIl7WPWO+$f*f*wT|Mi z+LK)6mzlRNy{=A8@8Cf^+w^^dM*ed-KWJ)OvdjKU$=s#*Ec5Y1XF?MNW zMK*3zjr0O=n2h9y6A|evYR0gtwPCM|s3ih8thdFt^$>+*Y_+NGkVgN#kI@Gl`|G`@ z3!L)s(h!9);}j zsuF6y+s^k52HhtU49Gp*6m9yp!)Q+v#%@5N+$*qZ7z}e^)G{1`k2M57X`>$QB_Ilh zPM_q+^G9fr!8xkz=mqQo&E&r|!^gDiHn*MczMI}7aIL-!@4k-?! zCt0-dxbL`~F`IGuOyHKf4Ybws?0%1S5d=@K$b3V5UN2c+UNE7h5GJ%ymQ>htN+Lq5 zh2ByXnTw4m`C&{KPUvEygH~527dXano0@1L-Z}I=1|n+TH+;pm7eZwx$Rs=+A!Qcf zmLwxzI0emoC`bZ32&Sfxl{Q2xwX-b_;>#Ys$;&~(yz?FfJ`|h=s{|d3K{@0IuuXqE zbsfi&N}U2dm>w@#yOG!` z@DT6M4A^n?h~BR8gv5asR2)&{*RJxhBe^y)<;ROBaAfzz@ye~q(fCBq_U$lQQnViH zZo$YV_9uN^mdj&{F4|V(UeuBzFn3T|`Hz|f4jdp3!UR;>pu$5z1RMRYnd%1p2?$;O z)Ew-J(BYK=^9Qga&xd*;8f8A}k@=vWL0!yW^il!Adhav$~u z{=2R3iND@MydWWeW(?Sm$&x`PlGw&CiM*pQx?GqHo0g4GmToHZ&>_HQHzVFzNUV1Y z+7@+XmCt=te-OU(colbM-76@I7apChZykD_L#6u4OI6t39VKzuB^%njx}L+-RYL3oe3ydr4Y_@XDD z6ks0q`G#^j!{QC*<;^*GO%n6RAO32Kag!#p3QN>H<(H2at8`ojmQ2-inn#`QAz0GB zl(G^kSDfEDd(Mrs3!Zm~l!{Mb-yU>eLYJkgMh5_cdY2x|Ttt+49w}K#VS4azfRY{i z*A9N1;QxoSn<8Q7v;21)2?{G1E*vh04tF~=7W!CpgCM-BM{RV`Ys5Q(s^K8Q^B?rP zPvu`35H%Nqb2Oz7$c)i>*x)HuFwTg`s2?gHF67>{cdF10BVzN|UHt;>H0@F#nL|Uc zu<_Le+{d$e<;}V_WX`kfZeDzj7{eAS%Lmq7KXH^bmZa!l>sXbq%199t$vaHItNW10FNe{LDW)HcSdquk*fPU8JUZZ4s(MhGk$EJ%HS!@jC+vgDmudG zyl6Rx9K}wDZC4((7QjI&1H4)>0$wW-Xr81EC1*GnvPD{!TTl-7pjS!japuI-%xyWu zv~22jP}o+{uJWA?)cgQ<%5s`oIdMT%N8`==$@ZfI|2wV<`(~9?;kV3Rl3+))htoj| zde+L=!<2;^Gd0%oa(ceNjdw)pQ%&g_gjCi*thu>edNc1@aw((CNc|qDE)V2$ zgZe4hd61Yqh5rT-XP-wuTdTDJYfQJVK*{&prRj01a38JWb1QP{2os%(D%UezwX$?Y%CaQr= zOc1mIDfC$6yOyvD#i5lQ>L_>7b7Mq@hOiS<3t6bJSP_m^NbW=)^O`V?BWDRP4-$W} z(k5q-?v@pabVIR8GXf)1*MvFm=jty1xcg+!znmc@)(XF62@3?%+oFEAFNir!fA+T> z)F}kzVEP=5k?e)+H6DF0-tbSD9AcNVwd}=HJUx%{5jWFVpKcnkld)@T@;0$t18X;SDBt`z{2BF)km%GAt<1~WIcJuo3?`m3Qa&88FA}rpAXA@vV^DVxS zIW=burAqcGwXnn;KB@RffC||yL6rDn$YjaD77ioZBKL3xR0JtSE=}9dpelxB1&TRW zX055{^@@xES~6`S99I(PvPogNLmN*webiNWHAaq__i^uIPx9$}4@0#zUkPgaC#|qj zMGsF7rl>N-WNC0FuZ}Jn8)nKE>@D^E=#loXHcXjz`9X4o4Bo>c=P1m_L65_Mkpax# z%9T@mhOpLA;?C%)8aW==sWYLrM6mw&j5a61_)bZ=)ZUCw!%fJ5Z?EU|cNvLjmlTfv z&`iDLqw7d3A*W=*-`8WWtqgV1&LPWqMzcwe2R{G>`74OPUIT`M3J)=GN^`B^~z3hiIFM$G2b3EuP)u>^kOpS}7B=9e~{JxXBt6yzNw=@hE> z-L-Z_is@U6KXxp`nYagIQRO}&P0Ox&r1m;|ZZzarV;1sBC;!*k?An;T%Zby<(b#CZ zS^Uz)AL%={7Y%a1ih231YBrT8GHnN5Tb>$;dplEA$L(ipWt@MrxmB6&_*PTb!gJlL zls3E;*RFJ6J0!`KwoJ~%g@3bop(flQ%xkP|6f@i)*|OMrI{&_JIvKmzl7u8OtF!z) zaBl6a6N!>V5Z7eH2j&VY{!orjx!)A;GNO1=o(Nn0a|GFW;GT3UWZmiNH|LKIc;EzG zJtjP|7S`_FUCAQ7`?>1|>GqMRpGNI7nd|`rmxrhNMk}<^NsUgAotWWC!sKI}VADTx z9*DFHGV%sjAprq8P~XD-iWUWc!XL-};p+8e4B!*z7CwG&$dmM<@fsH4U+nVuIq{Vl zH1W#0mVvCwkkS3DrZLBx&LKSh4FxUnW%=cm`n^hMg;i*%RVm|H2vdWtp-N+IaEgGzU zv+DM`JbyFH14PoG6mPxQFYlCei%Ak?GWj%=^it*AUUw2BFrS@>pPp75lAG3~?9|yN zvCUNWCV^o>7HJ}do0r!c9i&R6&Sp6E+n?lkZRONQ>qlAoRaXBlOdMd&|IpYbM{z6y zt?=GP5@o-<$4x~jN2xt6uHd+yffM!rIz|5}B!`saW{4|5 zXpUHq?Z0uRKurJ#fSHNfW&50r<@B)eGDrUMwS0$eAf82eo{N7(Ii74}IHkZ$Ks_^) ztt^k=YrUa(%iOfv%=y!s5^>Y>`%f0mZ02AbJv$v zL(T>C*%?~{GjxY0#AsZfxI4icY#XwozywJuBot-DnrNRNr^WVPd--~e_-aff3eo7X zO^K{_K%y~REGxZ0E%|4s#4Z>4%e))0gZ&Q~6DMYnK^*nMpg?Neu=sj)RJf*Dh+g<7 z?H4d@0dO8*y+0Qr|MVNoexMvxpN$Ndmu2utvHr~IgTivOHIoKQnWQDq)TBXNYb1F> zdC?Ys56YpD_~58Vy^=rPwWL_C%$6xF-*=U$v_m=anIgM~deKA%u<+jwkvxizVp35K z+Za;u7RFN`Ep=4@9wpv5jn~vnzO zQ$Y25+lofqululUV8UDUm>O)0V;GbhKrjwwN5nuAHdR`4i z!iLP!(o7hTdCGVSil(m`eSwHb(7>lC@gHSt%yIt{FG(_oLpwa`Q_b@xpOh^WgW(yP zC4hgKoMXdzOxRIgKctWX>liMUT(9B0hB!c^Kn-diE96A{tYFq6Cs1QF2SYsS#=|fx zf|Zn41X)&X>l%*(ci6F9=w|pshB-O$+U)R0!8}XN|WHmT%-gYAIEBjvd!RacMkGV&i0n` zVW;N?J70TeOhlfHy%^dA1p_q;g26)Pl~ zW^fhOAlopGR4-vjhl|LJ?5!xt@sOoCq|GdPk9~fjL6XmMx!Q(NhNr=PD5qq)r!qen)E~x5`6M3Vl5=2>% zh?5}F9Z>F6yH|DqCWiP%$bL(qnHXtRGl-+x08niXl6wA~xknS>jh+by;GCUU>~fap zQIdxuhpi*#lQy3|?}zyG-}DNECZ#bDLG^Rzg&#ioz7Dy-Ghq0mFHj~L6JwfOEU64+ zbm)PgzV5sLrD6?$DS!UK6sDvXd4WGsJqt?2siB78BxrwwTDlR#y}o|j?E{*p@5QKY z8D|%Lny=%Gk1!=PPV2rH6;!G%8WD-#&JQ$g1GeSN|^I0bnAs8V802- zfD6MTDOp`%sK1LB1YKbNuuk>-l#1JrNdSgnYh=g zf6-0;87@;v{o1Jig2$CY|C5iPTCR^m-d8WJICvprb3ccm+>^gg3 z`J?!3YLdHP&Q96BUiqhgf;!Yw(cHmRdcg!k48FPqm(JdJkFb&QHctRfzk&n%-_wNI z5tnu4qcHcxg^L8dU3^hD2zqedbLczE_|qv!(1klH+8{d`i>1eDsIR{;Z5x)fT9+~} z4TC0z^N-bt|7X7XnHA(fBW6HU^jT<3V4XpDq}PnMC5v(pYP112RQ*2K2~P{}vy#36 zb1%B_&%MX^aPHB@Ln>CiQ6doUkIN>C3Fq(XIoou9N2K2!0WrIJkyEUUPFoM7s4tYl z;|3|9bTOUWTUj0-R^|5yA&fI-LBdV=D^9NtDEOn#E+?B)`nyCTge_%MAi5H zB#9M(TQiSDq5gestz@{~Uy2ZTjj(JyCDa;vfVhBD#3|9i*?nfFHk^fTofVZ8D9@TPt>NZdAKLrkP(>r{dn5#aC-xC< ztB;X=uA0hgi1wR!%l++#n8h~6JT9Nx)~m2f;-fGz2NBMU^xURph@AvfcI^mBT?6|N zbM4LGNL0m)u4Qn*xg^=3ap!(MAAvM^vJc&{=Hot&tLMi#i`<@wPP0=B2O?9Yvz_LSX|uXh9B7SX+PHSP27t5SUB0iKTAnoy$b zA=IfaeZ(LN*2^Ago4MRQ|3UBqun^#I2Kg$*z#sco!(Q$`N#@Wtl_&f_OBYatQDulw zse2lv8ve=Sm={n$ls{1~RTv$i$=E=THgbywFRo*;$t}u-7ktdN;{_8gS z5~jGKl?rW965GYt`{gV;kff?7EPCQ7dkwlH*w`?=U`_{+qZGId_^8;4MyOB$J%T!< z$_&aku(I+!+|@?lD0ceM8We3-89l@Y&7A3jMFQ9TGURco&j>y3fvS=l{puw=&C%7z zB$MaelMIFNn;MIaOO!^csP(tRX|l$?@yCD=i_V>gi2jOrA_rO=n-al4CTeJHDMno{ zCs|%%#=m>iepJMocCjN$dIYd4LP3~(fp&CBnk>~TR{CI*sz{q}RbwQS97A?}3liVvFJr=Q7K@i4et7OuzIEKD>BL#_L1UgqcI%^pj~nn8OfHpMSGD{}cQF;G)_UAhKL*^9}E;LT>dnK>6IjQuYZ0@nP;kb$`Q$n41= z`yJ$C&BWANAZ zy)u>@wt2XtRY~~fW+XpAy1znnFNHsOP&|!p-?qpe&%&axe`$oY)EPA+twmv|Iuz2g z^^LaRzZ><1xb+QTcvt>=N!{%0>{9Zm3nvV<>cTO(ydc{Yt*0)7W0>tz!BA*lu4Hqu zR-8hWuj2{(_VdUjouU4s^yB`o=QjY>LTu4$i|*rrp!C~usZQXkGZ+b8}oC%tn8A*>3jJ~3|Y zAc60#jWESVauq;2yz-TUFK>5s9^)+$;CsHFuGgfI{m&tNN)lvZ-n4mU~}8}A7HrmPW@C63O(5dHD^$3WkO zNdYwR^2_>N zZ5pE!8sUDHFP;?h79cN%{g-BR^lx9MP@T(bfpaC{RzT=Cp0{*WT?jgi6^LFaUUhyr zp_4nD$e?59E(3X`nZ61ccm{nQsYP_U9|zx~I@q&oRTTy9SX0;r246vBFHNJcJ1lrW zxc&aU{UGWN>_QXIpC23bQeeTMR=O*;=OsF)?@A9G;x4q0W47^bNrm4%0D z+5=08S;^1ev}99qevm0}vvH}kh0yep{ZK#<*3f}S&6~R zfw%czZyzRnpv$0ss?^)e|E5zQd{x$ZTuwY)l#_%y)OWXzxLNlW_Y4!2(hg;UsNgNd6O1@+(B8jS2EkX^20F@8XX z<)^c#lv3rxJh0@7rcP{vLFY>JA|q7YjUj`y58`@dm{lYNXvky>vBHG)I!~|&;`bRM z-^h$`qEnh6Bb4JE`nM7b%m#Wi8EKvL7>!D+8BBQM7pf=W6054Lq~dC*w`%`tFycL3 zV#RYAl@emrZG7RxB=oSOM4)=Q-tHf<^{2hthE^mQOX?p|*x=Th0xLmpvV$)eKn;|E z{Q9lXy8LXV-%SBDJZU&Ft$;TTO{Kqsr>g1_2OZAI7ZY?h%-^B|1T_+&je3l~L5lz~Ywky>uBMgI z#se87xuI`U>zDj)IjYvzUyCYUG~r6r>4&|OhsY%egIeQHXeFU@SrtgXD^jeX?`wRc zZmWR9bfZCGcEDGwwjY+XtG@na;mWj|NFnKd5ZWcE`o#cEy2t`+@=dbugeBy>K=k*8 z>CKsKwn4Yvmh7SnCqHL1|B79*SuJBzz(`JCx7L$t)LimpyV^yYL-sm;b%aSnROd$e z%yCgPpyC(HS?!S=1#iNf#FMqRk(%ltosNrY4HPTCa)ykMZ}e^;?wlO$wfZFDH+FDm z(u9C@8aI@(-%z1{4n;14^M>xQzMtj)-rOSdXy44DEhww^~o_ zpOmQ-hASL9TmWua~n_b=sEnrNx4G)C9~RHfM2yYI4~p_=xX0Z1vJ>) zm3|K*`UW&qYEb4>yoJ8i^#*CfzaycrlP=i5R2*N8du%r{oA50ovcV`p{EOcA_G2~1 zjiqp%j~oq;PQ)0ii?gsDofNrfAU;179b#!D%fsP_T%!q&24q_fE25{UIg&y;b3#;H zYO~pbHbatYni5AIs~P;>`nZ%-aIyKpy{qGVM)sgR@u~FT>@plpG|&BquayUzoBLSj zf+Vem7Vl`qafT+(qO`9qe4T2w6kQZ$aMI1{Hp3(rR{*na{_6pK_;WLr{Y?d_NFZhu zTXUR0tgtTo85=v9#zop_FeuD>MSoGCl6QGXJIdv0R_Vxu{46|laKYGt`U4VHZZ$fV zSC3u{zpcsRhiJPxj7@f#z`8o>|iGm zTNp&NSoQ%QgY={>zSsTumE;2%a3Udm6q8dqd2S<-S0_u4s=_%Ohv$Kwi4;}%Wc$P} z={@o>Quv#*is4YiI8vrQ_BWQ3TaJX?yS-iyF~caKNYs9aYL+mANqaqF8PmJh8Lu-t zqieIY?a`WNwLH^d(VGT+lYP=va&v=~_Fs3_4U9Ae798d9GVg~TxoYns18Ol;w&R|s zesB|PvR_5ub9Ac#-{kBk5{6X?($pUAx5Hh{^B#^L=sz~c-|;O~8+??zZ=O2&@2Ys< zADz}1&?}~^*8*FlY~$J{7oX`nXuQAj5(%FMphOmV}|7qbwdQ;8xTaqo$MgSF)H zw}!Vuu@RtFp+jYFD)7FhsKlKxe2)Wjk7~}P*bIYF(ft#A*yHcpZq3O;|A%{PBt+%d zhlz-sp`E*uORbnZ=Pvm9|O*nYtY{W^Fr?YCwmgp*6qz|;mo5&k8b zXCjiP-+T2(vd45|L8_VG1~Hi1O2i#vFif*L)1Bt~;|p=647?`^25#<+#ET49GNPl_ z3`%gy=?8`CNM)q=YC9;if*b;i!W*f;rRqsVUl=)+btM*!cZd57Z;j|pwV(Gn5+R~e zDbuCKV}=rt#zgxAa8Tq$+Sdlnv5lc2pP=begY@j3t1<*4AiF!KMF6X$}730i(Cvn$7Ntr`LxDApvp=gaiI5{^!C8KM*7@G4SvI zEj%CuPz4cXG3CSottdv}OvWW|RuuDq-aqnxwcdjzAE#Be9ZGOijYHZMl9-^;-8HdF zkT>7BNv@|~r%*n2a^}xtM&+e-+Hp>Eh)KL-h|!(^m3RE;7hX5=)9CBrND)YlmNOlq z16dD5{>##T2=CI#>qk*-waP9Zu~1a>spE#X%mwKXQ0c`jJUV~}X~L!`85V~U13fTf z&`k(46DKhYa&aJud#eIwg!#h@OtJ&o&x+QTR>MWEBNYqx4qE;@XIzpx8nqg%=3(J} zSYLAbMj2zFayT!~7okKihG>pc2xwWMqK^y^m}=_cqX3 z_(@n$Z2uXeQCxGjwD(kn(3}OXhJu-iN3JsSfZB2bUFC*{Y*}FoTg_bi(l()}XO9zd9mX zV7FPCrAsQc#tWIJScT=fr7~=rX{VU^5bel=_7eEiK_e(BjvNAsV zBaNAe3G9>vEGGt|^p7!yfeB0ILI*z<1Zaf89Joq;_7I{yFQ+ZR8tZemN}SBK0{yYd zZ^WE6RlM)mXBf)&!0$kV@C&w{xFB1PUsKfd(oO$xs( zp+4r6*qj&kQ`6@0&lfAc@?>B2B^>!oP~GFb$j1X%f7~`wJQ|HWa8h-c!tpGFS~+Y4 zymzl~`_?je7;;R;zLNZ!j5>_Ul1R7 zHH51fB*FQzzNSLqF6UaF)1J~nqNt}YN5BdBXIRbUHjf9%SJUoFhwPZ1FA=Y=MYeFv z=M}Oc2flXm2pDE?oD4rDKvwM#CIH2#k76Fji5I1*IB|Zk--I!nj-Yp_#BocR^+!YB zt!f|hk*NPCJwS%V91txH{YUok2}uH39A*kKy+EV}q^Q3dXa`a8Tp6>5v^OFA&aBr9 z61tPqF@yg%_wqohtYK}45bArw{k|K}o`9d+w}lz$dc;?Xlt|zm+|N!*N{ke3pJX4! zV~GF>??-o9qRyQ4B-8IicZkdit-D-G_VY^bbCM+q0}*+@zM7m;A&5f^QVooAdg^>e z0+_QD0r40VC)24M%ou;p;J2z3^ zp_%y5i%~#g6_qTJ=eA{)!a$VcD@G)F5Lh!P`oXwf*>^eJO47f7Nvmn8krymhpOYS^ zGNaBcV;$J1vmGe|r?Vw@xR&y>15mEd!7 ze|*#MBI&Em4Kv|a%?cKfZ~sXVKKYXD0U;oHDwO~JCJ8=I09RbymYXR>-2(gX;{dm7 z7qr*QN6WtNsOZ}U^n3K}LdKAz3u4TjGd)h8ouRZ4$+Fis;VauKtC*K2UsVWSG+tW& zy`dWWB&95;lw_?zl-~68D z;}0nOUaVX&R!(QhCvf$*hb4A67sl)=>30Tr0)ccdmUN5JV@Y3`qSx`c?anlf_!;se z$I6;xMwV~p31**mjoOl%9aO@oLV3)_+OCou(gX_=1F2+Yb!giMhLk3HqL z(gtih&e)=oVJ~VG3TvW2FemIQJ8IZ&Wmz`pj#Hw&igNEPr&*R3)gBf?UsZD8zLgz% zp0*pEp6fWrj8i;Yo(#muBdo|$YMhKxk;%^K-qHI;0DK2#P1oy+lh(xN_T;lVou#Wp zUM!}pF9s}%OZL9LyU@&Ld2a5(4(I>4;SR$LC0EsKX?c|EglmWMP}i2=Cgu`NQ#vSP z;$L;fUP4n*dr}bmvtYm^5U<~r`$`-4<#s$ho@Kgt}M9~#!dVWPV-5~=Eck>`mFj>-6ts$G}m6Yard_Z zdm6fVT*6tG7}X==lM^aH3F)FXWf!kjO$V+(0$R<@X9oySytk_ux0}QGKsOfxmSb}2 z0C=cdO2^O-dM02NRy-$tktyzLNtPA8?-|}7^VM^r8MBp9rdOEv_;-jG_j+6Vm+)S% zvmXEi;j?yMH;<3bR-<1XF9|iyXbbC{Rt+2J)4yi<0w-yQ-iPmJ_I_TxAPhZgEzX?$ z(luR5ax?LI@!p_ou{$`u|3PQ4u^H5?lr{n*->sd&t1f487Fk=+W3oJHn-HpUm0^3q zK-P&E@6XO)<-AUFbU205*GX4Jdon(Xl0=)0kQP2HXCBXUVpJfQr zGYSa*2dVHgCxO?O78rcjAyU2{07yxs-OzWpFb}i&WK)zH67(=|BapD*^#NJ3%fjn! zEXk*>H?*McfY1jlio>lRI!oBbGWfow=lrpbPng|{8n%2Acl(FXi#<3se|{)+L)7Y5 z(bSh2IX^4z#AF8zO&BXC0De|iJZ#I1RveJ4UP2_~40$e59>NHh#(>t`j zF~40_+*~iCxn!YIJXZ&|Pk^^t7TLCSPlUEbk)L*ZFDQGtZPqw{u-(<*!p-rGAMtIl z7&ockQ)|h*tKg2F<-pQq$Ski>a1*rr&VYWBt*I_ofZU8A&znz&C|XpgIf+XBAzlV= zE`wRf6=>3+POMy>tLuckppWe%2K3hh#~!BGxFY1}GyW@PodycC;ES~Ndfy*y8THs$BS|E)Ep61i`I#J9={FAMeb@z|_gZJmE zNv^wXiqU*^-iUCE0Lc@^*}m^`=+wvu=cLd*(LwHvA=%5VAKg}3{*ZXBTV~9r?>3Y0 zyIObEEK#^zfq;`WCuXv&5!xfq!s|M%!Ge0#{(DDL^St$n7lrYf;Wl%x&VEt$4AMB4 zl!G;zV|j;B@>+P*0Crv(!vh@t!wK@3bTqYbJbHn0oiz2`{Il&$gdQQTP_+z4gbbWF zcvKSXz)~mjsPDp_+v~5st;06w|AKxy^>D_!mtza{k~70lk`W2|FK^?MK%@ShH~>t9 zI`ri81U|!gz+br4v1zFG_K^**BPGb0x^YI_YTmGeeEqubAIT2&l=V3Y)FO;`ysCj| z166QW16(b=^+Z(N&5A_Vx4shb{31L6musD727!|HApv1Zln>#n7c!&u)K2E;H8?ln zrwAuhyP~;BiGxV^$bn6!Tz$$xP*oZHy>!?msg4ZuWI@{>bB2D|<%z41JwM5DQaHOC zYxMWQxEPhE8TsB<`6b)VZlGCfp8JHOJ-Wu%*4K&w(v>wCH`i}7#p1k3Fs_~e=hxh?z8_)ssBL)Z|dWpRR(PBspJh0^@vFn z2nxWb=-ka^b?v7^1S)jDddi=ZmM-Hf@l|86Oe6tjHk1s|E0A{`0 zMXE>EQRf_r1;h+4V*$n*AMw|9tx4@Ud3C0fFZ7VSP)QL%>!raE_3D+eiV(j@C4f>u zTrT4rieE>20_4QMn49RrDPT!M27`a0jx{1vUIo#EZtFsRH#{RD71=M&#l^Dka2b%& zUTudn&}yQVD{b<~Mb~c|FEH`m%a@3bFlJ(b@v4Fg2MMO28zzsaaDw)r1fQrdphi@k zLqCx6YmSg*Q+ZkZrs^of3P|a|V}=G5 z1ZvIDp#L8^66dGL7-&wf6K+N3$9xX{6N~(T>q9BU2ldsljr2)Qrqv}{rt!e@c_B<9 ztQ*a#0@{gB8r1{qa|S9v@bSi<;)&{pz+~bcUa0{GBCryGA{u%{Fs)+X-uHJtG1DI^ z)vO`ri0r(WvsXSOH(E>q&0ak;!c#46hB)2?#V7Ha`hj(!$cq`_;bE@+jHg!;^*}{h z&?KBw?S!cYQR7&%boiS^`*^*yP1n|fy5meqU!;@#!DWUhzg$xFizC4NV9|s!jWyC` z>|0D%Mm`Fs&6CBHDaOj*4$k>Q>^j6~kTELf{)p)n*!e;G2fG-$gY`XBVyc;mgN$Bu=_QqS`z>3jA5`LhG7ofUm^e_w z0&Gf*ANxQr4!#${PCww0kRu!aiN|I_L4|%fFnif|&uJTZNBgK8;2%qP8w>NUyiH0Q zc628X31@KJAPOBTc*7xB>hI%+@$HO5y3lx64)Y4SYci;^9;iZF71-cqf8XjgAv7In zLC3=0@R#!_C$NOzh##k|In7F}vAqbkoVHOiJUEj+ zdUM^_&~cPq2G+cBBPa96~|u>A(OEL5jtZ0A!yzNKRrt z`?{VX3?_*!0SZG4loV?tC)ar_x{aPqbI>YrJM^n}rz-fzq&VoZyMF0VjB(dB zSlk&bPqke3KcR4)?X!;_M%)DmRAxmj2E&q>`^xwixzTYPS0IW?WGj;|w9+L4gzEoX z_NsX%a`vFee|OyP`oMaF@NrH04y-u)CsF4ipZWkDmP4Oe;s6qW>oT$@m1}mispdGA zFMlW=&wa^k5Y=!HXXe9v!1=ifeV3G<_Q;;Pc4wz$kEyg*B$$dPR^A#-_GJh3jr>|;VuNUYiJPk%e1nmS zGB+unoKPZ(Fum52wX>n2ayE@zOo0p!LRaF_#N{qX6{4f%Zqa|-!yJ~BYEMJg!-}7y zjiZ2B@E9b)nE+iIud2Cn?j}(cF=z0AztVrA(FJmp!DK{Lb@XWFdF{js&}LC zH{!d6t&9*9=$ji~xSsBxfkJzIQcq*j79R)!zG8rSZa$PV8K02DobQybj1UJouZ~DW zcG{CA!^mkg;~+&^3b@a(`!L2DE?D&H&OW4>f#Zf+L%M2@irDOWEl@?>zF4=I(H@V| zU$W|t{6`*DR4ZRRQ^vQ(UxsZmx!&je@24}ep4wK%GL1~NtNAW|qu(WSOtm#%=l=nh zGM=`st)qfbbQ!jsB(%QEo>rgr%*y_m+&re5-fmTT-ltVJSxlNNeo#e|1}hO|INkfb z=8Cu^uQAFHBzNDv$ojnxCzcrB=330N88mgBO{v8YMHCS=Wl=g3^JZb(M5u+oHPQ(1vm_dULyL{V0_dF0tsQ@;=mqrTFv(M$bwNy${myz z>Jjp-YFI`kAEw~tuZbQKQ(hsv zf>43P3yRB}(MF-4tnOw!W81S1J1{O?sM6P*kLWqCkbG%qZ|3F{U3nifSHqei$^E09 zkjK1DgIs6i7fmuDJ8Cg_@^G_9YL=~Osfp#3LAO0d9Ow(fQi1@y`?+D%^VTUR73~45 z2Qj}0h1Dto@-rmZ)CdT5dWksr));OWW;}|Yom>h$Z=O==a%RMFG7~yzfl>fRpO2p8dO;s(6Wh zsr__571EwP-}etBCA}b`a{=kBm+DKuixEjuke}uG!}Q&FjglvPlnD<=Pw)2S12imHLS>FcVyBxC~_Wg7a`CrL%}Yuz8@OdKk3Rrz%6k%j(=9 zbOrn>2i34gi9f>X3?fo@r_n+p(j*upBAB*R?Oc^xEU1Ls2 zos`SxMrnQI{r+7c7NCW6h#k7SmoT2iI8u4|bb$W#eVtrL3f|bGB^~;`?SrR~fA;+i z2iga#16gVMeG-aC<9we8m-Wz~MeYmJc^swNNnKMPs`Lr*H@o8dhdsSkzox z)B1QdOeoZCgPZ;Nib&Zq>jYP_Eponn70L~AWQ~G-i}vq660Qe!3*ZI zgus(}IXVXSCxfhLoi(c>3HmQ!?Llr{%dFeG5e2QF;hGQgQl>`5aIUVqV4OqoI(dIA zoiyYqexfIZJ zi%+#vh7?J(Q5*Aw2q>N*O3Myvcg0F}{n6*U{Em@HoU}76m0Qxp_cq0xCnSP&PMiYDT$7SQS%oh9*C@w5RwcS7;zk`_>@^*Fs&l3x*K^s+~!aisBYpg|3`JnE^0LP ztN{4P#d*R)zmE(0);dV|-41#LrWn|epHS&Os>9Y(;<-jXU0B9-ADPK5!=NGKgMx(L z=3yO~?M4c|_x8SCHBH9)jJfr(}TXmkm4|_f(eLz){1Bplts{r$`QDs^hLy#1q2NcHgoEh?*=ictP9sX`Y_R43qT+&*7v1D-soA08H!cWWQJo1b0l}8_kR3yk~ zx+_k(0v9Lz7s4}!QSlB>rsck56e=>U+VfbCekWVw2#U=?o@wfQQirAHX;_z!qp@r- z5pFdk^IY@(hzS!w^>w?{mYmL;glS;eVItwqkS6H@BYTY4o-nJ=RRkk&FVXc?LzgCU z(Ee9IHm+-Eg@U85?h$XBPl4#wd;w0EE06Qyuy1EYqqS1^I;_c-N%38K&`OGUI3!fLAf_U8>$0Poe!AjK2*4X^C@-6&XMs^D4< zxFASsia=P-kJ}smR^K7*louO-KMZ5PynFudN7`=Y7g~||qc@T;f{^VBjX4xA@&4U(WOC9z{rztMv<} zL*k0oydZ^Q`*Rzq5lLI zgLO^MTUW9&CvjC+y{2glmzz)yX8qNGp6!j@9Qs4L!`>@!f|!|+4V?~d`2B@rD*jlW zeClDBdHBH$Cj_wQh;C_`a2kg;@c*+{!u@P3pWNSnDH|`_Zou6(2ozvIfJGQnl~RpC z1>)%3*mVn8;*I{}k(|FZ);>lC3&`bi-r(PaSz9x8uq7Ki<$Yc%!~;^+TzGT{dg5@rRdDO zHR5#FLlr7Z@N7+0ZIRJDy9_nzFCsAY{dV><$ft9tQFv6RQBL{G+h1|YNZDq^9OMj} z>=l9z;urG##Nf;uPbO)ERLFnvyLP_T$dB<0jth0&0|IBIc|!kUlGf27Dk9AYh}IJ4 zM4-yn;Jo)8!jE?@ZX+VHR+-5qD&e!epY<|~Y1?PaZxBM7e^IR6gJ9Wz4lT$>VY*eo z7B(7bI;S#1llk2#^OoT}6eD`KyUt&DIMTeUXWJCW3C&ac4?(Z`v$G=(3;HQ+16>P0 zNd(c+FP}$%=WGjhyA|CaP{9ZvYH@1J2l%j)0904pB#Ylf4p3~S{ZY|m=zrzxcNNc< z>L*#_*yjiDRvH1+CLAgf4P~n>obnPNrAQy#5p!Qa_ z868ekM-%uRQ{p0|JWk7dTpQ6uP8uXsYCw)tj-R|ie2^;`iKiBCzNK)&F<^L7{z5%R zLE}UQs9?I$!Aar-7lW8dCQd!cICS+g*3{!RzTh@J$={h z$iycC9E<7#ijM#OET?tdI>KJC4h!ZD`X%E>2xKDtyVn50qeq9B7NS-dBkdM^?KWEi zmKp>!N$~xwkc-)SDVmTAutV%+OA!fxI^p-rS&>~su6@z`O1ZJxh2yrQ=TiQ&2D@0=tM#Z~ zC$#>QJXv&&s%C@GvSdABGGa~ajF0Tc>Udp;4~BJD4)GNGy$+r{;T3M%y811@F7B54 z%teE|nxn^AH=k=0Z&rrYv<`a6-yf+?(!tO>3ZvZqa%Uh$MuVJ7BHKF4})|jmW0^BUpG5=4kr|_SqhIj$i7-~(AVKO2-5_! zZxrQyjVS3?^}__ zxnsc;E3cr8J^aVHIWN22ROA3Go&d9x%rc)Oe}>S`b`CY9*!#u%>@r-rs_w=L5qK3o zmxvBc&aQoMsq1pFvK5@$A<6~z z2z1|R<%tCi`{YM?3TG@-7d+egdhaT@BCkF_L^Rs!p7?(Dbkx# z7P@Ss(UjpH|0*u?opW$z%saLJF!{!&(D=r2;*xMhs;Zphl}!j5zZpeuhzSDG#P zm_%Q0;I52BH2&LuM<-eMn)A^1gbIKNBYOLG3oUpakuf2Dx3PKZ*i=yF!U+OC@$VZ7 zTyvnIqV|fV%Kp<`)?F7re~(|3W_VFfS!=20xP8jy&2E@-y7+t0rCDxzz{y0QCvmLUqDex$RpMxWz0mDN_jipD8b$R3Cfm{F`kEZldi#V3z}G1il1{6WG* z>PbZoa{x%q-p#La|583A6KR)7--A&(B;abnLC?&>vzLNuwM~%CE<7%f3C7o~Spo^6 zC)b0*3l(R^pxRQk?q-0I;@PzaI_but+taQ)6izqQuAAn_2b;3$M889I8-4|YeF0b` zI0iog`Ns=ln;IGse&o|#jO-t9;Gb3l?}bYIg^vU!EEaUN!Z|hU?n(;f73-~c#5W)9 zDHZvRz)daeu=9(gSef{Vw$Ka?3St8aVcaPxzdnD)4AS2x0A4+je+{}C{Puk~N_-jb2~T+X@qxh8IRD=7bpiSW*TxwJ*I9(r?smr~^P zwD+;m71yP7V1{#gE>A>xS0DX;E9-hCuH9g;SfuzfffL?~a=Lu9Ze3QpIZ2c{Mn;Cr zjxusu*XXR|YL^H`9emtkLTT|v&ac6>JHVPZTiCP7BPAwJP@kAQl@`}jl12aJhLXFKtReDIKdE+_9~ z11u<(;qbAqc~c6wx)V*@ej%agjqn?hoki^9w3$Aekz4xjBu0u%L3qHBI5@<|wuJA^M}w>xZ|Ai4AebXb_NpAae5(Q5aIbod(; z#Vy%uU;+T1>Moe-4r^M?7qp(m@~i$z?lm=iC(=_;Yf2xO9vOd28vk`8K&sD|pBKjL z8aLmX932H4V11W^yY*XI?V#KR0n$}aE73fzuC7!V4Us~a+spFXB{|`MWgIW8jZQvD zBsW-Lv%tg@8WU426e}rpBkWg+;SxPRmRA-4vdS-f__n5+O3#&JnXib4;q)SwQuV0CUjY=M1T8%adaaC2)a*a`?HC9pznqTHM=Ppcy z!0s4>O+i>5ZlR%E;TlbL5t1;rqDO(^bJjzZNDaTXOdV?tFY3Cx&0mEbEZ?-5$v2Yl zGQ65`>Xw%C!0l7-ece8iq(ult{)O8=jm5C@>F)lK3p48j%kDfidp(AaE>X(VF;PIZ z{PtD;it|z><)xcnio6?r{X^~eLr73?N4EYO(k)L4ZB&lI^$dUKR{O2q>E62}>YJQX zI$WVe>TL;zgoYPUV`cAkR!cFnx1#DMj%Ucpt;RxB;PEu$HFG=3K3waAzxct;bcRjp5{W@QrF@ zOvl+aD%QE_w#F?six#1w)g`d{W!m3hO*K**Jzo6CKcDiCi_hiGj~`xdFHbul3q^;G zf1Yq{&xc@eInX*D^*cXkt$)3{MQ!m%8Ii5l?t&1^{`bEA8uqcHFUT9b8^N+1VY z@)s8#U0uUIGfs2Mr(=R{)Il)s(5pT&J}+OjYioYk^DOOh`bKDZBFhUd-OqbTxocD( zm#vN&Z0b3&sO#7F%wmAo5`V<)lVtHUoBC3YFAA zXPW#t4QS+TWhuAzp#K>16%b7nKH)Hor+%B~!4nm(Mm4}%1d;BDiX`@a1A|5E0@0lO z0uZa4g1~^Pc-T!%Dp;uYW%*F^&PSha8j|ZD&>aX13OoQfxOOIj_^+zN=P&yd^vWPO zFHkvR2pT0w%NpPctQxzevSWn!fcsh&y6d`7KF=+1VT#nQ$+IisLlQzvuv{+~z~CtQ z9`E!dmY2miW$X2=hIFYR>HvG8zHRSvX zgbG&ghop|8_;oPwq<^S1ohySx6^|*;7p+m^O723_DzkFo zTj3(yL*jGtsj!(MD7CSX$>aeT@nU)+F({~o{u0qPB92Pmq$TLNVXDw7lP78V^e0z5 zJrLnvSyG4zUeDbVR>_-fxnJ~c&TVR+v@^dY5~uP#>?>AC+vNKac~eQY3L6(PJ7ZF> zr9Hmn>jyz}!AfOv!&bRxGY~Iu$^jyF+s639?#YsyN$(w#7!c?SG%gp<9Oe5SH80%r zS6Cr|Ay^?x+fv?}dI??3f{&KDIMB8CL+N$J zcBv53ooz(I&j6FytI&@=6YukrGlm0z;<>&KA&z<`!_&(I&;$MhD4V# zS-*p&*s*wo=5Vk2VmAe!%n@-<1ct+9S-K-7+cnwBUQyJF6tb~A-QoU2n^j;8#Ru`N zAg6744L)(Qssq7}9U;!#@}0VdQ@;kHi$QaS@8YK$Kb8uEfliv2w3;VG7uUP1!6M={fF}rUV!0 z4lHHHM^!s1{sgz>SJedZ=f^{H&#?k+WbHG+-Ll60vyz#OHs5P)_ zAP52o9@Ky`VmZqXFxej-L8)uGyF3c-Tz)-Jx!b>5mApSak+}t3G$WiL>KnsZ&;a}n zCB}~Kp7Gq5@^1a0PZT}nhNCqRTw$CENH5FuG`>?UQ0>8r%+c$jB247~at#J9#|}!| z+o5sE?Dw@@CgG2!{=1KMZmlWqM&XfN*QzmhyYUSGv;v8C9l+6UN1$!y745SV|ue%(-LJtoWnQ0ZyhsOLPrn3dQg8SrslWeykA;j zQn0|j2AU$rnhCN=y;-~BA!>Ig52;nZq_RI!Ul>Jc6$n*yY+mWjSCGwLNw{k|^`awL z6@6<4@>{y}&UEFnzJKxNtu}M{aAL^``(5IL$`Z2me|EHh5t2RxAe#JZjp}np1E@NX zuY%9Y!)q;f2;{r6cje~ZhwG>QjlrJ4hf`5k*R$`?PD?mkdi&{g6xqxZdB%s*rf z!iP@s52DpFMlfH;u26$ zVi6K<(5$Bb_HrVEyJu83=n)}AHucw=xo^7zhF0QNSOLP|s-+mLC0tZSa8y8N;4}mM z>P|?%nE^XDX+xqj(V|6Us_UybF|Dd(x%VCuIEGE7*`xA^m(^Tqg!NP-9hBf(Mvg~LgSwuqEI*7Id<2@_@#E_Z}Jo%`j)BmUSht>cP5kOH89 zGt27F*}bnfe21$+zFD9034{mmAG$#TdmbF)dv005LOJ#98HlI8ZZ~rX3AOeWB_htl zlj;4RuolMMN0gj@Do&?OZA*R!o_-+g*B8teoRFREG zRUEY!O{NpV`xKm9%XG%zG{E~L8*eEQ#gNr;P-`ri>! z;OC#`j+{z%ze(X!LzFxr3>T_%6OZ`QZ(uq0CD6~M$PaP7;QJAQxJryFyBzkVv#IcW zAMqSNjp&)*EfMKQu))r6$t#T=nAA+OQ)_==l|Uv}18rwNdz%+0<_#>JM9eTFy8+u_ z+58`+y4zFbc(U`k(T-xSxaeH|B#vX^k&ur$8ElY(-T0_5($%ciA+|QQ)f*F8XA~uB z?P&uGwRd$Fv&=ah3Y%AtD&Jp;ZS@amculJxpPf%_`c!L*4SG)r6_rYPOgfFNmO}6| zS0k%Cf5fty}Ri)(5H3}3*uD$XbYkpNEzdmcpyVX*Xu}KW~Z+#@wORn>yj4* z$D}p%Z?BQZA=5!%LvbJMvG0hfXE0;=9v?~{b?TSnK{|IA;K`Rl;@#G8?QoHe;~tnbh5ANN1X1_1j7&xtTf zlluo3gX9Ag9{Bxt5Nweal!t1k>dZw@y&o2|NBbKp*=H=_{XFHVf|A}-8o6aT7vd|4 z0U{pfCbVrALBbRN`^Cto1>s4x!tP-SosSvP{z(6U-utI-$jc?OMK*oPH;W%I!t#8j zVI_{^sl{enri$tb?ozl7Rm#uWhnI3i?R}_~v6b`bV^TYF=L_AV9%HH-2H7ue&WG!2 zDG4`2RvP?FiPESIZJ&jrD<#rEsaRvR(hDE=%H~EDmAZg4{nD#{i!x*>U*kG8l3CRNq z&}RO|Gy&)``2dHXKUeG}2>B~O?ICykTt?aBik0122#|taZ36thD~Kk18hyrx`*QAT zzl*+?rA&)|Ao}?bWF|`~G&=YB`A!O^%R=H^;EMK|muYYw$=YP5KZ>npf)FXjF?yjj~p z{nS)hA*O#L(Pg@syphoIJLT7MpDPi*c-H>ifCzGK!^6^LaM~Z0st~8wvefYWs6Z}% zFxGdd6e-H)8aFjh)r*^%6ey*&#F5ZAqkiDdinnYDUpKs~;4 zw6}jM-2yjDYG*!f34<95DjQnE6^V64GlX9P1q*7_Mdwf5Ju0o7&w{slCsKzyPCDu# zDIDcp)xFTCe^qsIdbLxqV2jsi)gjh;MvKrRec`Pao1}!Fz~^3U@L_6W4a{gL`GKn)&@Is|oo9 z{HI(`4x1=Esbo$_t)|kCShh4#^rV3jrg@R5{d6gS^bsEwR&BI0F?NIeAu$vkb&RI?eNZI%Ti*9Za- zLt0QNQi?DNGICAaMH3JaO2luo+iH4DVKWv|Y`KD(Hnu&=6CkS)vra#N;589!*u{K~(4!vb8{m6~XdZEX z(TbfKxb(5}zqJ0mgH9j!hj;w<`T?L@0}}cE+7$j>Qz1VzENF@1nW>25iNyYJ*`Ly4 z*Dtq_MBg%4M@pm_7$H}i#32gJgken3=l}4a9?Ld-@kvVO8K(hH9{(0 z+CXUGM}0xUJ3mWKqeduhZCOYeG=?#u&&|)mB zWu}{}3YF)a*IhaiQ64{z{C<>-Ww{Sfpp411GkgYi8 zoX(56zYv0ks-{xB6-k56RbF2qXR0_o>T#E*i!ie^_?<`W`>iwuGBG~d48`J=x$1nL z8dA?bf*e-C7cJ@^_SU<8O{TyKmjF-|BOdqzVZ$o zxjZkDd=vegF^Qe|X5@Xv$mGUBd0~hnlPrYv;`XY0>}LWi>*0_wovRL_$7X`J-;Mbs$vneQ^Oz&UKrMn2*a;b-YO@ z6=k4OW>M%xRgSCPbY9-HEp>)O79GR+D->NJl_bz0RtN#bj|^wn0&1}VQVMJvyNEsu z$_SZsZH``u5cab4D(!wairu=sDs=;BLK}VSw$;`rt>k9w6#war3f;%enuP*CnVCbY zx|~QB>__VssG zPUDAfEEvgMBwYTA@LisIRpcD;wEddB(q$F^E6np;&Ji4+b06zdIfq$HE zwvd$RNPcQmu`*C9$0IRg5#jtm?(;Xn)MLRGU%B$9kF?41-q0@F09Dg!Vz~hFg^oqp z^X}_+%8!)ptt*AAmMw0B9JA4q5(W^3e!2XV-dVvfJKT%6mSY+)Ka2iKqzw?N8QaF#J`wV=si7jZ%DV0ZsMJjUKa^ zCKr9S>R{!^7f8E7P0hv>!`#4xBctnM8A}K`5ogOe#0DWbFiY22KWsa~Rh1-x1`eUe z6?15R4JwzVUR#+lnd1%SrlIk8fJPyKngIl{$bGZncCwxaPm}EDk$t49f`L{pt&7CI zbCwh*0v?#aUmys+dJS;X(*Ny=?SGn$Qc>NMqCXXq#+Gt?)<*Wy6U$QWUDP3(TPQ=S z8@#e~prSFUscY7ds_{FmJzEYDViBNVO8a+N1#nOaVEO!r}N|Nqi)y_ddq`XzC4)Ha-BeFSG!BLF>?YIVx+ep^wgatF9}R^v#q9>I?9@ zjkI3^?DJH@E93#sSd@ikJ`*euV%77n(|6X2*IWL>U%rOH%Sf#tgg`o`&uh0+DNDZc z{tge&>-^}d{PESAgVq(}S{=RAIlwC#fqwg~40lkZ7sqcmE57@nT zP(!HG!dbqXsX;&9YT8Jhz%mihv6gPR8<8QQXx zypwqR1SOjQ8^DetXOWNI6fCBwWU=3dHIHJ$^cdwB`2IY{ldC9xMkntQ9IC|u9>p2~ zr8PCE%Gr*JyH;s1L4An}1v^O5Eg5ek9sCTru^Is4AyvZTt5G@)09E;aU#;3sA19e$ zsw^fet&JHDKY~je_|EX^mq~71tL;F)l`v=@dhuhv1`(hE-~RVi)fB}>=@tIB;Vc45 z55!t4*q-b!1~x3U!#s5|d@4W{|8V&PE9Lh^;p9$qd#d;f?kzNsz(=xNed{DDgmEx_ zZ{vgXHun`2>60M$c8~Zzb0V1%b?j<~m_q;?3Gu{WeNwM7t@gL}NL&HiokZUk(+(ox zijX<71jCH{Ch{$AmPDhlFd5X0)|qZlH0V4W`a&GqeGGk&a%O2uHVGNMe2qM77xM2Y zwzv(xd5vuDsUeAh-eF*uyGs(p9TvHUORs3X?X9RdDL%|hXZ10k^6Bp-vZ=p$>~i#9 zoU=ZV9KOG@UvYkTeIk$R$OSad5ib?`exx4DO4$Fjx@+_DrHDeGP`g>{)W#MHA?>l0 zLu3;~phIvUW#lD~2Ip7o(V+IOJwNN*1t!Dt+u*!+Fq78)Vht;M!BS{b^^k22vd>!= z?7yw`pj0L5IJ;`1Id(+;et8||x5PzR7?V<(&uQ=PVfc4b{gaPH90TYtX#ez=>OTcX zpA(r3>x*O_Q$$z5*pf17u%N73(r&@| zz)RIqt*?Uoh0))aR77RdazF&zvKgB zHhhK1rPeS=37Tpo4f`5>+~y6b;qDY$9IYO8gQHDzmV-#a0|gz%1*2?F)T1@0t%e`3 zI3WPtGpVKCng9gkJ=vTQ(xk)u&{Zw+bTlHM(Vz2h+}e4c46EiNMT#dg+i(65dYu@E z)g;8r92sPsj{_S5?pd_G?mF>F6_DQc?U7iL3kV-Doc|@`0p>>NWH522`9C)%^Zomt zzluG9#Ct1eP9=u;iuO`NB=eyi+|4U*9g&e?gY;W-|stM4V!4; zp0RH*V4vx+fQ9O!MkR3+Br3*+)cF!(*C65cgX2K2%Kn!k#+`9gsrSO%<>5YW3p({U z+nTORq3}iKX37NrQ#Ur}q{kR%Y&X6R-TC4EvRcpD{O`n6%dN)QwT$p9Pw(5cwxk<} ziZ`;h^!6$J4IiDelxlU$egx;(6qbY+q>k4?539ZN9Q;!J;iJ!mK0UzXh+@vqAA4NFMJ$xxy^Q;t~unabi``hcXQAV}QdA!I+pY#AU(-Ty(q052=NAfWtt znZIJ}``#45bB6;{x3`E>9cWOAjmR(|W|&$D_j!3IPW;rx+}q0Cd5sx;XDnn`P#q#+ zQU!k!yjLuuT;mY?1l^Ph_p2@37iJvFkq9{v5S3Eg&jV!*G?W!u%c#Kt5Ze%}aEw|Ao{ zUCC&(2+%Oij|d=q>Pv_btTEdcZgxo14m9RmdZck*ZYu10yg8CCQj(0m&{09Jxz`k6 z#UHVOa%s`l4{x*AZJ8LbV@Zvd9}k zSCgASt9pIxHg3W`8OC(pn_s$_MalvX_g6d2RNl->bxxyH(@$E_2Y~niQ{>+lk3@t= z9Tuhtuw6cH04%Df@PFicAQIbM$LqISj##7##LxoJS4X)6c9(dvubX}fUk&Krk&SK0 zP#_Q#zzq>2a)slzT&cNou|{qfuPhSY=s!(ZbUznUoiXJc>~em)6!08*4t;^-d;E1f zv+C_YvS*QDl~UHU3wno|#6p=8bfC-v%b+$WcUZ9!Pz*fVrA@c(9kwg;v(6AjYTE4k z!1P=M*Em#4K;ZmtP-%G`_JB}_hYi%kmL+rbHKPg)IRco9mq+tCc%hH2*^7<*_j%Y% zSq~_P2XZEo+M#DV^XtV3WHW3f?_0vuHiym){Ju*aRY$qNfMm zL+{@gL*}lwUA87qNao5A9Gm>GqeGFEE}_kbJ)Q=HITbFaq|^-d>s&yLv%n`%Qo*!H!Pgo8ZXh~ zm-Kj1(crnxxfC6ZCll<@Y6^uvvD z_e$~#eP{Jo%Eig}o$$VqUn*1;=o1MZEPxXqM!10&`}p;vSj<=H<;(fXh6Xk;BW+G2 z;><_E9Auc&x|kOVy2{ec;LrQz;||(+tdCeXUnot^Ib~G z%vQPzTO+*Esc%@)(*@vX6jeWyF|cP?alIEUN6S}cxuhVN@p4!tik1c2dXsvNJNNQ< zCW794=fBDYXwRafLf>Px=3qTF&+n!zF1$OK4W6q{J(gvDvVN_uk&K(>h~M}&Y+?vG zFINX@Vj($mi*GG_YxplN0}yurI2>gFKoSs9w)77m`FZ)=P&NX^am>z(xd3f17#h?? zj)~ z&-ZC{Dabz2;vi61()43J#`r8@{@gOt4EwZ>i9>ABR2mH6Jd5^96-k{`Ofoa=jmxUAG>&bCwqxKKS9^JNI6$Vhq!uT3|x!-}Qs$p+dx zjpssyuJh2-Q8EhKic~j>)5w>0mjLjIYF_n&hNkz1jdl;Un=^e;;~8_WJ^#j$0pUO)K#hv|1Cl8JxoHEQJCIKx9Eg7gX?mu;?E|-(exmZifxRe!ge&1=f^tfcg2rbu^>oqU)v8;n~#znm*b^%)K5jRKuS?mN8IEJW2LnMrt}OT^vTw z{i4mtFR%WvJEO&!Xz*VbgaV++^E~wy9&c^ekB>qi=dZqpH=)YCTEoKd@|IHjOUbNUP1tJYz>Q z+e7xGo13gg_B>rTYhh|;7(>_wwI_59QyxcxdLzg`@DVn&3UYRL80yM`Nfz9D*u|=1 zP+@DFQ&&kqJ(?Z|yiWA5^#g#jK^_tQ6-(~*PwwYGoQ)T|(Y0Sy(o?hFc3Xdwsa;S=44CXCx$dkcM1>@IFa%;^el-~vB1 zGF95!`h;}wE!}E|jBZ)CvKg)8^OUq4)lZvaH+(?07xgc=YK|Xh+`!c)l`b=pZ=PR8 zo|C^T69_^pkTy+E)N_b~DcOZ@G|eam21_}qtL`PYDOBU$ry;Gye~OdkT42aX2&N3u z)nr3QsXmz@4jxHbJkqr(nHP`!T!MW%+4#VG@Uv1Z(39=;Ka+%2^thyb!jL7tcYg@{aq zgZ2AcM0^H@_&}eGA!0B?nom?f{tq;D?d&<+Ay6Br1z}PWlvEmEVS;=q;U*=2955J& zT3I+vr5=e$%J3%u3E9%n@CT?MirYkt6JP6KLA6{X_ z*|a`nB_%RGh+?IFit8DV{O~<_a&AOXokr(Vvx;NtRv2q-x{KD;6(_xQT@x_o();b3 zp{0U!zCe9J8O-5@^#V(z<^c+PpWiPMyz-K6uzR-}Zv6NkDp&hg@OpC{oy#j_EZ!dM z=Mrr0N41VPPE6!yE82`uz2#esn&PyxuQ}D4>i^xag;F~NLm;_?!q1aOx!D3~1W+*qmy1hdup+85|1z%D#)%{pR;+6pqQ*EZXJupH>1` z*0z(`y3&#EHpUAKlb&u4yggzk*fLS+Uvn`$j|GK`f{nxNg@H_2W*b=PIHnw%InehP z6kMxri&NaJR|M8z;Xf>!x5vL|N;3p!aW@Q{7qrIWZkx5c(cZ-+jmp8u6G#L(&1dzf zVHMB;vGU|CXRz+h={1qePl`t5!aPOhE1dW|7bLd!d;jYBnWncor)Sp#R2eIZ-|-U3 zD!ata&#t)LDM#)$+_a*CS$$b5JH(ovLa-8q3IuBZf4eFS761#2c!&I>>WKO?!hlJ^ zouQCGlOO_{!*}=#cPrR8jYpQz2<94@1u0I@F z&agic&bAjIGm7`PSy>oz8x~xgIsh^fAPZ~JY#5CS?h4k(v1LO1{SIywrcX|dsWcOw zZ1Ra#ca}au%0P%MLV`8MVKDZWKY1Qo&14~l74i~A1upMk$sm_0HWUe_myDcQqG36rZ1kK3$% zMG#eJP8{^sq1^Mkl1cc$B3$N6&EwaLtyPL8YFvAiX7l6LjXl+k1w_YhN;*;!$FSXv zD8nRJQPili(lYig7@;ZzFO?Cw)k9mBLHENH2)r#WP*Cp%B)P?ScEBfv$@bvv%q8mb zV$~fJB^ViT+*o{>>#PM8Ozl$+5XMjs-J^^@Swz(_sAam$cAHH z84(6-Aw5E-9m!;=i7a@)R{8c2-S25_=DD+J`df3#AzoNsgWTT(q>d8YbA*?r#u28; zXWatL|9P7B3{!Xzg)vNWQ$dq`NY9^)W?*fDP`qyfCUohahE7L>MjsaW&z6*nkKi;6 zs5SPo7MoZQ2{8j)BwN*IZ~V?Dl+;`L$8Rvf{>yi4DIe_H&Xr!~4UsP{{6D4noP}A+ zj+o<5HyB7CW1lFIK2=-Js6uymD`m4Wy@qDX%?w?}2Q_0aby~OuoLs+a2;zT9bpk~v zx-Eh6?<$H9qDp;Q77qHLB3cH*->61T2ok8><~e;jh9?vmS!)N^6uqjSy6`9S_vO3p+P7 zLO*)up#v8^k;QZd1&h|q_h%^P6P@V3!b1(|2g&Erf#lI;ZiPPlK;ENhL;En+99L0H z_~r&^C1VGRT@7tCHg8FY-w)V z&m$BJ$6iD|RkJyiJT#;td7-uC#zsdtty#y_p0j*|v$vuvR@Z(xj%kH$Hkjxmh~i)? zHa&4%J}Au0!+tHmis*LnhfZ-G7m?LZm&irmU~9g(FEr%ND5+?c+j_nO=>ow4+WY_A zuu%rZfmDtEL1qyCCP=)+0P-UU;5^uh0;KvHscHRiq}?gsaDqNfK9{j_W8M|MFIY6L z1i&tz2`6Gzifa0kPrUz-J+8Ssqx{hS+&@vcz)U>zmCPh8O3-P1wC7Kq?KF@4xfnCk zq|O~G!8)%_mS2L8zoD-&vQwu{br#0E4QsJ2LRVgqNuf;oMM+A(Q+L3~zaG^@c|+TJ zrrab9s{uNrp%;|m~U$6bA+muZU^1jamCG^lDt<`fu z<0l2Lj6Nhy8(fHz5>W!1cq=&t7ZUEiSV4T2I=YuVsv7%rM^^8EYs`g;V7syt4zX*R zhSpJC5>Knjoim>SnxwR+2vPCS%k?^IS6lHM%Xvx7dD+4~R(oWLy2J4QXHTL+f`Ay4 zC<74lGyy^~+-X3of%7C01<#Q4)*fhN4r+puZQg?R1?Trxcn|GzrhM8+r@u!~{Z&S0 zuT?ZTaP_K-PBn73DUJF{{?-@nm*4VKjq-;1LOpAilA((Bg(NoRS|76?lT7|JwFenv#v~CYdnW ze4Js&0W58i2z^(K@9TwA_F%4;b>&c7x)$%^>bk!z2veA{dNS4z=UJYSjxy?eQssSk zlzfJJ*c+p9C6n)2S6a1J*^Nv4V{DBQnjsN~ZN@Y+Wb(2z%dtA>6bXMB*4U+rg{jBq zb}u80;vYHkUOe)B^+1L!~({?3ibwSq6Gco`4zE5=?W=@{^-~%&%-?u-;>$T+mMGu zgf8O#^V<-@OawrY7l7IWIW)hQ1ye>iV>98b3{z4jKlLa~Zf+%zzlHi$ zdoUV!hJpde*w|owcvl}zQ-+9Nl5RB&DY_e&3!ILWVLJSU zgBOT>4A8LiJB4bJGZ)|s843$1Dyksu;YgZOZrb=|oV=cS3X@OV8&gB*QUO6TUL4rm zj$6e8V`g+O_F?24*pS*7ot9{WJTTYYO_I{5A9Y7Fg_3!&=UIL2-MHyiE;x++C^&Jz z3q=1~h5=z=K_LA4<9=E*{HF8Z1L!<|KCEIAeBLriKcCS3<$nxFRz0i6{wCZh5PcUk z=nx?c?OGCZhQE*!AIZ-}4y+_5h)NCxdD|gEWq< zW#`$%i*O7+$j-8(896uQi+Bnq|WHS^S6U$aVX-7`GK*3*q z_Tj~QzQ2pB~WV54IN8rmh8J=y8Da`H#PUGn5F>AMJf$Zp< z&YxDbDd)WdKg@7c#d=Q=-YuH>+=0Jyma_kHli{8_&*_M30aUHe9uMFM9m(}d+opJ9RL$lJnD!bkXj)sRH#>LfEDU%#2*HMoV`Nia|9h2 z3XWzxh-l&)1o5rcD`Na7!P4yj>3zd^3L{Eqc-9=T?cNHY<&4C862<$nBust=TIBvR zI7?8_I#o7=qY=->PhQc%kY~46;NQ7OKde01DfX@YRb};}w6D1p^m?v2D`CW3vW_kj zg{&3gZcic=0gDxd@;rlA!Ll9k2HyOW2#KAzVi6f5gjQ9OiEWS}OgWY$QTei(G!WTc zD~O0BV$^8Ft``Mt#D==FLm6b+0Cjj)$Z_A?gw)%Vx^AGi?^C_=v8GH$0rs@M_4Rk7 zF{5y)d1P}Wc8hWSV#;{+kV`dtSEbyUAmqMt$w%2)!4yf-r{&*%hf!G z-=H@u7DApPv7<58E$uH*ah1SQU^Y-ZxK^o$0@zbnhjxp8(|(ce@489b`3~ z+xZuJ9w=jl}nahSW+}=_Qvv0&kDo;+Exi=ZdOHPbXOVe8; z(X`Z*R13yvkj9FQ&Jao}T*PRZMJmU*SBmMUNX&P)XpgYRI}4(?ib>P}55#VGDn&ay z=AfcXk81-sx+b3ry866+uAsuGjOWp~-Y_GG1NZb!=tQl2Y)_wfwM6;r#D3yuJCtbT zgz<*#PRsBzBpryM1&?02@TwnOc2cBB0@2)_FJTDjSR*aU>!?Y}`FTp1n&~uKZs$kO zV77v!HFdhBh+a!`uhUz`wxE#m{yY?o%nZNa8e2@89?>$LRSmv4L%eZt&%EOyRvBm6 zIrtTj)M&QUFkc_!@nv`r65M|_5@D0qQyO8j{n*7Rd zo(sbXIR-kPi@56gy3cWw&uNO@-Yc;07g|TMWGmB?N)EDS)=J&1pQ} zpxEA$88M9)2`eUwiT_bjKyfjjNknQG^P=vyh@Oh`;rB{wdW0z1whn_3J`QwY8_g?y zdPzI7V|>h}cI{72q7U-CswuJUZn9rofd&ednN6 z1#UT^3p&~kyzU-OWCOnM`DbJV_~v87?lR8&)vd0QWkSHc{BT@1?J3`=Pz-0=}-=2+KePC3gJq>m4sQLH%kH6XZwQM zh;r=45b*wQCf45^aHx=q=is@UN8cED8znvDw?M~UWd-s0=E?PJTek%{>o>#o;?!Km zFB4x)61Hf}gbGeCJ!T1me=eyWEa_|L4qMoeqC z*;;S2RA|OH6fL1_EcjiUTPZS7Y1KW7zNnQMANqzdN?rkqzG*E66;J#zI2Y!&#rFSl438&4ES+>~j#$TsRCgzZ{j0mEcyFBGW%d0MpYKxSS(N~!c z7(p(!HGp56?-ttov|~yHyM!G z%af(e@fN&?b~Nw@>vHfjQuf=e`|u_EsP(9y+TjrUDrHFGhyqoi^zyjn5gBb_-5)Hj zUB2vmvk@D|@*>bVR9A5Bv?4iEpa5|yFZD2^XQMw#p`YE(zTe*ITP;W`*r{n;g_`qz zxMGlStgV3eFxKH=_kO>LK`UR)+jibxbmNJ+)=<>ET}b6kULz)DjM!paazv2`RjS3x zHzxDVea$BR3H#B-jb`S%YYK|pRP{z&#V&uP0O8=&1BfQ-MYcD6v&V0lo2rp?vcssc z*Yx_2RcYuuK8I?MKB0?6`CRxT5bQ`J5`myObc7wYQMINhA}RO@9C9uAbs{sn;_(4Z0jwh@fmuDggLig27j+Xmtt6jFkN74kT{eO3u?32 zjz@~f!ZzA^(!jHrOSJopjMG-Oe5uU>^ICN|=nr(+mh-9Wy}Fx@6Zh(F@z3gG7U`zp z#a-Q1c0Ilf?~X>7L+y{6XuQ+y&BwOfY}Z9DmAUN|uH|ET4kPV{Q{_SLPJ;`S-G#O( z=-Zx{FV0$1S*fniru*ge^N4FpX04PP%NgbetXK&&(3(PbPkZ>F(IO2yR?1{@vd329 z(*sW#(OFkRN2Ub8^WOaUJ2@VKx3CNV56rpg858@fRzch0`f14=jF#g9-^9jZwGg_qD~HJy_Ih_Op-g! zF45u2x^yRVswL7^g2rS3b6J=5J06##f`{h}H`JCT%#4v$RJ40ET`lo}tVYeUkC#x+ zQAPVh!Z;K{Fb8p-X)^?dWio7JM)XvmAel`>VIKCYqV!pu+aU=vQ(!Gm{pKnV27;!M z6Ut6{Q&mL9(@PN;ICk}RlhsQ(g{*SG*;EzT^sd{JF0j#{$nIFJfx%Y(@x$|nE>A_* zLaXxYG)2mWDiTi|>F6g0zlVTU7BQ&msbhZd*Aj(DbgQ3m+%+BBx0<#}+tEaG%Cqa| z4821hQi0nwx`@hH9XhT7u(n#oeM#M21cf$xP4Yi|(<-2U6S7ubh*ooUvR3&X4&o^{ zZ}hPLU*aKv0rW4j0On$?DG`5n#>K45rQm9BO9x zEQ@P?P3uAU8PEPzKzB(amOHbXpgWe1uZ=|FGJ1pHb1D>|U5!|iY_eZKDa=E6R9@nf zmE(pxV<*Z31_P1_2r#y11I8qwQuddkMC!6;6=1m9-ZUoTdy1tWOpy6>k~pExd>^Iq zG;rX89v^@*IIz9ftKPL}Htz4+-Ff{abSkn`-E$_gC#Xy_jGtaOfxW3#)2Q0qPZ!6G ze_+zP1wwTGkL(WUKP+JXN7>k!eEF-}L#VhUsBQumhtQ+LDIrG4Jw_ok{!H~Cyi@Q) zE)Wd7gr4DBQkp()Fi@e3T$I{vL>B}KvWp7hHh*Fr{-E-!2PViorR5+4=^h=-*3(6t zKNU77q`A&@KH+V~bR4%IF@s6NNy>7DU4TR8E-F7Dh4~I%DA?H|@j~D2ciC3s`NHo( zcJk5bVL@=dcG~0fsduWO;I-xZV%4I69(57d{*-`$-*`QF;#nlT{IE`DEOkGoeo;Md zb$Yh{`{mKa%jt*V8cfG@%lS#G07zwrNrpiTqik2}!8y%^-r}xJqUTAWM>T($L$mH( z!dzog5Zp*kxSUiR_IMjcQXRZ-KKGBkP1Gz#f?M#rgMO*3%CD*@c*S1K{xp~6`|cMI zJ(ZV**U&s>sgk^QD{TiAH5#IInK2)ms`9enAsG-=2>JAbcqQk$SAMCCl!jGzYwvQ~ z4q78b$Vq`-C&f%#!u8ihv8fiepv;{Ft-35=mDV{gEBPb zoUY_~O5Rp-8bT}~Q=(pYF(kb8ZHYX9l^^*Ma$%e&BSkbcsL*J!L{Z~u+Mx2-S6prf z%ddh#R0Pj6PGW=BsnVSi6f?>&I>8xpq% ze@%%X8+8{&>s~s11WVnOG!=C|KQ?P8r#oXT;sy#bG7El!Bq-rqg=gp9#b*z`Vy1ww zW(xTQL*92fAacwyIg?>J&h9wJMYFFzE6_5XB((iZAicXpp$jWe4O_@|lwrGqTf!(R zPOY~7+RP}C5xIA_v>lAokSL({P5&Vi)*yJ6ynb&1a)(ZWiGC`Ph(u9m+lWi}*NW8Y zI}ZVkNz_iDL=`njd&rx%5Bu_jLCiXX+&iHic%Q?J6skj-0}S&>B{vEFuM4FVZoS8d zzG@OV<4kPm2q2c_e`feU#y-SR;YMYEu!vJaKvX=Sz(CiG;%`lk)ZAJWvekWZ-MAwM ze!UN}yPOXc)J+RHMZ@>YHZuK8{4*R+qP|+jT+mHle4<_ ze%|l=g)#3n*O(Xek$^E0YX%Qc=JiSDyXD2sXYUJ%KD`k3yM%7YexD|$o^gD8;wxzu zsHjBzvg~qENewyeV$(A9o=8%n@(E658wOEKp1_f9t%Lwq2$4J-Lmk{e136TdKr#cF z`0jHrjZrL8L?qpY(&JqRET#4yM?Up_ zqB))*p|jiv6OvEM>q9WHYx+2=ZMn=6CJ3DpZJ?dSCQyagE>1Zb?LWh_`s%JDsSbk5 zn424(AQzF1CVl40~~NuB8%C`Q%D8#@iO zVsTIbknT^M-zX4WjRl6J!S4uS6%*rXd>U83$bWl&n%wt&=bcul*!z6o!mG2|>sGYz zBHH?4<>Woj>3YchSET_+YNLh<)6e@QbRYVAr*U)aA9XvtHt`NZfibfWy66}`Q6Dcs z-1EtspDu!E^+(kZ@p1umbQYur}8LK|MmgHVwX;=r3tvU#@Nh zqw5D!(KGM@1mqaUI;c3+IQEu-1-9kJEmtm^)uY{Gvvvu>-|Dl z1+M4Twia&g;ld_+nZ_iW&y2Jjp-usbkgs8`nRjk5fvy(zY2_gvnH>q;?a~$u`R8}5 zNltfWgrKah&91wL$YT?>aU|mwk7InQnsA5`SIuQLRw_qqvzP^a4AQiwvr_qLGhhb9 z@QI&1FrJMK;~KQIiwD{VbiqVeD!&DI z=jg(nO!Fqi zW10N(v+f^BD%49wjuDru741EDH4Gz;D%;%PAw_<068|xKe|;Tf5urf&nE$1s{&SZD zBf|n#$YN4vBrIrBN*exzlkIx4$I6!zlrJ8F+ck=3lv^3O$>+TA^z8V6T5{_1A7TK} z-dY#2+;#31E+BfTS6a9i6%XF@l)FlF3zz@{KXIo0xgoL)7XUMe6mf|s1xd^I%W%4EtDO`{R!bTkf?V~nT(vYjau7e-w{!~0(n~9L_ zmS=wwR*GW{g!?KNE~Y=^-^Q;Wbc<`bH?=GN>jg&Z1BxxJVR!lxr&N+ShnKKg}YsXJtAa_TG7V~BjM9>ctDlqhtx&=k z(qxs1fuFpkF9>MFU#PN@6S}>v0|VJySzI}%xzW;NpOa*7Cf}l004%L9&8S*^SV7D0 z->U@BZS{|G-G=^;7hp>V@T3>GXEYazBy_3>Pg-BLB>kT_@z2E{+v2`{T(9y8brTaEHkeN3RY91-tr03&@?hyF5>{BIID6YgA$Uf#v;}u{ zvp!duhJs8-hFM(FMyCY{ugBES#es5mccNx7U)i2diSyjvp2^PdxnSpdvP6%+G^Qx+ zoLhu#R;k9VSC<&^$~#g#fND6|icI@sL9uJ~6d#=q*hiEf{k-P_UKMX~zR4VH=v}Wq zXeL7vz$TN(rQ)>)tfIiKm71q@PNly3>kzXmh;`%NV=~P%%G=ekw4?U zPdq1FN%9^Ex1FU+2C?OEF9FLwpQV8;l-!Up0K+dnYJdU4i8)wXg!E}j=`5V+!AOo6 zyM19MTKmRM1k<1iwLF;ApQ)VxJLOM#>J@HfpgAS+nc7vs>;w!Rp}5}6=6J#wZVD9z zbG#}Bm?yr#gpovKL|k`vll>!$jh{_X#HFu0)YVR~l*s}lbO%q^g=P@LWgC1wpKKX6 zkt;IxwLfDUO$DNxPn{Vkeh{=b05vy^`s&G6kUM@5yLL3DR)4&roDm~b1A2h|341{0 zfB_aNQ~pQS4tSjYa})%kXL<#>B2x_Xlw{xb?deDWH4(mEz4g$%I%i#Q)tKC{h{iK~ zzkFw=Sn8N`Tt|d^ZoVJ_e9uBY;u7CQohuLueOovh!_^C}D704qZUl%8PK2)RYu!{! zU$Q&E%GFShJw=;qbEGP~CoO{FkBJoG&m>n@$k!gTYJ(+1FkN)`Et(<}?f0oO{tl%UWa!@_f@%w(m$C6XV zPi3$LY8V^iA}2FkyFx+83}yzFPZ~4Xfex6cTM_=mK|E(L%Y`U6L&hMJ@FYDT)xt@d z*bts8ia)9SlTD=g-$Rz{wbktkmcK4$^ALL~Us0shr7kFFEW@BeG* zpaXCoXiy;herf#Z|HX0sRYHQ0U=aa@`@7;_N|0dDlo{$Y1}D5jde2aN_7fBve_^k` z(plL!wlF_?dE~77wAz zTntp7G=p|la)H(l3KD`{e< z16?3S_Eyd#H_YTS_XWI1e~R*95bj0wFuaxZbHe|#9W+3Xz5xZs7hWvW(>QjK;PhSS zy+dybF?92qQaLL$`?U7m=VPX0V;bXk=V-V!pUPnuo6<-<`{_E>!k(7sTGA*esR6Uv zHT$yzGkmB8Z2E!&#L%tEj0mc@jpG4`&_O0BGp<^*l!C2GQm zLum>|+w+dTQ=mW{Sm)!E99#AuBp{>jv=ma+H-z)W>YNYZw|GVa*#=A%8n9qLCd?pZ zchJ|@z2W52stUqgc(PkBbkWE$mlJ@-OL{J`l+d+Ug|vgaJH@L~@H}jrAIuxq(O%_Q{` zku;82|DuWTXU7;Ar)%MuchaYBkncS7yP`NCjQ5A>y>&8AoVt0_0jSPyBj}57)Dd6a zg7Ol$SFTDHSPHeTT>vh&`TfQAK^9SLb9z3^L=N0v~ z#h4xmVFXifSsCqy%=Lomw0dWhg)ZkqUmprUpO;r2orvif5^2p1jsQev)_F zdvuUC$Un&C{c_-YL0$W{54&bQn$Kz~^r49UdrmU$k(0h|OV<9sO5ne{Ia1Q!bTD$H zKkESR#uMPH2f|dHyZgc=2c8Ixs=pO?lyi!EW&BVc`%12SiHd(SI(SWuSzgk{Qdm+N z>tbq$0^SiPYkBsO+D(0$q4;hGmoszr zY8Eoql_7JdOS4h>&H3A3pjB+Ps!t8+JKLFR=80b9eGj{k4a5D)!=E{f~N@*2DOhs?SS3F zq<}yvZJ_$2%g4{wL&(E72Vy3cW~b78xtgF_{b{54B=!}1Btvkt362AIA~6g1g<1#u8K5HxwXO{>33;1CQ14cVNdG;s=U7EP#A9iwK^YT z2BAYXBKn>Pn5;%bF)8hy$!vp;SSl z{h9zg=}~)Q$uN}3h@=~~1|98)(z@umrc$riyo+*!4{gI%LDxuNOXSAFH)K7K^rUV# z(a?DRbfs7xQFA6uXMHz!p6oi|-&ZdjQ&hwzFj!a@8zflgMT{T>n`q?f$5Pv2c`jb< z*g8f?{0g$Lx|KdT5cJ$e4jWjjWDA$mjtYZ}Vciz>7z(2gBr417;S=118K(S@J^#C3 z9^}7qAOdP5|L24R{)e~zq&0>aN|-N0RiBMARZP6Vr=xhXe8x}sQu%T2l=|DKOmYh) zk4)W#IjqUmUObuOmye>b9+voiZ-0x>?(rWOG**fd&TT!re|AmXF5$^ zEHfx`*q?a1$lnn5dNDCw3Dndd%|w?gJw14?8}w}^N=7!gR%yRw_o5z8++RpL2g7~M zCb;I{9(0Hn`hvf5bhiaN2sYM(G3ll%$lL$5Y zTfxmupMd+{NbA453ub}xKRDOF@;hTfGX))?R*}TmM>SeeWOn+f122(J*Z_j=-19#| zle#5+v?6B^yQhRKC{#3k62o_ z_FplQuVueaXZxht3+c(jbeMafqVBO_jS|;U3MAS9 zTds4K*uRC3UY)(UC1v{6%E4vSsKREQ`Z1rVqu%nEv*9+rJ^1`dVq2+*wsH=X zlR`xauh+6WYqvF7lY4!jlaq;9?T=i8u>FNvqbff)EpROu|6NesDmS}9^9RF{?mgM+ zRFWP$5I*3OXaA2vWef@iCgi6?L=N+@`zyPPxy%DRod|=%LX8N+L7Dle2VCV(Nj8g+ z-eKJQLf;TO!x2W_C}s1Tu1MEs8H zmvNGj5~s;IJ8A7WgBow%OLO_Ii&?sdy%v_ds+fwd6_Uwl7$BuPs-JX5VDp=4!)*7m zt*xR^&5&R_PnIQ$wGd*$QT^rB&#}o1p@fv@#zAPMaO(^t96*e++mX!?*Vgr^*jDD6 z&YEUrV#tva{7qoRmxy1TzhQwl^^HrH6UhKC=7L4s4ZQm;7bZm$D(CY?|G|uq(c+h3 zJgD<%Fzr|Mn=s7ml-abX+BbQ=2zQpiQV2>sEPS zrU+?A?5!(+&svJ0itGvzlQuqEXEuZ{n@S6UQr~AKF-zgqA?U)YSGY`TmOL1W)0?aUX-*?m?-^lC7Es`@ngf`~UXMRC7!fm3az zd?y`CjY@4RReV{tbnA_g*{UkWY5EJ=6n>*^swIT)UF&vOq4{APPV}Tdh3A0Rdudwh zWUR7{%h2tVWdrhwqh&xy3IZs+kOFQ81i}3aal@v+OrD#(ePlx1_B_r)pu^PgIHCbD}o5Ti`UymCrGMuI#7 z39@ZAbS`v2kf(gjKIh`Cvb;Sm(W_fl4~hpv{;shSAYd&wLl!mUA=Y2{Z5Qs->3x*( zc>7Ko?VmA1(Yvn&PY8Vc-!TC2uD{HYFpzqPcNswB`;R0Bgp7(PE<8&7uuo8YkGhD8 zuKl1u&w($DitiEQa}WBH@D0su&P4!~GA>CtrYTRh_U`K}@o@W!;(_FKBH@c1;XaV; z57*?AEA}#@KQjq6X}Z{J6fPgp?8o|u=l#GM7Jg5y`P7P$0-=S>SiAB{u64LdkdXfm z5o0(52s$$Knlpc5nIqx>s$e$Nk4437IO5g~U}XiUc=;)0QOQ6VYUBKd@iYd(O@V|Y z10;Iz3Q!AYGUg-?#YxE-C&uS}buw3=HwO5t=PfI(tp>(yib)Qi$QNA~)zW?1pvBhcVycYq5B00Oi_Ai_j>xBP_{N4Wuq0me4@7Ay+_gpp;e zNCM0&cEgZ6^>vXZ^=_j#Tb8=q%cA~^m$eVawZ9xogTGlPd+>#cJ8>6_ATxI@F7$c- zEB6JV(#NKeJ^EMiNL238=lEH-aZN)ky17Po_-BW2e&_n&!GYJOoiXo_yUoS#*IEdb0*zb8j&uF@U{qo}%L!86!!OhYzSH}U zTcmf6Cz#Z{Kh8h0A)N!8*nBZP@et`I*V1cwwI2`Y(!%=~s}Ymp*}$8N*`<8m3qvT^ zsIlg2+x?~r9L|gVj@>RsWkplkkyU?b(9#sP1=i~+Z?4jkms6x-Ni2Hw*oS+!qY4G5 zZh4WrQTg8)1SuDxBHBYI(h{N*54;9FXdyX`IbT4Ob$|z5z>?U9fwNl4P%cY5=$jf+{xJ19WgNQ zmY(UXqGON7k?_+!SkBP-LP5#pY?~L%qs75dPG81Czdcs-;jOqOTVLEg41R7iPg3Kzn4-CK;X+55&@JaO>!R!fFGYA)B!W+r7ni8;*d{x z2`h41mUh6?n>yg#@WmbAJI(zBC-sH%-ZnYT?9d9OZc74+*S{f+L;18zG5eiQFX`*= zlzTV%*#-8|d=}*!mxEUn;+0{AS;UW1<|}anzqPGBSlb9smgac-OS0cl(5vP+Ikak0 z`oMOX#~Bt`8~cHuN~+jlc6P7;5A^?D8NA;e&& zWFWU0qO^%H0b@T34wnOU;gk=`Hd9o0ep6kb$+Q7~=?>EYe-fG77XEP`C(3j{s1042 zp91PCa$)$<+Ep50E+VCY8l^n%;)yTaZ(ss8Ju#)Z*~}_ z4;s;~1KSlpRN6)=wbjddZ*LCJWhRU?JB(xYngAaKC{*OB*^to|zmqXvipXy5qI zmH)=}0EG!<`^QPd1!qgGs2sK-YwScCce8#%3i?i2zHTHw$(7cBO1dR(%HyWVQDP7U zo*|Eo|Cwgw>xZ1W)4O1J9+A4O3{UNdW}Rk`CqXh6 z07A*Hik9;mnyCnR5-5vYS|r+>daNnPwp6#gh#_8iLhv&ZT9h;qQBprAM1YB;Xm|*T zzx1umB-0oCT$`6`U~&iKJhj$d&nKE_CB-^d%^#yfjz!7D=MMyrE!naj=|aPTO(WW| zuQsbJ%^kJEmRWKwmcCCiL=#)cam}=l%B<4%x&^6b&{6TIxs6+g1hi-fGG=>{Pv&=e?@VlEM+x=UcV8Q|iP&Z|wCO3^o?pqgYpW&_2*EDB z+|4IgEEYd{U3HeinGPMYGT$h)4)U%ss$*P)3h8>cfY;X*1z1JIPxKw4Lb3ozc@1KT zB+J0HS4i{jpF6;~z$w`$7aiGBYZ;#P6cdYYt8%r!Ac^Q_bN`@_?fNS3pW3xIc$U!f z{Is9gCt2^bC+NdCLk)f1@MSpQL-#6BKJ_icQK>DRXbJA#Uwf0c_`dqnLbCz~%iQRJ z{m87)+LDu&ZXY}H9^&^HM?}oKH{KWm*0*$pLwF-~>Okc83bN?N039Y2ByLT1edqnv z5p#X5xJ9^HYPHttN$^H)V};y{?JlxG6Ms{U%U(MZKRbnwLQb>O^ z7kFaw(ng&f%?X`4s3)OeivmWPb@OO|k#XoCp;PdNo+MGboP z$L|ih)WD;5#JhXaclrk|hNFjnAAd{hN|E&}y&RK5TS+0KGele*0X5dxY0X9{M}#Un zdA%Sr72f&q{Qd?>(Dd=ocf=$7)3@48`9+)6Id4tEy0e6~Gs`j$JJ{N*QSTqJ+6POI zDR`@wXKh@Q@5n_^Xa%Z+v>Vu5!n-a;yAVV}v=o?)k+n|7 zxwza;#VQtQv^)jCKksXh{H~E)W%Mz8qr%IfuiQ*p`5n71`jX!e1wy5D1X}A5KH+OY z!)X)09+P2OYz^8fq4(P`$eP_ zNQB=Sm{{xY*SI=n522sT5CsUzYi7~L_qU3uvc|?-3#p(uO8e;;{ zaWV?a!F6IA6*7)}0bZzjCq48TPL6w+S-ug)8|ZRTS4;byKs*fp3AO-cFF=ik{NH$c zv)uLDIlzd4ppuNkn8*Z(al?eg36Y0@>D(^`^<;ZXi1_ZQzo?mfUAzL#?sR^BY`P(v zm+DVgTyU|FzCzYPf2e&%823&$zGsbl1L9s}CYq)U-ucE{@#Dq}f8qc5jYm<%`bl*D z%gd*ww7UFI=bY(A`2gme^^9=1#YRLDIv?d>K*9ZXs0!UEl5;3S96Fs|xoB8f6P*>2 zh;{5IwO&1@(R6zfs6J7PQOkmUO1})M-v)EAfLYj*sz2Rn-bHPPG4YNyYMQZ=iOfW^ zvQ}Qo+he$Z{N!{a+Cic}21%6#^i(ZlM3gt@E=hA;AL+0;<{mSq1-Lq9KRgoq@h@7~03`~&JaDreP~^k^YU@Y1@$i)%AmHRdaIv-M zgo+dt2YMm6H&NIXdL}U3C*Yi7Z)Q%b!rZVP&TCpsu`vi{X>VzPT}VjQw^2wW+&~?M zp#*AQq$wB-aX6+49?MCbg=#X9Z5fESKWGyA*4I!xRG?Ty<_U{9))otDo}iT&1(f}w zi0y%@>N!(K3_9PV{B^cEvnfG{ zS)zvv*?xhaWW86%=Hl)}*Ywf)Zf?I8)Rb4t_X`7)uynG=*r_KLfzyJh1X0HvzfQSqDNhmPB5a7!F z?->M240Nz(jD!rtL$veaz@-;X3MfMLbs$*Xe=S9Potj3JQjAv;iU(WkNhLBTi``U1F{e1<0fjI`#^{5YV4zQZYN z2eNI+a;ns?lu=BRBJU7)&^0l~wH`L$IB19oLm|f(w2o7tW+_SNB>)mN&JpNnkghZ6 z30q+-K}Muw*fAuTRa@U1*PwFII+lZ+F1In#psR>U!6KD~ceErivhkv$q?2tD%u$~I^LY7D9|UA`Nw#1FeK!v-I^$LJkdB52Ydn^j}tV}yy}HnFF==UuJv9`n2^ z6Rt-bDTAgPx`6-Kik9TMPU7zNDC9$pF{qNH3JuoDzG=m%={h|E%u|WLQFeccDpIt> zr+|YQf=_n{zP(apu%-A$Q;R0AS{_M8sPLWyWPkyDP`U7!7 zD(ITw@l^D9C#RJvOpPW#5^uZ%XF88_JEH@@d8JxDCn#o!eG%d{65KyF%Pfg=g_qq* z9E?adY1KL@nUs(aSBAsR-8G}^qM%`*D4{G?ukvD`_``H;66%PIviWleL$Z$*kz{%R?JAa{L+Z4wmW3bMMRz7FSa+k(Dse39-b{!o$_E$2$O zxbw;S%Ea4m+O1UY&qvHn#SIUwp%6o<~5xL(Kq++2HV%x zUivQRXk6B1MSYQW#EVhoe@;e*%ae6@5sT^dlhDFe6Z)nCO~T3~MBxl1$fLNBB;8w> zBu_nr^YL`QL$ScT24N#Q+(-ZLiJxIqg>lQ~;l>aQwf0kQ!ALxvTrs@YBbaZPLiP!N zF7w#+)6d%h2OAIXrfbOKlw=@+0|s8$H!H2EKc*oMZNb!={^5q4-fK!;srvQ96-|2M zx=+>eZvEMqhe459%hJ4Q$es0W51ISb`m5s19RIBC-|s+fho}cHu&%#_fT3cs>>_2 zJmq1{XFsLwTP05nJ?egJe@v;L)E@Pja$lJY*>IE|PC66Ibb_6CY$;a-z~<^e3Lm z`T$w%(hwr`i;S5x_r`6YxF;y-H>}$>ugq=t;}q&%-^_N0o&35e_EVFF4YwPw=PLGEfHyvySIa6RS?$kvQ%iFh}u2c8rVRIKNnubFTl)K$wqmd3w9&ef= zVNTZb-+RQJ>>f|t5&e1U^eM>-03I41H?s~)^QgIr%wxSqEH?mVxH1sD-+1X%sfn)&#|4UE+IDcH$AOPn-M+oRk{})31 zdltpbzUDgmW#CDb?`MLZvth=cNWNYRdK+)A$;dAlC+D3S;6&gDzlaebE!hKrq{;J! z#A=?`Vz<8bAcQ_-zZwGw!g4y9oOA<6m0=ZSz#}BAnxM%I&vI%`yFE1yH$sBlC!5zS zE19;S4T>Ao$P{|f!Yo2GsJ?lIoh9dsOUO_&Lq(ejdeyOwAQLgsmlavWdpSm_AS(e^ zgw+^e;)J|A6}MRiwUjD&1@Dm;fYA9f@>gR`i`Ez9c7dqJ4YIq138Ge_i8?W$E9(cL zLL{AGK7h(3Lo=5w1n`$u(xDfELuR8^WV$#CBXXi*!Y_#5+-M)$ET`|+UDczU11y+6&_YAA!Pa#=yyV zMQ?uU`+vc&OZu=JsD%uGUKr2Z#z#NERDnpDkf_E<4-^w*=wL=aAq;samce{FzBrHx zj;}shg80DjN$NRbc1T%{l@BP6RWxC5h>iGp=+-iXlb(10*+umwUv~m1@^GA7C(f7}>&RNEK8R z0~ic&)9`>G6}2)Ei^NIMem)&2^fjXgBCfz#h=|A1#_yp46gYa)_K4_7X~&vP zdl`s4N|Sm9tI|Sna#y`(=(|}{J>>b%O#w&{g=a}35azl*xP5L2<7dM1c7vu3vjqs!(9YF?cS&fZ(|X0(RAtyk81r2m%+D~$*!GVups z=3$61akVXft&Mhn-Ce*9U7@GB& z%}JOO9!?5gR{|8_8mzsvW!#(gGymXg((-*M{G(rdRc=!qsXdk1^3ifSVxAoY^VRgcp_|h(31L5onlb|9c)>l!^6=;*iag5 zc~61Z-Fmq|F>fK+6hgTrpsX;V?MK-}!UHB+e(}B)TQCMKWW4*3fAevPJK3lKd0ny# zGS~d5iwDbMjf)s^?Wa0V z+532@<59v>SMQmT!Jm$qX@~Zi8J9G!RzM#5|N5i<3R_sQ^BJ<<<{w=F%^80`eK1KT zAtGebxY$?iDplAr5N-Osc>1W^E%n@IV&4)DzD(VKz9;o!i}dG(2y5_TC)Z&~VsEZ_ zjHy^!rzH(_Cv3uOtu%-79W?2U3zdjmffqicE zkgi@03&=vCU7qc9vocr6Sk<`mi;9HA-Kh0w=eDr)e?kITM_OZv6D5Js&gLgp& z{6az2MsVAoiPbT4k;n-a=lv;0THiY)!!c#VV1-yF@0d^L zQ3m5^G7dG_Amc9&4BA!BPId(A7pr3@KvoRHu=-)E?T8?}bb%g+r1{xTY}f2*jp)PYtI{1IYCEHo<<=7_0#`hGrgVI$Pbd)bf=_X$$2r%@ z#e1k8+hZLdYGx5-8q%3(Pb^`p)mngduwk^Wl`W5tct{uD6?LRU&g4802vQs;U$BrX z<_?i0WmHV{KL{*Lh_k3bX7vaoi})!MmrgdtP`1mVcuYn*RkeI0Whk5{9qxntExK(& z8m>(lM;FU|8YgJg!>1aXaE|`|hH~ zklGZFS^mfOX)%1pfzTsZccw@c9YNQ&3C^5qiqa}h*saM8WP`n5*ITXM2mQe%C5y1| zF-r0ZhU1)8dGI3mh%kOh+q~O~CXzb_T7NG=l$>^HZ82=VtJsf8<8~oj+5ZoxF}%he&dKoRGoz9tp|50lv{E{2f75 z?ggh%`D-PAQyL& zMDINMG+K#lUlB%~BzUvPxB?M`U+Qt&Ehk#i65)?-0+RO$pEYHR2}n$(*e(=Ly79%eO<_iH-` zE>qGI_Il1u4793J^lY`&=Y?bzI{CgyKk3kRFRDzgqfPH($fvv=6O3RPgy@7$3I|rI zMM7XIDI8}N}r5>l#`!$p@;X(aCkpc3>p^k znrlP^;sJm#|KlgH;sSv3fhY-Kz=Z)m3jj-D_jm4bLjaSDeY|)$I~z4kxsI@>=}GLa z(AhY`A4IPSqUZJTiEKWD1be8up_BxgAc+t{M%ljc9jtJD-~JZ4)!cQOoF6N>TreCtrdwv)we)~2oI<3JH{w0ly+p9 z&d&Gb#;~78B~kYea=vyW^tO)g8@)cQaWeUAoJJ?U1Y)mvm|D@(9E5kq6pUKu;!i%T zWYTNVm5keC$yHc+EIMr|$Mz%0>~e&~vyl2P$O2c1gRSX(kqsbUYI~*b8r{^$efaKJhJP}%F@4FOV6H^^ zE4Bo1w`o`@Q*uTYa$m>&@vR;|Z+b>$K9g+sp#C;~;Sgk71W@})&=b3{C2^mByYf|`sC-V4+w9#% zA^TwPt%jx#{>!3uJ--CFOaS#(m{=4F34TL zT;CSFMz_H}gMHNv^$uY_&4YU8-4#pr)nfHsxyKM3^92+F_s1RyIOE^+-s7QsiyEIb zp-gbs!g`VvYKWG8y{3Ty4yH!!@G)cY1K3@8lIP_sn*1V zK^sIxp|w4-4Xta>)KeEdWhfo2sT(fmtGDnICV~{a^?!gI-uov_O)T5WE@tCNGQB1^zKVrRA`N6Ve$xYKNU(c!|6;IUa{+Mj?h1GJy z(ch*jiCM(MB)ScNj<~>!p(oDG0JV^@TbHo3G(XQUY8xLsL$<{hIHgwPf+`RrhWOpx z!0;VBqML}`B-A5_!oYX_Ug=AA) zd~)mJdcF7Ge`& z+ruha!a_d3{K*(1V)sjtra}X0K>JaO-H*y-j>?d08r$+vtc7`K0862m@&Y%>sD?#q zGTw)7MfK>k&6%Z;NtOvfPp8|f!$Fx&6_SfUW{3z+;W{!Yhfn4yn<8}HMjRllnLfNa`Z`reB#eh#)O3(@_L$+Gh3hH@nQOQ$jSH7 znmi9MQdtXwDC=pTOhp9t717(XD88So6Mw1v~d!?nW93X{0=ZBC*m zxo0t?FQw6opxEC+Rg&EZu9J#oSYU;)QAA7gP`F=NWMe%b2`{xD(a>Mfjj!N|ujR+z zdc#yu#!0FG08c=l#Jg?Z*WpHY@H4)}dV3|>{04+d)=E9gN)}7hV{(&)SXO!UQf7%# zM2IW|nL))sfc?Hwrc4ly#KD!(BjYqQZjq!F-TDZrt!<3W`}?)>MZVKxU49qV3U9v;swcpbaJzoTF#Zan zf4reMj#b=$yEo%LhtFM3|Ah_?s1URqANoB0LJ`zE1OGTie&=#I`<{1;ZKQQLMGy6r zJ7x8Ga@0dJ`BE5q=kOC3rJ<63aJIsG)ETV3h$M0Epu( zD9OPx8PF$_Ct|qZANH;9k+8Xv(GGcCs?!x_S;>1#jc5DNT09E$14rtRtMNm|8A4ec&h`c1=1QQ+C$Y$l+S4ae@q_Vpv*R#7LCf(zq1r z6JpQd_bthyITl^KA+8N^>c$S%Wu1)z5y$H3dlH(5<{rO56JLFRqJG294$6Hk*NEL+ z^*MT!W=qDfoUhqKM%qMXN3SUrskk60z?Sgvuzd+=RJs5Zu^f2ctu(hDbtRvW zR>@o>p3(t^)rvYvb14I^y14I1Q(|~5rc9%b8rnWbzw#oHjTDj)v~nt9zzxJAuy_